Skip to content

Commit 5de1dad

Browse files
committed
NEW React site tree
1 parent ac4bb9d commit 5de1dad

23 files changed

+2934
-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: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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+
dataProvider,
16+
error,
17+
initialViewState,
18+
loading,
19+
rootId,
20+
loadSubtree,
21+
mergeSubtree,
22+
} = useComplexTreeViewApi(TREE_ID, currentRecordID, apiEndpoint);
23+
24+
/**
25+
* Renders the DOM for a tree item label.
26+
* For synthetic limited indicator nodes, renders as a clickable link.
27+
* Otherwise renders the regular title.
28+
*/
29+
const renderTreeItemTitle = ({ item }) => {
30+
const isLimitedIndicator = item.data.isLimitedIndicator === true;
31+
const isLoading = loadingNodeId === item.index;
32+
33+
if (isLimitedIndicator) {
34+
return (
35+
<span className={`${CSS_CLASS}__item-link ${CSS_CLASS}__item-link--limited`}>
36+
Too many records (
37+
<a href={item.data.linkUrl} className={`${CSS_CLASS}__limited-link`}>show as list</a>
38+
)
39+
</span>
40+
);
41+
}
42+
43+
if (isLoading) {
44+
return (
45+
<span className={`${CSS_CLASS}__item-link`}>
46+
<span className={`${CSS_CLASS}__loading`}>Loading...</span>
47+
{item.data.title}
48+
</span>
49+
);
50+
}
51+
52+
return (
53+
<span className={`${CSS_CLASS}__item-link`}>
54+
{item.data.title}
55+
</span>
56+
);
57+
};
58+
59+
/**
60+
* This is triggered by Double Click (mouse) or Enter (keyboard) on a focused item.
61+
*/
62+
const handlePrimaryAction = (items) => {
63+
window.location.href = `/admin/pages/edit/show/${items.data.id}`;
64+
};
65+
66+
/**
67+
* Handler for item selection - no-op for now
68+
*/
69+
const handleSelectItems = () => {};
70+
71+
/**
72+
* Handles expanding a tree item. If the item has unloaded children,
73+
* loads the subtree and merges it into the tree.
74+
*/
75+
// eslint-disable-next-line no-unused-vars
76+
const handleExpandItem = async (item, treeId) => {
77+
const hasUnloadedChildren = item.data.expanded === false && item.data.count > 0 && item.children.length === 0;
78+
if (hasUnloadedChildren) {
79+
try {
80+
setLoadingNodeId(item.index);
81+
const subtree = await loadSubtree(item.data.id);
82+
await mergeSubtree(item.data.id, subtree.items);
83+
} finally {
84+
setLoadingNodeId(null);
85+
}
86+
}
87+
};
88+
89+
/**
90+
* INTERCEPT KEYDOWN
91+
* This is the key fix. We intercept the event at the container level.
92+
*/
93+
const handleRootKeyDown = (e) => {
94+
// Check if the key pressed is Space
95+
if (e.key !== ' ') {
96+
return;
97+
}
98+
// Find the closest tree item container
99+
const treeItem = e.target.closest('[role="treeitem"]');
100+
101+
// If we are on a tree item...
102+
if (treeItem) {
103+
// Check if it is a Folder. Folders have 'aria-expanded' (true or false).
104+
// Leaves do NOT have this attribute.
105+
const isFolder = treeItem.hasAttribute('aria-expanded');
106+
107+
// If it's NOT a folder (it's a leaf), prevent the default browser behavior.
108+
// This stops the browser from turning "Space" into a "Click" event.
109+
if (!isFolder) {
110+
e.preventDefault();
111+
}
112+
}
113+
};
114+
115+
if (loading) {
116+
return <div>Loading page tree...</div>;
117+
}
118+
if (error) {
119+
return <div style={{ color: 'red' }}>Error loading data: {error}</div>;
120+
}
121+
if (!dataProvider || !initialViewState) {
122+
return <div>No data available.</div>;
123+
}
124+
125+
return (
126+
<div className={CSS_CLASS} onKeyDown={handleRootKeyDown}>
127+
<UncontrolledTreeEnvironment
128+
dataProvider={dataProvider}
129+
getItemTitle={item => item.data.title}
130+
viewState={initialViewState}
131+
// canDragAndDrop={false}
132+
// canDropOnFolder={false}
133+
// canReorderItems={false}
134+
canRenameItems={false}
135+
canSearchItems={false}
136+
canSearchByStartingTyping={false}
137+
renderItemTitle={renderTreeItemTitle}
138+
onPrimaryAction={handlePrimaryAction}
139+
onSelectItems={handleSelectItems}
140+
onExpandItem={handleExpandItem}
141+
>
142+
<Tree treeId={TREE_ID} rootItem={rootId} treeLabel="Page Tree" />
143+
</UncontrolledTreeEnvironment>
144+
</div>
145+
);
146+
};
147+
148+
ComplexTreeView.propTypes = {
149+
currentRecordID: PropTypes.number,
150+
apiEndpoint: PropTypes.string,
151+
};
152+
153+
export { ComplexTreeView as Component };
154+
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)