Repository navigation
feat(ou-tree): render browse mode as a virtualised list of composable rows - #1786
Merged
HendrikThePendric merged 30 commits intoOct 28, 2026
Merged
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…with measured rows Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the label Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…pieces with their rows Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…dispatcher Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tates Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ggle-column placeholder The roots-failed rows used the error message as the retry button's accessible name, through an extra label prop. Every failure now shows the same button named Retry; how failures are announced is for the keyboard and accessibility batch. The leaf's empty toggle column is a styled span rather than an inline style. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A test unit under AMR that looks out of place among the countries. The derivation script skips it so a re-derive gives the same fixture; the recorded root counts follow (AMR 26, CAR 27). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The tree exposes rootIds, the ids as given, and rootNodes, the loaded roots with nested ones excluded, empty until loaded. The prop on OuTreeProvider and OuTree is rootIds, so prop and handle agree. The data layer keeps roots, the endpoint's parameter name. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
DHIS2-21263 specifies rootIds; the dhis2-core PR still says roots and is expected to follow the ticket. The query type, the stand-in and its resolver follow the ticket, and the stand-in's option is defaultRootIds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A failed later page showed a retry button beside each of its empty slots: up to fifty identical icons for one action. Each slot is now an OuTreeRetrySkeleton, an error-state skeleton inside a button named Retry with a tooltip; the icon appears on hover and keyboard focus only. Clicking any slot reloads the page. Failed roots use the same piece. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
HendrikThePendric
changed the base branch from
master
to
feat/paged-org-unit-tree
October 27, 2026 14:21
OuTreeList carried the only styles of the glue layer. They move into OuTreeContainer (the scroll element with role tree), OuTreeVirtualSizer (the box as tall as every row) and OuTreeVirtualRow (the element the virtualiser places and measures), and the roots placeholders into OuTreeRootsRows, so the list is a short composition over them. A Structure story shows the three together. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The node row's inline switch over leaf, loading, failed and plain toggle moves into OuTreeNodeToggle, glue with no styles. The toggle piece gains a leaf state: the toggle's space, hidden from assistive technology, so labels line up and the row keeps no styles of its own. A node row whose node is not stored is an error now, not an empty render; the row tests wait for the node as the list does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On the node handle and in the renderLabel props, `children` was the ChildrenStatus, which reads as React children. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
|
🚀 Deployed on https://pr-1786--dhis2-ui.netlify.app |
…th the tree An inline function changed every render, and TanStack rebuilds its measurements for every row when it does, so each scroll-driven render walked all rows. The callback is keyed on the tree handle now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An error row's key included its flat row index, so error rows remounted and lost their measurement when rows above them shifted. The error row carries its slot index now and is keyed on it, like a skeleton row, and the row builder reuses skeleton and error rows as it reused node rows. rowKey lives in the tree layer with the rows it names. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The story description counts 19,190 units. CLAUDE.md lists every glue component and the test-id suffixes, including the leaf's -toggle-space and the composite OuTreeRetrySkeleton. The pieces stories' layout helper is Stack. A list test counts a node row's renders while another node opens. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Storybook forwards STORYBOOK_* variables to the preview, so the OuTree stories read STORYBOOK_OU_TREE_DELAY and pass it as `delay` to every resolver. The gist resolver gains the same `delay` option the tree resolver has, so roots wait too, and both share one `wait` helper. The loading and errors story keeps at least its one second. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…fixed stories `OuTree/Pieces` made `OuTree` a folder in the sidebar, unlike every other component. The piece stories now live in the tree's stories file under the same title, after the tree's own stories, named `Building block: <piece>`. A `Tree` wrapper replaces the file decorator, since the building block stories need no providers or box. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r tooltip's reference Wrapped as a node, the Tooltip puts its children in a focusable span and positions over that span. Outside a flex row the span spans the line, so the tooltip landed in the middle of the page, and the retry skeleton had two tab stops. Both now use the Tooltip's function child: the button and the icon carry the handlers and the ref themselves. The loading and errors story opens Rio de Janeiro, so the failed first page is on screen without a click. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ntrol for both failure kinds A node whose first children page failed never opened: isOpen is false while the status is failed, so there is nothing to close, and opening the node again requests the page again. Before, clicking the label of such a node closed it and hid the error. Both failure kinds now share one model. Red means failed; hover and keyboard focus show a sync icon inside the control and colour it red200 with a red400 edge; one tooltip says what failed and that a click retries; the whole control is the button. OuTreeRetryNode wraps a failed node's label slot with the error icon in the toggle's place and replaces OuTreeErrorIcon and OuTreeRetryButton. OuTreeRetrySkeleton draws its sync icon inside the bar. OuTreeNodeToggle takes `loading` instead of the status, having no failed branch left. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…error icon and the label is the button The retry skeleton's button is itself the red bar, with no skeleton element inside it, and the sync icon sits at the bar's end with air around it. OuTreeSkeleton loses its error state, which only the retry skeleton used. A node whose children did not load keeps the error icon in the toggle's place (OuTreeErrorIcon is back, without a tooltip; OuTreeNodeToggle takes `failed`), and the label slot is the button: it hugs the label instead of spanning the row, and the sync icon follows the label on hover and focus, keeping its space while hidden so nothing moves. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ide their control, demo latency by default The error icon in a failed node's toggle slot opens the node again like the toggle it replaces, and shows the pointer, so the whole row reads as clickable. Both retry tooltips are placed beside their control, where they cover no row above. The stories' stand-in answers after 1500ms unless STORYBOOK_OU_TREE_DELAY says otherwise, so the Netlify preview demos every loading state without a change to the deploy workflow. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The missing pointer over the error icon was a macOS quirk of an inactive window, so the clickable icon goes; instead the icon sits inside the retry button, which is the row's body in the failed state: error icon in the toggle's place, the label slot, and the sync icon after the label. The pointer, the tooltip and the click then cover the icon as well. OuTreeNodeToggle has no failed branch and OuTreeErrorIcon is gone again. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…p with labels The loading skeleton is 20px tall with 2px above and below, like the retry bar, so a row keeps its shape when loading turns to failed and stacked bars keep a gap. Every bar sits after the empty toggle slot, as a leaf's label does, so bars line up with the label box and a loaded row keeps its place while the arrow appears in the blank slot. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
HendrikThePendric
merged commit Oct 28, 2026
5ebd5e8
into
feat/paged-org-unit-tree
19 of 23 checks passed
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




