Control Tickets: type column (bug/feature badge) + type filter
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user