Skip to content

Accessibility: Create hierarchy tree react component #2070

@emteknetnz

Description

@emteknetnz

Follow on from silverstripe/silverstripe-cms#3136 (comment)

Placeholder for react site tree replacement

Note - component should live in silverstripe/admin rather than silverstripe/cms because it's not SiteTree specific, it should be usable with any hierarchy like object

Update: Pausing work on this until we get rid of jquery/entwine

The core issue is that the PJAX response when clicking on a page includes the wrapper for the site tree. This means that the site tree react component is recreated after every pjax response. The react component makes a fetch() request to the server on mount to get JSON data. This causes a big FOUT everytime you navigate to a new page. I tried working around this by detaching the react component and reattaching, though it lost the react even handlers.

Also this work was honestly just too ambitions for one PR and this should have been an epic due to all the features e.g. drag and drag, keyboard nav, batch actions, etc

Keep the pull-requests around as good progress was made.

PRs

Overview for AGENTS.md (useful if this is picked up again in the future)

Details Hierarchy Tree Implementation Guidelines

Overview

React tree component (ComplexTreeView) using react-complex-tree library successfully replaces legacy jstree server-rendered tree with lazy loading support via JSON API. Refactoring complete with proper separation of concerns between admin and CMS modules.

Final Architecture (Completed)

Backend (PHP) - admin module

  • vendor/silverstripe/admin/code/HierarchyTreeController.php - Abstract base controller serving JSON tree data. Provides generic tree functionality for any DataObject with Hierarchy extension. NO dependencies on CMS module.
  • vendor/silverstripe/admin/code/HierarchyTreeTrait.php - Shared tree logic trait used by both HierarchyTreeController and CMSMain. Contains getMarkedSet(), saveTreeNodeInternal(), updateTreeNodesInternal(), and getTreePrepopulateOptions(). NO CMS dependencies.
  • vendor/silverstripe/admin/code/Forms/ComplexTreeView.php - Schema provider class (NOT a FormField) that configures endpoint URLs for the React component. Generic and reusable. NO CMS dependencies. Has setters for apiEndpoint, duplicateEndpoint, duplicateWithChildrenEndpoint, addChildEndpoint, saveNodeEndpoint, updateNodesEndpoint, editUrlPattern, listUrlPattern.

Backend (PHP) - cms module

  • vendor/silverstripe/cms/code/Controllers/CMSSiteTreeController.php - Extends HierarchyTreeController for SiteTree. Implements getComplexTreeView() to configure all CMS-specific endpoints and URL patterns. Responsible for all CMS-specific logic.
  • vendor/silverstripe/cms/code/Controllers/CMSMain.php - Uses HierarchyTreeTrait. Has useModernTreeView() method (returns true). TreeViewSchemaJSON() delegates to CMSSiteTreeController::getComplexTreeView().

Frontend (JavaScript/React) - admin module

  • vendor/silverstripe/admin/client/src/components/ComplexTreeView/ComplexTreeView.js - Main React component. Generic and reusable. All URLs are configurable via props (editUrlPattern, listUrlPattern, contextMenuUrls.addChild). Supports callback props for PJAX integration: onEditItem, onDuplicateItem, onAddChild, onShowAsList, onRefreshNodes, onRefreshParentNode.
  • vendor/silverstripe/admin/client/src/components/ComplexTreeView/TreeContextMenu.js - Right-click context menu. Uses configurable i18n labels passed via labels prop.
  • vendor/silverstripe/admin/client/src/components/ComplexTreeView/useComplexTreeViewApi.js - API hook. Uses configurable apiEndpoint prop with fallback defaults. Uses listUrlPattern with fallback.
  • vendor/silverstripe/admin/client/src/legacy/ComplexTreeView/ComplexTreeViewEntwine.js - Entwine bridge. Maps schema data to React props including contextMenuUrls, editUrlPattern, listUrlPattern, labels. Creates callback props that trigger Entwine events for external handling.

Frontend (JavaScript) - cms module

  • vendor/silverstripe/cms/client/src/legacy/CMSMain.ComplexTree.js - CMS-specific Entwine handlers. Intercepts tree action events and uses PJAX (loadPanel) for navigation. Handles oneditnode, onaddchildnode, onshowaslistnode, onduplicatenode, onduplicatewithchildrennode. Uses delegation patterns ('from .cms-container':) to listen for onaftersubmitform and onafterstatechange events.

