Skip to content

Commit cf7d2d7

Browse files
committed
NEW React site tree
1 parent ac4bb9d commit cf7d2d7

23 files changed

+3070
-5
lines changed

client/dist/js/bundle.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/dist/js/vendor.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/dist/styles/bundle.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/src/boot/registerComponents.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import SudoModePasswordField from 'components/SudoModePasswordField/SudoModePass
5555
import Paginator from 'components/Paginator/Paginator';
5656
import UnsavedChangesIndicator from 'components/UnsavedChangesIndicator/UnsavedChangesIndicator';
5757
import UnsavedChangesIndicatorTimer from 'components/UnsavedChangesIndicator/UnsavedChangesIndicatorTimer';
58+
import ComplexTreeView from 'components/ComplexTreeView/ComplexTreeView';
5859

5960
export default () => {
6061
Injector.component.registerMany({
@@ -114,5 +115,6 @@ export default () => {
114115
Paginator,
115116
UnsavedChangesIndicator,
116117
UnsavedChangesIndicatorTimer,
118+
ComplexTreeView,
117119
});
118120
};

client/src/bundles/bundle.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,5 +123,6 @@ import '../legacy/HtmlEditorField';
123123
import '../legacy/TabSet';
124124
import '../legacy/GridField';
125125
import '../legacy/SearchableDropdownField/SearchableDropdownFieldEntwine';
126+
import '../legacy/ComplexTreeView/ComplexTreeViewEntwine';
126127

127128
import 'boot';
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import React, { useState } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { UncontrolledTreeEnvironment, Tree } from 'react-complex-tree';
4+
import useComplexTreeViewApi from './useComplexTreeViewApi';
5+
6+
const CSS_CLASS = 'complex-tree-view';
7+
const TREE_ID = 'tree-1';
8+
9+
const ComplexTreeView = ({
10+
currentRecordID = 0,
11+
apiEndpoint = '/admin/pages/tree/jsonview',
12+
}) => {
13+
const [loadingNodeId, setLoadingNodeId] = useState(null);
14+
const {
15+
data,
16+
dataProvider,
17+
error,
18+
initialViewState,
19+
loading,
20+
rootId,
21+
loadSubtree,
22+
mergeSubtree,
23+
} = useComplexTreeViewApi(TREE_ID, currentRecordID, apiEndpoint);
24+
25+
/**
26+
* Renders the DOM for a tree item label.
27+
* For synthetic limited indicator nodes, renders as a clickable link.
28+
* Otherwise renders the regular title.
29+
*/
30+
const renderTreeItemTitle = ({ item }) => {
31+
const isLimitedIndicator = item.data.isLimitedIndicator === true;
32+
const isLoading = loadingNodeId === item.index;
33+
34+
if (isLimitedIndicator) {
35+
return (
36+
<span className={`${CSS_CLASS}__item-link ${CSS_CLASS}__item-link--limited`}>
37+
Too many records (
38+
<a href={item.data.linkUrl} className={`${CSS_CLASS}__limited-link`}>show as list</a>
39+
)
40+
</span>
41+
);
42+
}
43+
44+
if (isLoading) {
45+
return (
46+
<span className={`${CSS_CLASS}__item-link`} data-item-id={item.index}>
47+
<span className={`${CSS_CLASS}__loading`}>Loading...</span>
48+
{item.data.title}
49+
</span>
50+
);
51+
}
52+
53+
return (
54+
<span className={`${CSS_CLASS}__item-link`} data-item-id={item.index}>
55+
{item.data.title}
56+
</span>
57+
);
58+
};
59+
60+
/**
61+
* This is triggered by Double Click (mouse) or Enter (keyboard) on a focused item.
62+
* For branch nodes, this should only navigate if the item is a leaf or if the title was clicked.
63+
*/
64+
const handlePrimaryAction = (items) => {
65+
window.location.href = `/admin/pages/edit/show/${items.data.id}`;
66+
};
67+
68+
/**
69+
* Handle mousedown events on tree items (capture phase).
70+
* For branch nodes, only allow expansion if clicking on the icon (not the title).
71+
* For title clicks, we'll handle navigation separately.
72+
*/
73+
const handleRootMouseDown = (e) => {
74+
const button = e.target.closest('[data-rct-item-interactive="true"]');
75+
if (!button) {
76+
return;
77+
}
78+
const titleSpan = button.querySelector('.complex-tree-view__item-link');
79+
if (!titleSpan || !titleSpan.contains(e.target)) {
80+
return;
81+
}
82+
const treeItem = button.closest('[role="treeitem"]');
83+
if (!treeItem || !treeItem.hasAttribute('aria-expanded')) {
84+
return;
85+
}
86+
const itemId = titleSpan.getAttribute('data-item-id');
87+
if (itemId && data && data.items && data.items[itemId]) {
88+
const item = data.items[itemId];
89+
if (item && item.data && item.data.id) {
90+
e.preventDefault();
91+
e.stopPropagation();
92+
window.location.href = `/admin/pages/edit/show/${item.data.id}`;
93+
}
94+
}
95+
};
96+
97+
/**
98+
* Handle click events on tree items (capture phase).
99+
* Prevents react-complex-tree from processing clicks on branch node titles.
100+
*/
101+
const handleRootClick = (e) => {
102+
const button = e.target.closest('[data-rct-item-interactive="true"]');
103+
if (!button) {
104+
return;
105+
}
106+
const titleSpan = button.querySelector('.complex-tree-view__item-link');
107+
if (!titleSpan || !titleSpan.contains(e.target)) {
108+
return;
109+
}
110+
const treeItem = button.closest('[role="treeitem"]');
111+
if (!treeItem || !treeItem.hasAttribute('aria-expanded')) {
112+
return;
113+
}
114+
e.preventDefault();
115+
e.stopPropagation();
116+
};
117+
118+
/**
119+
* Handler for item selection - no-op for now
120+
*/
121+
const handleSelectItems = () => {};
122+
123+
/**
124+
* Handles expanding a tree item. If the item has unloaded children,
125+
* loads the subtree and merges it into the tree.
126+
*/
127+
// eslint-disable-next-line no-unused-vars
128+
const handleExpandItem = async (item, treeId) => {
129+
const hasUnloadedChildren = item.data.expanded === false && item.data.count > 0 && item.children.length === 0;
130+
if (hasUnloadedChildren) {
131+
try {
132+
setLoadingNodeId(item.index);
133+
const subtree = await loadSubtree(item.data.id);
134+
await mergeSubtree(item.data.id, subtree.items);
135+
} finally {
136+
setLoadingNodeId(null);
137+
}
138+
}
139+
};
140+
141+
/**
142+
* INTERCEPT KEYDOWN
143+
* This is the key fix. We intercept the event at the container level.
144+
*/
145+
const handleRootKeyDown = (e) => {
146+
// Check if the key pressed is Space
147+
if (e.key !== ' ') {
148+
return;
149+
}
150+
// Find the closest tree item container
151+
const treeItem = e.target.closest('[role="treeitem"]');
152+
153+
// If we are on a tree item...
154+
if (treeItem) {
155+
// Check if it is a Folder. Folders have 'aria-expanded' (true or false).
156+
// Leaves do NOT have this attribute.
157+
const isFolder = treeItem.hasAttribute('aria-expanded');
158+
159+
// If it's NOT a folder (it's a leaf), prevent the default browser behavior.
160+
// This stops the browser from turning "Space" into a "Click" event.
161+
if (!isFolder) {
162+
e.preventDefault();
163+
}
164+
}
165+
};
166+
167+
if (loading) {
168+
return <div>Loading page tree...</div>;
169+
}
170+
if (error) {
171+
return <div style={{ color: 'red' }}>Error loading data: {error}</div>;
172+
}
173+
if (!dataProvider || !initialViewState) {
174+
return <div>No data available.</div>;
175+
}
176+
177+
return (
178+
<div
179+
className={CSS_CLASS}
180+
onKeyDown={handleRootKeyDown}
181+
onMouseDownCapture={handleRootMouseDown}
182+
onClickCapture={handleRootClick}
183+
>
184+
<UncontrolledTreeEnvironment
185+
dataProvider={dataProvider}
186+
getItemTitle={item => item.data.title}
187+
viewState={initialViewState}
188+
// canDragAndDrop={false}
189+
// canDropOnFolder={false}
190+
// canReorderItems={false}
191+
canRenameItems={false}
192+
canSearchItems={false}
193+
canSearchByStartingTyping={false}
194+
renderItemTitle={renderTreeItemTitle}
195+
onPrimaryAction={handlePrimaryAction}
196+
onSelectItems={handleSelectItems}
197+
onExpandItem={handleExpandItem}
198+
>
199+
<Tree treeId={TREE_ID} rootItem={rootId} treeLabel="Page Tree" />
200+
</UncontrolledTreeEnvironment>
201+
</div>
202+
);
203+
};
204+
205+
ComplexTreeView.propTypes = {
206+
currentRecordID: PropTypes.number,
207+
apiEndpoint: PropTypes.string,
208+
};
209+
210+
export { ComplexTreeView as Component };
211+
export default ComplexTreeView;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
.complex-tree-view__container {
2+
padding: 1rem;
3+
}
4+
5+
.complex-tree-view {
6+
.rct-tree-item-li {
7+
padding: 2px 0;
8+
}
9+
10+
.rct-tree-item-icon-parent {
11+
margin-right: 5px;
12+
}
13+
.rct-tree-item-title-text {
14+
margin-left: 5px;
15+
}
16+
17+
.rct-tree-item-arrow-isFolder {
18+
font-size: 14px !important;
19+
width: 1.2em !important;
20+
height: 1.2em !important;
21+
line-height: 1.2em !important;
22+
min-width: unset !important;
23+
min-height: unset !important;
24+
max-width: unset !important;
25+
max-height: unset !important;
26+
display: inline-flex;
27+
align-items: center;
28+
justify-content: center;
29+
cursor: pointer;
30+
}
31+
32+
.rct-tree-item-li-item-focused:focus {
33+
outline: 1px solid #777;
34+
background-color: #333;
35+
}
36+
}
37+
38+
.complex-tree-view__item-link {
39+
color: inherit;
40+
text-decoration: none;
41+
display: block;
42+
width: 100%;
43+
cursor: pointer;
44+
45+
&--limited {
46+
color: #666;
47+
font-style: italic;
48+
}
49+
}
50+
51+
.complex-tree-view__limited-link {
52+
color: #06c;
53+
text-decoration: underline;
54+
cursor: pointer;
55+
56+
&:hover {
57+
color: #0052a3;
58+
}
59+
}

0 commit comments

Comments
 (0)