Skip to content

Entity Tree

Walks the navigation tree the web interface draws, one level per call, from a project down to its shots and assets.

Terminal window
pnpm dlx shadcn@latest add https://sg-widgets.vercel.app/r/react/entity-tree.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://sg-widgets.vercel.app/r/svelte/entity-tree.json
Entity Tree: a project seeded open to one shot, with checkboxes, a search, a thumbnail variant, a row whose thumbnail field is empty, and the three sizes beside a button

React

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
/>

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.

proptypedefaultmeaning
contextSgContextrequiredThe widget context. Every read goes through it, so widgets on a page share one cache.
rootPathstringrequiredWhere the tree starts, /Project/<id>.
seedPathstring | string[] | nullnullOpens the tree down to this path, or to the last entry of an incremental path.
checkablebooleanfalseDraws a checkbox per node and reports the checked rows.
selectionMode'none' | 'single' | 'multiple''single'How many nodes may be selected at once.
selectionstring[]the tree's ownPaths of the selected nodes, two-way. Unset, the tree keeps its own.
expandedstring[]—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.
searchablebooleanfalseShows an input that searches the project and opens the tree onto the hits.
searchPlaceholderstring'Search'Placeholder and label of that input.
expandDepthnumber3Levels a whole-branch expansion opens.
thumbnailstring | falsefalseThe image field to draw a thumbnail from.
labelFieldstring—Field shown as the label, in place of the tree's own.
subLabelFieldstring | CollectionColumn | nullnullField 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.
secondaryFieldstring | CollectionColumn | nullnullRight-aligned field, drawn by its data type. A resolved column renders it by type.
secondary(node) => string—Right-aligned text. Wins over secondaryField.
showCodebooleanfalseShow the schema name beside the label on a node that stands for a type.
fieldsstring[]—Extra fields to request, so a caller's own sub-label or secondary can read them.
siteUrlstringthe context'sThe site the status sprite is served from.
labelstring'Project hierarchy'Accessible name of the tree.
maxHeightstring'24rem'Height of the scrolling body.
emptyLabelstring'No rows'Shown when the root has no children.
noMatchLabelstring'No match'Shown when a search matched no row.
loadingLabelstring'Loading…'Names the skeletons a read stands behind, for a screen reader.
errorLabelstring—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 / classNamestring—Merged after the widget's own classes.
eventpayloadwhen
onSelectTreeNodeA node with nothing under it was chosen.
onSelectionChangestring[]The selected paths changed.
onExpandedChangestring[]The open paths changed.
onCheckedChangeEntityRef[]The checked set changed. Only nodes that stand for a row are reported.
onErrorErrorA read failed.
slotreceivesdraws
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.

The tree keeps one tab stop. Focus follows the cursor, and the row it sits on is the one that carries tabindex="0".

keydoes
DownMoves to the next visible node.
UpMoves to the previous visible node.
RightOpens a closed branch, else moves to its first child.
LeftCloses an open branch, else moves to its parent.
HomeMoves to the first node.
EndMoves to the last visible node.
SpaceToggles the node's checkbox.
EnterSelects the node.
*Opens every branch at the focus level, expandDepth levels deep.
EscapeIn the search input, clears it.
a–z, 0–9Moves 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.

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).

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.