fix: child toggle always visible; showDetail fetches missing tickets; extraTickets cache

- Toggle now shows whenever a ticket has children (not gated on local filter visibility)
- showDetail() is async and fetches from API when ticket isn't in the local array,
  caching results in extraTickets so subsequent navigation and row rendering work
- Sidepane async child fetch stores results in extraTickets for the same reason

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
caoimhinr 2026-05-27 10:05:30 +02:00
parent 39bb53a72b
commit b068699f80

View file

@ -527,6 +527,7 @@ let currentTicket = null;
let expandedParents = new Set(); let expandedParents = new Set();
let currentAttach = null; let currentAttach = null;
let tickets = []; let tickets = [];
let extraTickets = {};
let attachments = []; let attachments = [];
let environments = []; let environments = [];
let currentEnvironmentId = null; let currentEnvironmentId = null;
@ -699,17 +700,20 @@ async function loadTickets() {
} }
} }
function getTicketById(id) {
return tickets.find(t => t.id === id) || extraTickets[id] || null;
}
function renderTicketRow(t) { function renderTicketRow(t) {
const visibleChildIds = (t.child_ids || []).filter(cid => tickets.some(c => c.id === cid));
const totalChildCount = (t.child_ids || []).length; const totalChildCount = (t.child_ids || []).length;
const visibleCount = visibleChildIds.length; const knownChildIds = (t.child_ids || []).filter(cid => getTicketById(cid));
const expanded = expandedParents.has(t.id); const expanded = expandedParents.has(t.id);
return ` return `
<div class="ticket-row" onclick="showDetail(${t.id}); event.stopPropagation()"> <div class="ticket-row" onclick="showDetail(${t.id}); event.stopPropagation()">
<div class="tr-top"> <div class="tr-top">
<span class="tr-id">${t.human_id}</span> <span class="tr-id">${t.human_id}</span>
<span class="tr-title">${escHtml(t.title)}</span> <span class="tr-title">${escHtml(t.title)}</span>
${totalChildCount ? `${visibleCount ? `<button class="child-toggle" onclick="toggleChildren(${t.id}); event.stopPropagation()" title="${expanded ? 'Collapse' : 'Expand'} child tickets">${expanded ? '▾' : '▸'}</button>` : ''}<span class="child-count">${totalChildCount}</span>` : ''} ${totalChildCount ? `<button class="child-toggle" onclick="toggleChildren(${t.id}); event.stopPropagation()" title="${expanded ? 'Collapse' : 'Expand'} child tickets">${expanded ? '▾' : '▸'}</button><span class="child-count">${totalChildCount}</span>` : ''}
</div> </div>
<div class="tr-bottom"> <div class="tr-bottom">
${typeBadge(t.type)} ${typeBadge(t.type)}
@ -722,7 +726,7 @@ function renderTicketRow(t) {
${t.attachment_count ? `<span class="tr-sep">·</span><span class="att-chip">${t.attachment_count} file${t.attachment_count !== 1 ? 's' : ''}</span>` : ''} ${t.attachment_count ? `<span class="tr-sep">·</span><span class="att-chip">${t.attachment_count} file${t.attachment_count !== 1 ? 's' : ''}</span>` : ''}
</div> </div>
</div> </div>
${visibleCount && expanded ? `<div class="child-rows">${visibleChildIds.map(cid => renderTicketRow(tickets.find(c => c.id === cid))).join('')}</div>` : ''} ${knownChildIds.length && expanded ? `<div class="child-rows">${knownChildIds.map(cid => renderTicketRow(getTicketById(cid))).join('')}</div>` : ''}
`; `;
} }
@ -753,9 +757,14 @@ function renderList() {
list.innerHTML = roots.map(renderTicketRow).join(''); list.innerHTML = roots.map(renderTicketRow).join('');
} }
function showDetail(id) { async function showDetail(id) {
currentTicket = tickets.find(t => t.id === id); currentTicket = getTicketById(id);
if (!currentTicket) return; if (!currentTicket) {
try {
currentTicket = await api(`/api/tickets/${id}`);
extraTickets[id] = currentTicket;
} catch(e) { return; }
}
const t = currentTicket; const t = currentTicket;
document.getElementById('d-human-id').textContent = t.human_id; document.getElementById('d-human-id').textContent = t.human_id;
document.getElementById('d-title').textContent = t.title; document.getElementById('d-title').textContent = t.title;
@ -782,16 +791,17 @@ function showDetail(id) {
if (t.child_ids && t.child_ids.length) { if (t.child_ids && t.child_ids.length) {
const childList = document.getElementById('d-children-list'); const childList = document.getElementById('d-children-list');
childList.innerHTML = t.child_ids.map(cid => { childList.innerHTML = t.child_ids.map(cid => {
const child = tickets.find(c => c.id === cid); const child = getTicketById(cid);
const label = child ? `${child.human_id} ${escHtml(child.title)}` : `#${cid}`; const label = child ? `${child.human_id} ${escHtml(child.title)}` : `#${cid}`;
return `<a class="child-link" id="child-ref-${cid}" onclick="showDetail(${cid}); event.stopPropagation()">${label}</a>`; return `<a class="child-link" id="child-ref-${cid}" onclick="showDetail(${cid}); event.stopPropagation()">${label}</a>`;
}).join(''); }).join('');
childWrap.style.display = ''; childWrap.style.display = '';
// Fetch any children that aren't in the current filtered ticket list // Fetch any children not in the local ticket list and cache them
t.child_ids.forEach(async cid => { t.child_ids.forEach(async cid => {
if (!tickets.find(c => c.id === cid)) { if (!getTicketById(cid)) {
try { try {
const child = await api(`/api/tickets/${cid}`); const child = await api(`/api/tickets/${cid}`);
extraTickets[child.id] = child;
const el = document.getElementById(`child-ref-${cid}`); const el = document.getElementById(`child-ref-${cid}`);
if (el) el.textContent = `${child.human_id} ${child.title}`; if (el) el.textContent = `${child.human_id} ${child.title}`;
} catch(e) { /* keep fallback label */ } } catch(e) { /* keep fallback label */ }