The space sidebar tree loaded children one level at a time, so there was
no way to expand the whole tree at once - doing it client-side would mean
recursively paging /pages/sidebar-pages hundreds of times. Add a server
endpoint that returns the whole space tree in one permission-filtered
request, and two menu items in the Space menu that drive it.
Server:
- POST /pages/tree (reuses SidebarPageDto, CASL Read gate) returns
{ items: [...] } - a flat list in the same shape as /pages/sidebar-pages
(id, slugId, title, icon, position, parentPageId, spaceId, hasChildren,
canEdit), sorted by position (collate 'C') then id.
- pageRepo.getSpaceDescendants(spaceId, opts): recursive CTE seeded from
space roots (parentPageId IS NULL). getSpaceDescendantsExcludingRestricted
is also added (space-rooted variant of getPageAnd*ExcludingRestricted)
but not used by the read endpoint - see below.
- pageService.getSidebarPagesTree: fetches the whole space tree WITHOUT
SQL restricted-pruning and lets filterAccessibleTreePages drop
inaccessible pages + their subtrees. This mirrors the stepwise
/pages/sidebar-pages behavior, so a restricted page the user HAS
access to stays visible in expand-all (an earlier draft pruned in SQL
and hid them - a behavioural regression). canEdit is per-page in
restricted spaces (filterAccessiblePageIdsWithPermissions) and
space-wide in open spaces. filterAccessibleTreePages is now public
and accepts rootPageId=null to treat every top-level page as a root.
- hasChildren is derived in JS (a page hasChildren iff some returned row
has it as parentPageId) - O(n), no N subqueries.
Client:
- SpaceTree is now a forwardRef exposing { expandAll, collapseAll,
isExpanding } via useImperativeHandle. expandAll fires one
getSpaceTree request, merges the full nested tree into treeDataAtom
(replace-and-merge so the authoritative server tree wins over stale
partially-loaded roots), opens every branch id of the current space,
and guards against space switches via spaceIdRef. Errors are logged
AND surfaced in a notification with the real reason - never a generic
string. collapseAll clears ONLY current-space ids from the shared
open-map (does not disturb other spaces).
- isExpanding is lifted to reactive state in SpaceSidebar and passed as
a prop to SpaceMenu so the Expand item's disabled state actually
updates (a ref mutation alone would not re-render).
- Two Menu.Items inside SpaceMenu (Expand all / Collapse all), gated
only by read access (not canManage) - these are view operations.