Skip to content

Commit 2da68ff

Browse files
authored
Migration to MUI TreeView v7 (#747)
Signed-off-by: achour94 <[email protected]>
1 parent a6fdc19 commit 2da68ff

File tree

3 files changed

+103
-60
lines changed

3 files changed

+103
-60
lines changed

package-lock.json

Lines changed: 48 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
"@mui/icons-material": "^5.16.14",
5959
"@mui/lab": "5.0.0-alpha.175",
6060
"@mui/material": "^5.16.14",
61-
"@mui/x-tree-view": "^6.17.0",
61+
"@mui/x-tree-view": "^7.28.1",
6262
"ag-grid-community": "^33.0.3",
6363
"ag-grid-react": "^33.0.4",
6464
"notistack": "^3.0.2",

src/components/treeViewFinder/TreeViewFinder.tsx

Lines changed: 54 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
styled,
2020
Typography,
2121
} from '@mui/material';
22-
import { TreeItem, TreeView, TreeViewClasses } from '@mui/x-tree-view';
22+
import { TreeItem, SimpleTreeView, SimpleTreeViewClasses } from '@mui/x-tree-view';
2323
import {
2424
Check as CheckIcon,
2525
ChevronRight as ChevronRightIcon,
@@ -64,6 +64,14 @@ const defaultStyles = {
6464
export const generateTreeViewFinderClass = (className: string) => `GsiTreeViewFinder-${className}`;
6565
const composeClasses = makeComposeClasses(generateTreeViewFinderClass);
6666

67+
function CustomExpandIcon({ className }: Readonly<{ className?: string }>) {
68+
return <ChevronRightIcon className={className} />;
69+
}
70+
71+
function CustomCollapseIcon({ className }: Readonly<{ className?: string }>) {
72+
return <ExpandMoreIcon className={className} />;
73+
}
74+
6775
export interface TreeViewFinderNodeProps {
6876
id: UUID;
6977
name: string;
@@ -85,7 +93,7 @@ export interface TreeViewFinderProps {
8593
selected?: string[];
8694
expanded?: string[];
8795
multiSelect?: boolean;
88-
classes?: Partial<TreeViewClasses>;
96+
classes?: Partial<SimpleTreeViewClasses>;
8997
className?: string;
9098

9199
// dialog props
@@ -99,7 +107,7 @@ export interface TreeViewFinderProps {
99107
// data management props
100108
onlyLeaves?: boolean;
101109
data?: TreeViewFinderNodeProps[];
102-
onTreeBrowse?: (nodeId: string) => void;
110+
onTreeBrowse?: (itemId: string) => void;
103111
sortMethod?: (a: TreeViewFinderNodeProps, b: TreeViewFinderNodeProps) => number;
104112
}
105113

@@ -175,7 +183,7 @@ function TreeViewFinderComponant(props: Readonly<TreeViewFinderProps>) {
175183
const isValidationDisabled = () => {
176184
return (
177185
selected?.length === 0 ||
178-
(selected?.length === selectedProp?.length && selected?.every((nodeId) => selectedProp?.includes(nodeId)))
186+
(selected?.length === selectedProp?.length && selected?.every((itemId) => selectedProp?.includes(itemId)))
179187
);
180188
};
181189

@@ -191,22 +199,22 @@ function TreeViewFinderComponant(props: Readonly<TreeViewFinderProps>) {
191199
}, []);
192200

193201
const findParents = (
194-
nodeId: string,
202+
itemId: string,
195203
nodes: TreeViewFinderNodeProps[],
196204
parentPath: TreeViewFinderNodeProps[] = []
197205
): TreeViewFinderNodeProps[] | null => {
198206
let result: TreeViewFinderNodeProps[] | null = null;
199207

200208
nodes.some((node) => {
201209
// If the current node matches the selected node, set result and break
202-
if (node.id === nodeId) {
210+
if (node.id === itemId) {
203211
result = parentPath;
204212
return true;
205213
}
206214

207215
// If the current node has children, recursively search them
208216
if (node.children) {
209-
const childResult = findParents(nodeId, node.children, [...parentPath, node]);
217+
const childResult = findParents(itemId, node.children, [...parentPath, node]);
210218
if (childResult) {
211219
result = childResult;
212220
return true;
@@ -232,34 +240,34 @@ function TreeViewFinderComponant(props: Readonly<TreeViewFinderProps>) {
232240
return [];
233241
}
234242
return selected
235-
.map((nodeId) => {
236-
const selectedNode = mapPrintedNodes[nodeId];
243+
.map((itemId) => {
244+
const selectedNode = mapPrintedNodes[itemId];
237245
if (!selectedNode) {
238246
return null;
239247
}
240248

241-
const parents = findParents(nodeId, data ?? []);
249+
const parents = findParents(itemId, data ?? []);
242250

243251
return {
244252
...selectedNode,
245253
parents: parents ?? [],
246254
};
247255
})
248-
.filter((node) => node !== null);
256+
.filter((node) => node !== null) as TreeViewFinderNodeProps[];
249257
};
250258

251-
const handleNodeToggle = (_e: React.SyntheticEvent, nodeIds: string[]) => {
259+
const handleNodeToggle = (_e: React.SyntheticEvent, itemIds: string[]) => {
252260
// onTreeBrowse proc only on last node clicked and only when expanded
253-
nodeIds.every((nodeId) => {
254-
if (!expanded?.includes(nodeId)) {
261+
itemIds.every((itemId) => {
262+
if (!expanded?.includes(itemId)) {
255263
// proc onTreeBrowse here
256-
onTreeBrowse?.(nodeId);
264+
onTreeBrowse?.(itemId);
257265
return false; // break loop to call onTreeBrowse only once
258266
}
259267
return true;
260268
});
261269

262-
setExpanded(nodeIds);
270+
setExpanded(itemIds);
263271
// will proc onNodeSelect then ...
264272
};
265273

@@ -288,7 +296,7 @@ function TreeViewFinderComponant(props: Readonly<TreeViewFinderProps>) {
288296
// if we have selected elements by default, we scroll to it
289297
if (selectedProp.length > 0 && autoScrollAllowed) {
290298
// we check if all expanded nodes by default all already expanded first
291-
const isNodeExpanded = expandedProp?.every((nodeId) => expanded?.includes(nodeId));
299+
const isNodeExpanded = expandedProp?.every((itemId) => expanded?.includes(itemId));
292300

293301
// we got the last element that we suppose to scroll to
294302
const lastScrollRef = scrollRef.current[scrollRef.current.length - 1];
@@ -304,11 +312,11 @@ function TreeViewFinderComponant(props: Readonly<TreeViewFinderProps>) {
304312
}, [expanded, selectedProp, expandedProp, data, autoScrollAllowed]);
305313

306314
/* User Interaction management */
307-
const handleNodeSelect = (_e: React.SyntheticEvent, values: string | string[]) => {
315+
const handleNodeSelect = (_e: React.SyntheticEvent, values: string | string[] | null) => {
308316
// Default management
309317
if (multiSelect && Array.isArray(values)) {
310-
setSelected(values.filter((nodeId) => isSelectable(mapPrintedNodes[nodeId])));
311-
} else if (!Array.isArray(values)) {
318+
setSelected(values.filter((itemId) => isSelectable(mapPrintedNodes[itemId])));
319+
} else if (typeof values === 'string') {
312320
// Toggle selection to allow unselection
313321
if (selected?.includes(values)) {
314322
setSelected([]);
@@ -348,7 +356,7 @@ function TreeViewFinderComponant(props: Readonly<TreeViewFinderProps>) {
348356
return null;
349357
}
350358

351-
if (isSelectable(node) && selected?.find((nodeId) => nodeId === node.id)) {
359+
if (isSelectable(node) && selected?.find((itemId) => itemId === node.id)) {
352360
return <CheckIcon className={composeClasses(classes, cssLabelIcon)} />;
353361
}
354362
if (node.icon) {
@@ -365,36 +373,39 @@ function TreeViewFinderComponant(props: Readonly<TreeViewFinderProps>) {
365373
</div>
366374
);
367375
};
376+
368377
const showChevron = (node: TreeViewFinderNodeProps) => {
369-
// by defaut show Chevron if childrenCount is null or undefined otherwise only if > 0
370-
return !!(node.childrenCount == null || (node.childrenCount && node.childrenCount > 0));
378+
return !!(node.childrenCount && node.childrenCount > 0);
371379
};
372380

373381
const renderTree = (node: TreeViewFinderNodeProps) => {
374382
if (!node) {
375383
return null;
376384
}
377385
let childrenNodes = null;
378-
379-
if (Array.isArray(node.children)) {
380-
if (node.children.length) {
381-
const sortedChildren = node.children.sort(sortMethod);
382-
childrenNodes = sortedChildren.map((child) => renderTree(child));
383-
} else {
384-
childrenNodes = [false]; // Pass non empty Array here to simulate a child then this node isn't considered as a leaf.
385-
}
386+
const showExpandIcon = showChevron(node);
387+
if (Array.isArray(node.children) && node.children.length > 0) {
388+
childrenNodes = node.children.toSorted(sortMethod).map(renderTree);
389+
} else if (showExpandIcon) {
390+
childrenNodes = [<span key="placeholder" style={{ display: 'none' }} />]; // simulate placeholder so expand icon is shown
386391
}
387392
return (
388393
<TreeItem
389394
key={node.id}
390-
nodeId={node.id}
395+
itemId={node.id}
391396
label={renderTreeItemLabel(node)}
392-
expandIcon={
393-
showChevron(node) ? <ChevronRightIcon className={composeClasses(classes, cssIcon)} /> : null
394-
}
395-
collapseIcon={
396-
showChevron(node) ? <ExpandMoreIcon className={composeClasses(classes, cssIcon)} /> : null
397-
}
397+
slots={{
398+
expandIcon: CustomExpandIcon,
399+
collapseIcon: CustomCollapseIcon,
400+
}}
401+
slotProps={{
402+
expandIcon: {
403+
className: composeClasses(classes, cssIcon),
404+
},
405+
collapseIcon: {
406+
className: composeClasses(classes, cssIcon),
407+
},
408+
}}
398409
ref={(element) => {
399410
if (selectedProp?.includes(node.id)) {
400411
scrollRef.current.push(element);
@@ -445,17 +456,15 @@ function TreeViewFinderComponant(props: Readonly<TreeViewFinderProps>) {
445456
{contentText ?? intl.formatMessage({ id: 'treeview_finder/contentText' }, { multiSelect })}
446457
</DialogContentText>
447458

448-
<TreeView
449-
// Controlled props
450-
expanded={expanded}
451-
// events
452-
onNodeToggle={handleNodeToggle}
453-
onNodeSelect={handleNodeSelect}
459+
<SimpleTreeView
460+
expandedItems={expanded}
461+
onExpandedItemsChange={handleNodeToggle}
462+
onSelectedItemsChange={handleNodeSelect}
454463
// Uncontrolled props
455464
{...getTreeViewSelectionProps()}
456465
>
457466
{data && Array.isArray(data) ? data.sort(sortMethod).map((child) => renderTree(child)) : null}
458-
</TreeView>
467+
</SimpleTreeView>
459468
</DialogContent>
460469
<DialogActions>
461470
<CancelButton

0 commit comments

Comments
 (0)