Implements DHIS2-21263 (client side, batch 3 of
OuTree)Description
Rendering for
OuTree's browse mode, over the batch 2 data layer: a virtualised list that looks likeOrganisationUnitTree, built from exported hooks and small style pieces withOuTreeas a few lines of glue. Still over the stand-in; still not exported from any package.@tanstack/react-virtual: 24px by default, taller when a long name wraps or a custom label needs it. Rows are keyed by identity (node id, or parent and slot,rowKey), the row builder reuses every row whose content is unchanged, and the virtualiser'sgetItemKeychanges only with the tree, so opening a node does not remount or re-measure the rows below it and scrolling costs nothing per row.OuTreeRowShellrenders onespan.indentper level, each drawing its ancestor's line, so right-to-left needs no extra rule and a later batch can highlight one ancestor's lines across rows. No nesting of subtrees.src/ou-tree/index.ts: hooks (useOuTree,useOuTreeNode, newuseOuTreeActionsfor components that act on the tree without subscribing, newuseOuTreeVirtualRows); styled pieces that take plain props and read nothing from the tree (OuTreeContainer,OuTreeVirtualSizer,OuTreeVirtualRow,OuTreeRowShell,OuTreeTogglewith a leaf state,OuTreeNodeLabel,OuTreeLoader,OuTreeSkeleton,OuTreeRetrySkeleton,OuTreeRetryNode); glue with no styles (OuTreeNodeToggle,OuTreeNodeRow,OuTreeSkeletonRow,OuTreeErrorRow,OuTreeRow,OuTreeRootsRows,OuTreeList,OuTree).renderLabelis the one customisation convenience: it swaps what sits in the label slot and keeps the indent, toggle and loader. It must be a stable function, since rows are memoised on the row object. Batch 5's checkbox and single-selection labels go in that slot; while a node's children failed to load the slot sits inside the row's retry button, which batch 5's focus model has to resolve; that model should also replace both retry buttons, which are non-conforming children of atree, with the treeitem's own action.rootIds(as given) androotNodes(loaded, nested ones excluded); the component prop and the endpoint query parameter arerootIds, as DHIS2-21263 specifies (the dhis2-core PR still saysroots).useOuTreeNodeandrenderLabelcall the children statuschildrenStatus. The_País_Testunit is dropped from the PAHO fixture.OuTreesidebar entry, aBuilding block:story per piece, each in each state with no tree behind it. The stand-in gaineddelayand a gistfailWithfor them, and the stand-in answers after 1.5 seconds so the Netlify preview demos every loading state;STORYBOOK_OU_TREE_DELAY=<milliseconds>sets another latency when Storybook starts, 0 included.Deviations from the old look, on purpose: a long name wraps instead of overflowing sideways; the first-page error is an icon and a button in the row instead of text under it; the guide line runs through the last descendant's whole row; the toggle's hover no longer darkens the line. New dependencies on this package:
@tanstack/react-virtual,@dhis2-ui/tooltip,@dhis2/ui-icons.Known issues
useVirtualizer(react-hooks/incompatible-library), a warning, not an error, and not suppressible without an eslint-disable.Checklist
Screenshots
Initially open (AMR, Brazil, Minas Gerais); the first-page failure on Rio de Janeiro hovered, with its tooltip; the failed page 3 of Minas Gerais with one slot hovered; and a loading page beside leaf rows, the skeletons lined up with the labels.

🤖 Generated with Claude Code