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 currentAttach = null;
let tickets = [];
let extraTickets = {};
let attachments = [];
let environments = [];
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) {
const visibleChildIds = (t.child_ids || []).filter(cid => tickets.some(c => c.id === cid));
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);
return `
<div class="ticket-row" onclick="showDetail(${t.id}); event.stopPropagation()">
<div class="tr-top">
<span class="tr-id">${t.human_id}</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 class="tr-bottom">
${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>` : ''}
</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('');
}
function showDetail(id) {
currentTicket = tickets.find(t => t.id === id);
if (!currentTicket) return;
async function showDetail(id) {
currentTicket = getTicketById(id);
if (!currentTicket) {
try {
currentTicket = await api(`/api/tickets/${id}`);
extraTickets[id] = currentTicket;
} catch(e) { return; }
}
const t = currentTicket;
document.getElementById('d-human-id').textContent = t.human_id;
document.getElementById('d-title').textContent = t.title;
@ -782,16 +791,17 @@ function showDetail(id) {
if (t.child_ids && t.child_ids.length) {
const childList = document.getElementById('d-children-list');
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}`;
return `<a class="child-link" id="child-ref-${cid}" onclick="showDetail(${cid}); event.stopPropagation()">${label}</a>`;
}).join('');
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 => {
if (!tickets.find(c => c.id === cid)) {
if (!getTicketById(cid)) {
try {
const child = await api(`/api/tickets/${cid}`);
extraTickets[child.id] = child;
const el = document.getElementById(`child-ref-${cid}`);
if (el) el.textContent = `${child.human_id} ${child.title}`;
} catch(e) { /* keep fallback label */ }