diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index 952c649a..6d56704d 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -219,6 +219,7 @@ export function IssuesList({ return getViewState(scopedKey); }); const [assigneePickerIssueId, setAssigneePickerIssueId] = useState(null); + const [collapsedParents, setCollapsedParents] = useState>(new Set()); const [assigneeSearch, setAssigneeSearch] = useState(""); const [issueSearch, setIssueSearch] = useState(initialSearch ?? ""); const deferredIssueSearch = useDeferredValue(issueSearch); @@ -320,251 +321,6 @@ export function IssuesList({ setAssigneeSearch(""); }, [onUpdateIssue]); - const listContent = useMemo(() => { - if (viewState.viewMode === "board") { - return ( - - ); - } - - return groupedContent.map((group) => ( - { - updateView({ - collapsedGroups: open - ? viewState.collapsedGroups.filter((k) => k !== group.key) - : [...viewState.collapsedGroups, group.key], - }); - }} - > - {group.label && ( -
- - - - {group.label} - - - -
- )} - - {group.items.map((issue) => ( - { - e.preventDefault(); - e.stopPropagation(); - }} - > - onUpdateIssue(issue.id, { status: s })} - /> - - )} - desktopMetaLeading={( - <> - { - e.preventDefault(); - e.stopPropagation(); - }} - > - onUpdateIssue(issue.id, { status: s })} - /> - - - {issue.identifier ?? issue.id.slice(0, 8)} - - {liveIssueIds?.has(issue.id) && ( - - - - - - - Live - - - )} - - )} - mobileMeta={timeAgo(issue.updatedAt)} - desktopTrailing={( - <> - {(issue.labels ?? []).length > 0 && ( - - {(issue.labels ?? []).slice(0, 3).map((label) => ( - - {label.name} - - ))} - {(issue.labels ?? []).length > 3 && ( - - +{(issue.labels ?? []).length - 3} - - )} - - )} - { - setAssigneePickerIssueId(open ? issue.id : null); - if (!open) setAssigneeSearch(""); - }} - > - - - - e.stopPropagation()} - onPointerDownOutside={() => setAssigneeSearch("")} - > - setAssigneeSearch(e.target.value)} - autoFocus - /> -
- - {currentUserId && ( - - )} - {(agents ?? []) - .filter((agent) => { - if (!assigneeSearch.trim()) return true; - return agent.name - .toLowerCase() - .includes(assigneeSearch.toLowerCase()); - }) - .map((agent) => ( - - ))} -
-
-
- - )} - trailingMeta={formatDate(issue.createdAt)} - /> - ))} -
-
- )); - }, [ - agents, - agentName, - assigneePickerIssueId, - assigneeSearch, - assignIssue, - currentUserId, - filtered, - groupedContent, - issueLinkState, - liveIssueIds, - newIssueDefaults, - onUpdateIssue, - openNewIssue, - updateView, - viewState.collapsedGroups, - ]); return (
@@ -870,7 +626,261 @@ export function IssuesList({ /> )} - {listContent} + {viewState.viewMode === "board" ? ( + + ) : ( + groupedContent.map((group) => ( + { + updateView({ + collapsedGroups: open + ? viewState.collapsedGroups.filter((k) => k !== group.key) + : [...viewState.collapsedGroups, group.key], + }); + }} + > + {group.label && ( +
+ + + + {group.label} + + + +
+ )} + + {(() => { + const itemIds = new Set(group.items.map((i) => i.id)); + const roots = group.items.filter((i) => !i.parentId || !itemIds.has(i.parentId)); + const childMap = new Map(); + for (const item of group.items) { + if (item.parentId && itemIds.has(item.parentId)) { + const arr = childMap.get(item.parentId) ?? []; + arr.push(item); + childMap.set(item.parentId, arr); + } + } + + const renderIssueRow = (issue: Issue, isChild: boolean) => { + const children = childMap.get(issue.id) ?? []; + const hasChildren = children.length > 0; + const isExpanded = !collapsedParents.has(issue.id); + const toggleCollapse = (e: { preventDefault: () => void; stopPropagation: () => void }) => { + e.preventDefault(); + e.stopPropagation(); + setCollapsedParents((prev) => { + const next = new Set(prev); + if (next.has(issue.id)) next.delete(issue.id); else next.add(issue.id); + return next; + }); + }; + + return ( +
+ + + + ) : ( + { e.preventDefault(); e.stopPropagation(); }}> + onUpdateIssue(issue.id, { status: s })} /> + + ) + } + desktopMetaLeading={( + <> + {hasChildren ? ( + + ) : ( + + )} + { e.preventDefault(); e.stopPropagation(); }} + > + onUpdateIssue(issue.id, { status: s })} /> + + + {issue.identifier ?? issue.id.slice(0, 8)} + + {liveIssueIds?.has(issue.id) && ( + + + + + + + Live + + + )} + + )} + mobileMeta={timeAgo(issue.updatedAt)} + desktopTrailing={( + <> + {(issue.labels ?? []).length > 0 && ( + + {(issue.labels ?? []).slice(0, 3).map((label) => ( + + {label.name} + + ))} + {(issue.labels ?? []).length > 3 && ( + + +{(issue.labels ?? []).length - 3} + + )} + + )} + { + setAssigneePickerIssueId(open ? issue.id : null); + if (!open) setAssigneeSearch(""); + }} + > + + + + e.stopPropagation()} + onPointerDownOutside={() => setAssigneeSearch("")} + > + setAssigneeSearch(e.target.value)} + autoFocus + /> +
+ + {currentUserId && ( + + )} + {(agents ?? []) + .filter((agent) => { + if (!assigneeSearch.trim()) return true; + return agent.name.toLowerCase().includes(assigneeSearch.toLowerCase()); + }) + .map((agent) => ( + + ))} +
+
+
+ + )} + trailingMeta={formatDate(issue.createdAt)} + /> + {hasChildren && isExpanded && children.map((child) => renderIssueRow(child, true))} +
+ ); + }; + + return roots.map((issue) => renderIssueRow(issue, false)); + })()} +
+
+ )) + )}
); }