gabriel / musehub public
issue-list.ts typescript
146 lines 6.3 KB
Raw
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