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:
ComplexTreeViewEntwine.js creates callbacks that trigger custom events (e.g., this.trigger('editnode', [nodeId]))
CMSMain.ComplexTree.js listens for these events and handles them with PJAX
- 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:
- Template checks
$useModernTreeView → renders <div class="complex-tree-view__container" data-schema='...'>
- Entwine (
ComplexTreeViewEntwine.js) matches .complex-tree-view__container, parses schema, renders React component with all configuration
- React component calls configurable
apiEndpoint (e.g., /admin/pages/tree/jsonview/0/123)
- PHP controller returns JSON with tree data and configurable context menu data
- React transforms data and renders tree with
react-complex-tree using all provided configuration
PJAX Navigation:
- User clicks tree item or context menu action
- React component calls callback prop (e.g.,
onEditItem(nodeId))
- Callback triggers Entwine event (e.g.,
'editnode')
- CMS Entwine handler intercepts event and calls
loadPanel(url) for PJAX navigation
- PJAX loads new content without full page reload
onafterstatechange event updates tree selection to match current page
Tree Refresh After Save:
- User saves page via CMS form
onaftersubmitform event fires in CMS container
- CMS Entwine handler extracts record ID from form
- Triggers
'refreshnodes' event on tree container
- React component receives event via
onRefreshNodes callback
- Calls
updateNodes() from API hook to fetch fresh node data
- 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"
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 GuidelinesOverview
React tree component (
ComplexTreeView) usingreact-complex-treelibrary 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 bothHierarchyTreeControllerandCMSMain. ContainsgetMarkedSet(),saveTreeNodeInternal(),updateTreeNodesInternal(), andgetTreePrepopulateOptions(). 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 forapiEndpoint,duplicateEndpoint,duplicateWithChildrenEndpoint,addChildEndpoint,saveNodeEndpoint,updateNodesEndpoint,editUrlPattern,listUrlPattern.Backend (PHP) - cms module
vendor/silverstripe/cms/code/Controllers/CMSSiteTreeController.php- ExtendsHierarchyTreeControllerfor SiteTree. ImplementsgetComplexTreeView()to configure all CMS-specific endpoints and URL patterns. Responsible for all CMS-specific logic.vendor/silverstripe/cms/code/Controllers/CMSMain.php- UsesHierarchyTreeTrait. HasuseModernTreeView()method (returnstrue).TreeViewSchemaJSON()delegates toCMSSiteTreeController::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 vialabelsprop.vendor/silverstripe/admin/client/src/components/ComplexTreeView/useComplexTreeViewApi.js- API hook. Uses configurableapiEndpointprop with fallback defaults. UseslistUrlPatternwith fallback.vendor/silverstripe/admin/client/src/legacy/ComplexTreeView/ComplexTreeViewEntwine.js- Entwine bridge. Maps schema data to React props includingcontextMenuUrls,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. Handlesoneditnode,onaddchildnode,onshowaslistnode,onduplicatenode,onduplicatewithchildrennode. Uses delegation patterns ('from .cms-container':) to listen foronaftersubmitformandonafterstatechangeevents.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 menuonDuplicateItem(nodeId, payload)- Called when duplicating (payload includesincludeSubpagesflag)onAddChild(nodeId, payload)- Called when adding child page (payload includeschildType)onShowAsList(nodeId)- Called when showing children as listonRefreshNodes(callback)- Registers callback for tree refresh eventsonRefreshParentNode(callback)- Registers callback for parent node refresh eventsSchema Configuration:
enableDefaultNavigationHandlers(boolean, defaulttrue) - Whenfalse, React component defers to external callbacks instead of usingwindow.location.assign()for navigationfalseinCMSSiteTreeController::getComplexTreeView()to enable PJAXCMS Entwine Handler -
vendor/silverstripe/cms/client/src/legacy/CMSMain.ComplexTree.js:ComplexTreeViewEntwine.js$('.cms-container').entwine('.ss').loadPanel(url)oneditnode,onaddchildnode,onshowaslistnode,onduplicatenode,onduplicatewithchildrennode'from .cms-container':to listen for CMS events:onaftersubmitform- Refreshes tree after form saveonafterstatechange- Updates tree selection after PJAX navigationEntwine Event Flow:
ComplexTreeViewEntwine.jscreates callbacks that trigger custom events (e.g.,this.trigger('editnode', [nodeId]))CMSMain.ComplexTree.jslistens for these events and handles them with PJAXonEditItemcallback → triggers'editnode'event →oneditnodehandler →loadPanel('/admin/pages/edit/show/123')Duplicate Action Special Handling:
loadPanel()to navigate to new page edit formrefreshparentnodeevent after 300ms delay to update tree with new nodeData Flow
Initial Load:
$useModernTreeView→ renders<div class="complex-tree-view__container" data-schema='...'>ComplexTreeViewEntwine.js) matches.complex-tree-view__container, parses schema, renders React component with all configurationapiEndpoint(e.g.,/admin/pages/tree/jsonview/0/123)react-complex-treeusing all provided configurationPJAX Navigation:
onEditItem(nodeId))'editnode')loadPanel(url)for PJAX navigationonafterstatechangeevent updates tree selection to match current pageTree Refresh After Save:
onaftersubmitformevent fires in CMS container'refreshnodes'event on tree containeronRefreshNodescallbackupdateNodes()from API hook to fetch fresh node dataConfiguration Thresholds