Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/8666-tree-expanded-derived.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
'@object-ui/plugin-tree': patch
---

`ObjectTree` derives expansion during render instead of mirroring it into state
(objectui#8666).

**The rendering artifact this removes.** Expansion lived in a
`useState<Set<string>>(new Set())` that a passive `useEffect` keyed on
`[roots, defaultExpandedDepth]` re-seeded from the forest, with rows computed as
`flattenVisible(roots, expanded)`. So the commit that first painted the table
still carried the previous, empty mirror: the root drew, its children did not,
and a second commit drew the seeded-open forest. Probed in the DOM the sequence
was `loading` then a one-row table then a two-row table; it is now `loading` then
the two-row table. Every mount with a non-zero `defaultExpandedDepth` showed a
collapsed forest for one frame, and that frame is also why a test could observe a
half-drawn tree at all.

**The behaviour change that comes with it, and it is the load-bearing half.**
Component state now holds only the answers the *user* gave by clicking a chevron
— a sparse map of node id to open/closed — and the seed is computed from the
forest during render. They compose by one rule: a new forest may re-seed, but a
node the user deliberately opened or closed, and which is still in the forest,
keeps the user's answer; every other node, a genuinely new one included, takes
the seed.

That is a fix in the same direction as the frame, not a side effect of it.
Before this change a re-seed *overwrote* the user's expansion, so any change to
the identity of the record set — a refetch, a filter, a host re-render that
reallocated the rows — silently reopened every subtree the user had collapsed
and reclosed every one they had opened below `defaultExpandedDepth`. Authored
metadata is unaffected: `defaultExpandedDepth` means exactly what it meant, and
no schema key changes.
14 changes: 14 additions & 0 deletions packages/plugin-tree/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ const schema: ObjectQLComponentSchema = {
Records whose parent is missing (or points outside the result set) are kept as
roots, so nothing is silently dropped.

### Expansion: the seed and the user's answer

`defaultExpandedDepth` **seeds** expansion; it does not own it. The seeded set is
derived from the forest during render rather than mirrored into component state,
so a tree that expands by default is painted expanded in the first commit that
has rows — there is no frame in which the forest is drawn collapsed
(objectui#8666).

When the record set changes — a refetch, a filter, a host that reallocates the
rows — the seed is recomputed for the new forest. A node the user opened or
closed by clicking its chevron, **and which is still in the forest**, keeps the
user's answer; every other node, a genuinely new one included, takes the seed.
Expansion is per-mount session state: it is not addressable and is not persisted.

### The `tree` view type is host composition, not authoring

`tree` is **not** an authorable view type. Neither `ObjectViewSchema.defaultViewType`
Expand Down
19 changes: 19 additions & 0 deletions packages/plugin-tree/src/ObjectTree.contractEnvelope-6839.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,25 @@
* ⛔ Do not fold these back into one wait, and ⛔ do not "fix" a future red here
* with a longer timeout: the failure was never slowness, it was reading a
* signal that does not carry the answer.
*
* ## ⚠️ The race described above was FIXED in the component (objectui#8666)
*
* Everything above stands as the record of why these waits are shaped the way
* they are, but one of its statements is no longer true of `ObjectTree`:
* expansion is no longer a `useState` mirror re-seeded from a `useEffect`, so
* the `loading → table:1rows → table:2rows` sequence it measures is now
* `loading → table:2rows` and the intermediate one-row commit does not happen.
* The new shape and both halves of its contract are pinned in
* `ObjectTree.expandedDerived-8666.test.tsx`.
*
* ⭐ NOTHING IN THIS FILE CHANGED FOR THAT, and the note exists to say why the
* absence of a change is deliberate. No assertion here was standing on the
* two-commit sequence: the positive arms wait FOR the descendant row, which is
* a condition on the settled forest and not on how many commits produced it, so
* they were green before objectui#8666 and are green after it. The reason to
* keep them as they are is the one the section above gives — the table's
* `data-testid` is a MOUNT signal and the rows are what this file counts, which
* stays true no matter how many commits the component takes to get there.
*/

import React from 'react';
Expand Down
Loading
Loading