Entity Tree
Walks the navigation tree the web interface draws, one level per call, from a project down to its shots and assets.
Install
Section titled “Install”pnpm dlx shadcn@latest add https://sg-widgets.vercel.app/r/react/entity-tree.jsonpnpm dlx shadcn-svelte@latest add https://sg-widgets.vercel.app/r/svelte/entity-tree.jsonReact
Loading the React demo…
Svelte
Loading the Svelte demo…
<EntityTree context={context} rootPath="/Project/70" seedPath="/Project/70/Shot/sg_sequence/Sequence/100/id/862" checkable searchable/>Behaviour
Section titled “Behaviour”A node opens when it is clicked, when Right is pressed on it, or when a seed path runs through it.
Opening reads one level and keeps it: a node closed and opened again costs nothing. The node being
read carries aria-busy and shows a spinner in place of its chevron.
A checkbox propagates both ways. Checking a branch checks everything under it, including a level read
after the box was ticked. Unchecking one child leaves the branch mixed, and it returns to checked
once every child is checked again. Only nodes that stand for a row are reported.
Typing in the search input searches the whole project, not the nodes already loaded. Every row the words match is placed in the tree, the branches above it are opened, matched rows are marked and the rest are dimmed. Clearing the input, or pressing Escape in it, restores the tree as it was.
A whole branch opens at once on Alt-click or Cmd/Ctrl-click on its chevron, and on * for every
branch at the focus level. Both read expandDepth levels below the node, with the node marked busy
until every one of them is in.
Each level’s rows are read once per type over the ids the level returned, so subLabelField,
secondaryField, thumbnail and the status badge cost no read per row.
A row draws a picture only when its thumbnail field holds a URL. A field that is absent, null or
empty draws no picture and leaves the row’s leading edge to the label.
| prop | type | default | meaning |
|---|---|---|---|
context | SgContext | required | The widget context. Every read goes through it, so widgets on a page share one cache. |
rootPath | string | required | Where the tree starts, /Project/<id>. |
seedPath | string | string[] | null | null | Opens the tree down to this path, or to the last entry of an incremental path. |
checkable | boolean | false | Draws a checkbox per node and reports the checked rows. |
selectionMode | 'none' | 'single' | 'multiple' | 'single' | How many nodes may be selected at once. |
selection | string[] | the tree's own | Paths of the selected nodes, two-way. Unset, the tree keeps its own. |
expanded | string[] | — | Paths of the open nodes, two-way. Setting it opens exactly those, reading whatever level is not loaded. |
isRowDisabled | (node) => boolean | — | True for a node the arrows skip and the selection refuses. |
searchable | boolean | false | Shows an input that searches the project and opens the tree onto the hits. |
searchPlaceholder | string | 'Search' | Placeholder and label of that input. |
expandDepth | number | 3 | Levels a whole-branch expansion opens. |
thumbnail | string | false | false | The image field to draw a thumbnail from. |
labelField | string | — | Field shown as the label, in place of the tree's own. |
subLabelField | string | CollectionColumn | null | null | Field shown under the label, drawn by its data type: a status reads its display name. |
subLabel | (node) => string | — | Muted line under the label. Wins over subLabelField. |
secondaryField | string | CollectionColumn | null | null | Right-aligned field, drawn by its data type. A resolved column renders it by type. |
secondary | (node) => string | — | Right-aligned text. Wins over secondaryField. |
showCode | boolean | false | Show the schema name beside the label on a node that stands for a type. |
fields | string[] | — | Extra fields to request, so a caller's own sub-label or secondary can read them. |
siteUrl | string | the context's | The site the status sprite is served from. |
label | string | 'Project hierarchy' | Accessible name of the tree. |
maxHeight | string | '24rem' | Height of the scrolling body. |
emptyLabel | string | 'No rows' | Shown when the root has no children. |
noMatchLabel | string | 'No match' | Shown when a search matched no row. |
loadingLabel | string | 'Loading…' | Names the skeletons a read stands behind, for a screen reader. |
errorLabel | string | — | Shown in place of what the failed read said. |
size | 'sm' | 'md' | 'lg' | 'md' | Row text, leading slot and glyphs, and the search input on the control ladder. |
density | 'compact' | 'default' | 'default' | Compact halves the vertical row padding and draws a row chip and badge a step smaller. |
class / className | string | — | Merged after the widget's own classes. |
Events
Section titled “Events”| event | payload | when |
|---|---|---|
onSelect | TreeNode | A node with nothing under it was chosen. |
onSelectionChange | string[] | The selected paths changed. |
onExpandedChange | string[] | The open paths changed. |
onCheckedChange | EntityRef[] | The checked set changed. Only nodes that stand for a row are reported. |
onError | Error | A read failed. |
| slot | receives | draws |
|---|---|---|
row | { node, id, level, expanded, selected, disabled, match } | Draws a row's contents: everything after the chevron and the checkbox. |
header | — | Region above the search input. |
footer | — | Region below the tree. |
Keyboard
Section titled “Keyboard”The tree keeps one tab stop. Focus follows the cursor, and the row it sits on is the one that carries
tabindex="0".
| key | does |
|---|---|
| Down | Moves to the next visible node. |
| Up | Moves to the previous visible node. |
| Right | Opens a closed branch, else moves to its first child. |
| Left | Closes an open branch, else moves to its parent. |
| Home | Moves to the first node. |
| End | Moves to the last visible node. |
| Space | Toggles the node's checkbox. |
| Enter | Selects the node. |
| * | Opens every branch at the focus level, expandDepth levels deep. |
| Escape | In the search input, clears it. |
| a–z, 0–9 | Moves to the next node whose label starts with what was typed. The same letter again walks the matches. |
A disabled node takes no keys and no click: the arrows and type-ahead step over it, and the cursor never lands on it.
API behaviour
Section titled “API behaviour”POST /hierarchy/_expand answers one level: the node itself, and children carrying a label, a ref
and has_children, which says whether opening is worth a call. Walking a project is therefore one
call per node (post_hierarchy_expand).
That endpoint refuses the vendor content types every other POST on this API requires and accepts only
application/json. seed_entity_field is documented and ignored, so it is not sent
(post_hierarchy_expand).
Which levels a project has is the site’s own navigation configuration, not a fixed hierarchy: the
probed site’s shot path runs through the field name sg_sequence (post_hierarchy_search). seedPath
is followed by taking whichever child is a prefix of it, never by parsing the path.
POST /hierarchy/_search answers incremental_path, one entry per level, and that array is a seed
path as it stands (post_hierarchy_search).
A field name a type does not have is dropped at 200, so one list of names reads every type of a level
(probe 003). A row’s status badge comes from whichever field of its type is a status_list; Project’s
sg_status is a plain list with no Status row behind its values, so a project carries no badge
(entity_types/Project, probe 009).
Searching is two calls a query. POST /entity/_text_search matches a row when every
whitespace-separated word appears in its name or in the name of the row it links to, and answers at
most 25 rows a page (probe 053). It says nothing about where a row sits, and
POST /hierarchy/_search does not match words, so each hit is then asked for its own path
(post_hierarchy_search). The words are scoped to the project on every searched type that carries a
project field.
A grouping field with no rows hides every row under it: a project with shots and no sequences answers
/Project/<id>/Shot as one empty child and no bucket, although the __none__ path under that level
answers all of them. The tree asks for that path instead of drawing the empty row, reading the field
it runs through off the 400 the endpoint answers a bogus segment. The two endpoints spell the bucket
differently — <field>/<GroupType>/__none__ from _expand, <field>/__none__ from _search — and a
seed path is followed in either spelling (064_hierarchy_expand_buckets, post_hierarchy_expand).
Reference
Section titled “Reference”The model is this repo’s own. headless-tree (@headless-tree/core 1.7.0) is the reference for the
lazy loader, the tri-state checkboxes and the type-ahead; Zag’s tree-view (@zag-js/tree-view 1.43.3)
for the state model and the ARIA, including aria-checked="mixed" and the roving tab stop; ReUI’s
Tree (ReUI 2.5.2) for the markup, the --tree-indent step and the row classes.