issue-list.ts
typescript
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2
feat: add repair-commit wire endpoint (API parity with repa…
Opus 4.8
minor
⚠ breaking
1 day ago
| 1 | /** |
| 2 | * issue-list.ts — MuseHub issue list page module. |
| 3 | * |
| 4 | * Handles: |
| 5 | * - Body preview helper |
| 6 | * - Issue template picker (pre-fills new-issue form) |
| 7 | * - Bulk selection toolbar |
| 8 | * - Repo nav hydration |
| 9 | * |
| 10 | * Data expected in #page-data: |
| 11 | * { "page": "issue-list", "repo_id": "..." } |
| 12 | * |
| 13 | * Registered as: window.MusePages['issue-list'] |
| 14 | */ |
| 15 | |
| 16 | import { initRepoPage, type RepoPageData } from './repo-page.ts'; |
| 17 | |
| 18 | export function bodyPreview(text: string, maxLen = 120): string { |
| 19 | if (!text) return ''; |
| 20 | const stripped = text.replace(/[#*`>\-_]/g, '').trim(); |
| 21 | return stripped.length > maxLen ? stripped.slice(0, maxLen) + '…' : stripped; |
| 22 | } |
| 23 | |
| 24 | const ISSUE_TEMPLATES = [ |
| 25 | { id: 'blank', icon: '📝', title: 'Blank Issue', description: 'Start with a clean slate.', body: '' }, |
| 26 | { id: 'bug', icon: '🐛', title: 'Bug Report', description: "Something isn't working as expected.", body: '## What happened?\n\n\n## Steps to reproduce\n\n1. \n2. \n3. \n\n## Expected behaviour\n\n\n## Actual behaviour\n\n' }, |
| 27 | { id: 'feature', icon: '✨', title: 'Feature Request', description: 'Suggest a new musical idea or capability.', body: '## Summary\n\n\n## Motivation\n\n\n## Proposed approach\n\n' }, |
| 28 | { id: 'arrangement', icon: '🎵', title: 'Arrangement Issue', description: 'Track needs musical arrangement work.', body: '## Track / Section\n\n\n## Current arrangement\n\n\n## Desired arrangement\n\n\n## Musical context\n\n' }, |
| 29 | { id: 'theory', icon: '🎼', title: 'Music Theory', description: 'Related to harmony, rhythm, or theory decisions.', body: '## Theory concern\n\n\n## Affected section / instrument\n\n\n## Suggested resolution\n\n' }, |
| 30 | ]; |
| 31 | |
| 32 | const selectedIssues = new Set<string>(); |
| 33 | |
| 34 | export function showTemplatePicker(): void { |
| 35 | const panel = document.getElementById('create-issue-panel'); |
| 36 | const picker = document.getElementById('template-picker'); |
| 37 | if (!panel || !picker) return; |
| 38 | picker.style.display = ''; |
| 39 | panel.style.display = 'none'; |
| 40 | } |
| 41 | |
| 42 | export function selectTemplate(tplId: string): void { |
| 43 | const tpl = ISSUE_TEMPLATES.find((t) => t.id === tplId); |
| 44 | if (!tpl) return; |
| 45 | const bodyEl = document.getElementById('issue-body') as HTMLTextAreaElement | null; |
| 46 | if (bodyEl) bodyEl.value = tpl.body; |
| 47 | const picker = document.getElementById('template-picker'); |
| 48 | if (picker) picker.style.display = 'none'; |
| 49 | const panel = document.getElementById('create-issue-panel'); |
| 50 | if (panel) panel.style.display = ''; |
| 51 | const titleEl = document.getElementById('issue-title') as HTMLInputElement | null; |
| 52 | if (titleEl) titleEl.focus(); |
| 53 | } |
| 54 | |
| 55 | export function toggleIssueSelect(issueId: string, checked: boolean): void { |
| 56 | if (checked) { selectedIssues.add(issueId); } else { selectedIssues.delete(issueId); } |
| 57 | updateBulkToolbar(); |
| 58 | } |
| 59 | |
| 60 | function updateBulkToolbar(): void { |
| 61 | const toolbar = document.getElementById('bulk-toolbar'); |
| 62 | const countEl = document.getElementById('bulk-count'); |
| 63 | if (!toolbar || !countEl) return; |
| 64 | const n = selectedIssues.size; |
| 65 | if (n > 0) { |
| 66 | toolbar.classList.add('visible'); |
| 67 | countEl.textContent = n === 1 ? '1 issue selected' : `${n} issues selected`; |
| 68 | } else { |
| 69 | toolbar.classList.remove('visible'); |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | export function deselectAll(): void { |
| 74 | selectedIssues.clear(); |
| 75 | document.querySelectorAll('.issue-row-check').forEach((c) => { (c as HTMLInputElement).checked = false; }); |
| 76 | updateBulkToolbar(); |
| 77 | } |
| 78 | |
| 79 | export function bulkClose(): void { if (selectedIssues.size > 0 && confirm(`Close ${selectedIssues.size} issue(s)?`)) location.reload(); } |
| 80 | export function bulkReopen(): void { if (selectedIssues.size > 0 && confirm(`Reopen ${selectedIssues.size} issue(s)?`)) location.reload(); } |
| 81 | export function bulkAssignLabel(): void { const s = document.getElementById('bulk-label-select') as HTMLSelectElement; if (!s?.value) { alert('Please select a label first.'); return; } if (selectedIssues.size > 0) location.reload(); } |
| 82 | |
| 83 | export function initIssueList(data: RepoPageData): void { |
| 84 | initRepoPage(data); |
| 85 | |
| 86 | // Filter form auto-submit: label checkboxes, assignee selects, sort radios |
| 87 | document.querySelectorAll<HTMLElement>('[data-filter-select]').forEach((el) => { |
| 88 | el.addEventListener('change', () => (el.closest('form') as HTMLFormElement)?.requestSubmit()); |
| 89 | }); |
| 90 | |
| 91 | // Strip empty-valued params before HTMX fires, so the URL stays clean |
| 92 | // (e.g. avoid ?state=open&sort=newest&label=&assignee= when selects are at default) |
| 93 | document.addEventListener('htmx:configRequest', (e) => { |
| 94 | const evt = e as CustomEvent; |
| 95 | const form = (evt.target as HTMLElement)?.closest?.('.isl-strip-filters'); |
| 96 | if (!form) return; |
| 97 | const params = evt.detail.parameters as Record<string, string>; |
| 98 | for (const key of Object.keys(params)) { |
| 99 | if (params[key] === '') delete params[key]; |
| 100 | } |
| 101 | }); |
| 102 | |
| 103 | // Author input with debounce |
| 104 | const searchInput = document.querySelector<HTMLInputElement>('[data-search-input]'); |
| 105 | if (searchInput) { |
| 106 | let t: ReturnType<typeof setTimeout>; |
| 107 | searchInput.addEventListener('input', () => { |
| 108 | clearTimeout(t); |
| 109 | t = setTimeout(() => (searchInput.closest('form') as HTMLFormElement)?.requestSubmit(), 300); |
| 110 | }); |
| 111 | } |
| 112 | |
| 113 | // Issue row checkbox — delegated so it works after HTMX swaps |
| 114 | document.addEventListener('change', (e) => { |
| 115 | const el = (e.target as HTMLElement).closest<HTMLInputElement>('[data-issue-toggle]'); |
| 116 | if (!el) return; |
| 117 | toggleIssueSelect(el.dataset.issueToggle!, (el as HTMLInputElement).checked); |
| 118 | }); |
| 119 | |
| 120 | // Bulk action buttons |
| 121 | document.addEventListener('click', (e) => { |
| 122 | const el = (e.target as HTMLElement).closest<HTMLElement>('[data-bulk-action]'); |
| 123 | if (!el) return; |
| 124 | const action = el.dataset.bulkAction; |
| 125 | if (action === 'assign-label') bulkAssignLabel(); |
| 126 | else if (action === 'close') bulkClose(); |
| 127 | else if (action === 'reopen') bulkReopen(); |
| 128 | else if (action === 'deselect') deselectAll(); |
| 129 | }); |
| 130 | |
| 131 | // Template picker actions |
| 132 | document.addEventListener('click', (e) => { |
| 133 | const el = (e.target as HTMLElement).closest<HTMLElement>('[data-action]'); |
| 134 | if (!el) return; |
| 135 | const action = el.dataset.action; |
| 136 | if (action === 'show-template-picker') { |
| 137 | showTemplatePicker(); |
| 138 | } else if (action === 'hide-template-picker') { |
| 139 | const picker = document.getElementById('template-picker'); |
| 140 | if (picker) picker.style.display = 'none'; |
| 141 | } else if (action === 'select-template') { |
| 142 | const tid = el.dataset.templateId; |
| 143 | if (tid) selectTemplate(tid); |
| 144 | } |
| 145 | }); |
| 146 | } |
File History
1 commit
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2
feat: add repair-commit wire endpoint (API parity with repa…
Opus 4.8
minor
⚠
1 day ago