Estimate: 26m · Depends on: 2.5.2, 2.5.13, 2.5.11
Rework the Tree rendering to lazy-load + sort per the 2.5.11 design (finding #57), composing the 2.5.13 reads. Scope split (re-planned 2026-06-05): this subtask is the LAZY + SORTABLE tree; virtualization is its own 2.5.15 (a separable DOM-perf layer) and the seed is 2.5.16 — so each lands as a reviewable PR, not a 1k-line sprawl.
'use server' wrapper over 2.5.13's listRootIssues/listChildIssues (session + active-project gate, like the shipped issue actions) so the client can fetch a level on expand / sort. The initial roots may load in the Server Component; children + re-sorts come through the action.hasChildren without loading; expanding fetches listChildIssues (the design's spinner placeholder row on the node, aria-busy) and inserts them; a "Load more children" row at the children's indent when a parent exceeds the per-node page (50), appending the next page on click (parent never collapses).aria-sort asc/desc/none, not colour alone) that re-fetches via the sorted reads; siblings re-order WITHIN each parent (hierarchy preserved); shares the List's ?sort= URL shape. Already-expanded nodes re-fetch their children on a sort change.aria-setsize = its parent's TRUE child total, but 2.5.13's read returns only hasMore. Extend the lazy read/service to also return the per-node total child count (a cheap COUNT) so aria-posinset/aria-setsize are honest (e.g. 19 of 128) — needed now AND by 2.5.15's virtualization.The accessible treegrid contract (2.5.2) + roving-tabindex keyboard model + the STRICT axe sweep must still pass; → on a collapsed parent triggers its lazy fetch. A specimen route covers the sortable-header + loading + load-more states for the sweep.
(PR #115 merged 2026-06-05, merge commit 8ac519e — the /issues Tree is now LAZY + SORTABLE (finding #57). The Server Component loads only the first page of ROOTS (listRootIssues); children stream in on expand via listRootIssuesAction/listChildIssuesAction (spinner row + "Load more children" append, per-node page 50); column headers sort (caret + aria-sort, not colour alone) via ?sort=, re-ordering siblings within their parent. TreeTable extended backward-compatibly (per-row hasChildren/posinset/setsize overrides + busy/aria-busy + onRowActivate + per-column ariaSort); setsize count added to the 2.5.13 read (TreeLevelDto.total + countProjectTreeLevel) for honest aria-posinset/setsize. IssueTreeStaticTable keeps the FILTERED tree on the context-preserving whole-forest read (lazy+filter = Epic 6). Bug fixed: buildIssueListHref only emitted ?sort= for the List, so Tree sort wouldn't persist — now both views. Decisions (flagged for review): a sort change REMOUNTS the tree (expansion resets, not in-place re-fetch); the filtered tree is non-lazy/non-sortable; no new /tokens specimen (new states are component-tested + hit the strict a11y sweep on the real /issues route via 2.5.6). 4 new lazy-tree component tests + the count integration test; CI green (one flaky workflow-delete-reassign E2E — unrelated to this diff, passed on re-run). Split into 2.5.15 (virtualization) + 2.5.16 (db:seed:large).)
aria-sort, not colour alone); sorting re-orders siblings within each parent and persists in ?sort= like the List; expanded nodes re-fetch on sort change.aria-level/posinset/setsize/expanded; the treegrid a11y contract + STRICT axe sweep still pass + roving-tabindex keyboard (incl. → triggers lazy fetch).components/ui/TreeTable.tsx (2.5.2) + app/(authed)/issues/_components/IssueTreeTable.tsx + IssueTreeSection.tsx — the rendering to reworklistRootIssues/listChildIssues reads (extend with the count) + the 2.5.11 designIssueListTable) + the ?sort= URL contract (issueListView) to mirror; the existing issue Server Actions for the action patterntests/e2e/shell-a11y.spec.ts tree-table strict sweep + the /tokens convention