Template

  • vendor/silverstripe/cms/templates/.../CMSMain_RecordList.ss - Conditionally renders either modern React tree (complex-tree-view__container) or legacy deferred panel based on $useModernTreeView.

PJAX Integration

The ComplexTreeView integrates with the CMS PJAX system for seamless navigation without full page reloads. This integration uses callback props and Entwine event delegation.

Callback Props - React component accepts these optional callbacks:

  • onEditItem(nodeId) - Called when user clicks tree item or "View" in context menu
  • onDuplicateItem(nodeId, payload) - Called when duplicating (payload includes includeSubpages flag)
  • onAddChild(nodeId, payload) - Called when adding child page (payload includes childType)
  • onShowAsList(nodeId) - Called when showing children as list
  • onRefreshNodes(callback) - Registers callback for tree refresh events
  • onRefreshParentNode(callback) - Registers callback for parent node refresh events

Schema Configuration:

  • enableDefaultNavigationHandlers (boolean, default true) - When false, React component defers to external callbacks instead of using window.location.assign() for navigation
  • CMS sets this to false in CMSSiteTreeController::getComplexTreeView() to enable PJAX

CMS Entwine Handler - vendor/silverstripe/cms/client/src/legacy/CMSMain.ComplexTree.js:

  • Extends base Entwine definition from ComplexTreeViewEntwine.js
  • Maps Entwine events to PJAX navigation via $('.cms-container').entwine('.ss').loadPanel(url)
  • Handlers: oneditnode, onaddchildnode, onshowaslistnode, onduplicatenode, onduplicatewithchildrennode
  • Uses delegation pattern 'from .cms-container': to listen for CMS events:
    • onaftersubmitform - Refreshes tree after form save
    • onafterstatechange - Updates tree selection after PJAX navigation

Entwine Event Flow:

  1. ComplexTreeViewEntwine.js creates callbacks that trigger custom events (e.g., this.trigger('editnode', [nodeId]))
  2. CMSMain.ComplexTree.js listens for these events and handles them with PJAX
  3. Example: onEditItem callback → triggers 'editnode' event → oneditnode handler → loadPanel('/admin/pages/edit/show/123')

Duplicate Action Special Handling:

  • POSTs to duplicate endpoint first to create new page
  • On success, uses loadPanel() to navigate to new page edit form
  • Triggers refreshparentnode event after 300ms delay to update tree with new node
  • Delay prevents FOUT (Flash Of Unstyled Tree) during PJAX navigation

Data Flow

Initial Load:

  1. Template checks $useModernTreeView → renders <div class="complex-tree-view__container" data-schema='...'>
  2. Entwine (ComplexTreeViewEntwine.js) matches .complex-tree-view__container, parses schema, renders React component with all configuration
  3. React component calls configurable apiEndpoint (e.g., /admin/pages/tree/jsonview/0/123)
  4. PHP controller returns JSON with tree data and configurable context menu data
  5. React transforms data and renders tree with react-complex-tree using all provided configuration

PJAX Navigation:

  1. User clicks tree item or context menu action
  2. React component calls callback prop (e.g., onEditItem(nodeId))
  3. Callback triggers Entwine event (e.g., 'editnode')
  4. CMS Entwine handler intercepts event and calls loadPanel(url) for PJAX navigation
  5. PJAX loads new content without full page reload
  6. onafterstatechange event updates tree selection to match current page

Tree Refresh After Save:

  1. User saves page via CMS form
  2. onaftersubmitform event fires in CMS container
  3. CMS Entwine handler extracts record ID from form
  4. Triggers 'refreshnodes' event on tree container
  5. React component receives event via onRefreshNodes callback
  6. Calls updateNodes() from API hook to fetch fresh node data
  7. Tree updates to reflect changes (title, status badges, hierarchy)

Configuration Thresholds

SilverStripe\CMS\Model\SiteTree:
  node_threshold_total: 100  # Stops breadth-first traversal after N nodes
  node_threshold_leaf: 50    # Marks nodes with >N children as "limited"

Metadata

Metadata

Assignees

No one assigned
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions