Control Tickets: type column (bug/feature badge) + type filter

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-06-15 11:35:56 +10:00
parent 0c7d5c7382
commit e1ddcd201f

View File

@@ -76,6 +76,13 @@ function statusPill(s) {
return el('span', { class: 'badge', style: { background: 'transparent', border: `1px solid ${colors[s] || 'var(--border)'}`, color: colors[s] || 'var(--muted)' } }, s || '—'); return el('span', { class: 'badge', style: { background: 'transparent', border: `1px solid ${colors[s] || 'var(--border)'}`, color: colors[s] || 'var(--muted)' } }, s || '—');
} }
function typeBadge(t) {
const type = t === 'feature' ? 'feature' : 'bug';
const c = type === 'feature' ? '#7aa2e0' : '#e0b24b';
return el('span', { class: 'badge', style: { background: 'transparent', border: `1px solid ${c}`, color: c } },
type === 'feature' ? '✨ feature' : '🐞 bug');
}
// ---- group cache (used by approve/instances dropdowns) ---------------------- // ---- group cache (used by approve/instances dropdowns) ----------------------
let groupsCache = []; let groupsCache = [];
@@ -298,24 +305,31 @@ async function renderReleases(panel) {
async function renderTickets(panel) { async function renderTickets(panel) {
let filter = 'open'; let filter = 'open';
let typeFilter = '';
const list = el('div'); const list = el('div');
const detail = el('div', { style: { marginTop: '0.9rem' } }); const detail = el('div', { style: { marginTop: '0.9rem' } });
const statusSel = select([{ value: '', label: 'all' }, 'open', 'closed'], filter); const statusSel = select([{ value: '', label: 'all' }, 'open', 'closed'], filter);
statusSel.onchange = () => { filter = statusSel.value; loadList(); }; statusSel.onchange = () => { filter = statusSel.value; loadList(); };
const typeSel = select([{ value: '', label: 'all' }, { value: 'bug', label: '🐞 bug' }, { value: 'feature', label: '✨ feature' }], typeFilter);
typeSel.onchange = () => { typeFilter = typeSel.value; loadList(); };
async function loadList() { async function loadList() {
mount(list, el('p', { class: 'muted' }, 'Loading tickets…')); mount(list, el('p', { class: 'muted' }, 'Loading tickets…'));
const qs = [];
if (filter) qs.push(`status=${encodeURIComponent(filter)}`);
if (typeFilter) qs.push(`type=${encodeURIComponent(typeFilter)}`);
let rows; let rows;
try { rows = await api.get(`${A}/tickets${filter ? `?status=${encodeURIComponent(filter)}` : ''}`); } try { rows = await api.get(`${A}/tickets${qs.length ? `?${qs.join('&')}` : ''}`); }
catch (e) { return mount(list, errBox(e)); } catch (e) { return mount(list, errBox(e)); }
rows = rows.slice().sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0)); rows = rows.slice().sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0));
const body = rows.map(t => el('tr', {}, const body = rows.map(t => el('tr', {},
td(el('a', { href: '#', onclick: (e) => { e.preventDefault(); openTicket(t.id); } }, el('strong', {}, t.subject || t.title || `Ticket #${t.id}`))), td(el('a', { href: '#', onclick: (e) => { e.preventDefault(); openTicket(t.id); } }, el('strong', {}, t.subject || t.title || `Ticket #${t.id}`))),
td(typeBadge(t.type)),
td(el('span', { class: 'muted', style: { fontSize: '0.72rem' } }, t.label || t.email || '—')), td(el('span', { class: 'muted', style: { fontSize: '0.72rem' } }, t.label || t.email || '—')),
td(statusPill(t.status)), td(statusPill(t.status)),
td(el('span', { class: 'muted', style: { fontSize: '0.72rem' } }, fmtTime(t.created_at))))); td(el('span', { class: 'muted', style: { fontSize: '0.72rem' } }, fmtTime(t.created_at)))));
mount(list, rows.length ? table(['Subject', 'From', 'Status', 'Created'], body) : el('p', { class: 'muted' }, 'No tickets.')); mount(list, rows.length ? table(['Subject', 'Type', 'From', 'Status', 'Created'], body) : el('p', { class: 'muted' }, 'No tickets.'));
} }
async function openTicket(id) { async function openTicket(id) {
@@ -348,6 +362,7 @@ async function renderTickets(panel) {
el('div', { class: 'card', style: { display: 'grid', gap: '0.6rem' } }, el('div', { class: 'card', style: { display: 'grid', gap: '0.6rem' } },
el('div', { class: 'term-bar' }, el('div', { class: 'term-bar' },
el('span', { class: 'term-title' }, t.subject || t.title || `Ticket #${id}`), el('span', { class: 'term-title' }, t.subject || t.title || `Ticket #${id}`),
typeBadge(t.type),
statusPill(t.status), statusPill(t.status),
el('span', { style: { marginLeft: 'auto' } }, el('span', { style: { marginLeft: 'auto' } },
btn(t.status === 'closed' ? 'Reopen' : 'Close', () => patch({ status: t.status === 'closed' ? 'open' : 'closed' }, 'status updated'), 'ghost'))), btn(t.status === 'closed' ? 'Reopen' : 'Close', () => patch({ status: t.status === 'closed' ? 'open' : 'closed' }, 'status updated'), 'ghost'))),
@@ -364,6 +379,7 @@ async function renderTickets(panel) {
el('div', { class: 'term-bar' }, el('span', { class: 'term-title' }, '◆ Tickets'), el('div', { class: 'term-bar' }, el('span', { class: 'term-title' }, '◆ Tickets'),
el('span', { style: { marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: '0.4rem' } }, el('span', { style: { marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: '0.4rem' } },
el('span', { class: 'muted', style: { fontSize: '0.78rem' } }, 'status'), statusSel, el('span', { class: 'muted', style: { fontSize: '0.78rem' } }, 'status'), statusSel,
el('span', { class: 'muted', style: { fontSize: '0.78rem' } }, 'type'), typeSel,
btn('Refresh', () => loadList(), 'ghost'))), btn('Refresh', () => loadList(), 'ghost'))),
list, detail); list, detail);
loadList(); loadList();