@dxos/plugin-explorer 0.8.4-main.fcfe5033a5 → 0.8.4-staging.60fe92afc8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +102 -5
- package/PLUGIN.mdl +340 -0
- package/dist/lib/neutral/ExplorerArticle-4I7PNGDC.mjs +459 -0
- package/dist/lib/neutral/ExplorerArticle-4I7PNGDC.mjs.map +7 -0
- package/dist/lib/neutral/ExplorerPlugin.mjs +10 -0
- package/dist/lib/neutral/capabilities/index.mjs +11 -0
- package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
- package/dist/lib/neutral/chunk-3D7BYXOR.mjs +37 -0
- package/dist/lib/neutral/chunk-3D7BYXOR.mjs.map +7 -0
- package/dist/lib/neutral/chunk-42BYLQQA.mjs +42 -0
- package/dist/lib/neutral/chunk-42BYLQQA.mjs.map +7 -0
- package/dist/lib/neutral/chunk-7XUDLV6E.mjs +287 -0
- package/dist/lib/neutral/chunk-7XUDLV6E.mjs.map +7 -0
- package/dist/lib/neutral/chunk-IKHJV3Q4.mjs +20 -0
- package/dist/lib/neutral/chunk-IKHJV3Q4.mjs.map +7 -0
- package/dist/lib/{browser/types/index.mjs → neutral/chunk-YBCHBVCJ.mjs} +13 -14
- package/dist/lib/neutral/chunk-YBCHBVCJ.mjs.map +7 -0
- package/dist/lib/{browser → neutral/components}/index.mjs +730 -437
- package/dist/lib/neutral/components/index.mjs.map +7 -0
- package/dist/lib/neutral/containers/index.mjs +9 -0
- package/dist/lib/neutral/containers/index.mjs.map +7 -0
- package/dist/lib/neutral/create-object-F6TKVAGV.mjs +39 -0
- package/dist/lib/neutral/create-object-F6TKVAGV.mjs.map +7 -0
- package/dist/lib/neutral/hooks/index.mjs +45 -0
- package/dist/lib/neutral/hooks/index.mjs.map +7 -0
- package/dist/lib/neutral/index.mjs +14 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/{browser → neutral}/meta.mjs +1 -1
- package/dist/lib/neutral/plugin.mjs +12 -0
- package/dist/lib/neutral/plugin.mjs.map +7 -0
- package/dist/lib/neutral/react-surface-APBW2VQG.mjs +26 -0
- package/dist/lib/neutral/react-surface-APBW2VQG.mjs.map +7 -0
- package/dist/lib/neutral/testing/index.mjs +139 -0
- package/dist/lib/neutral/testing/index.mjs.map +7 -0
- package/dist/lib/neutral/translations.mjs +33 -0
- package/dist/lib/neutral/translations.mjs.map +7 -0
- package/dist/lib/neutral/types/index.mjs +10 -0
- package/dist/lib/neutral/types/index.mjs.map +7 -0
- package/dist/types/data/cities.d.ts +4 -4
- package/dist/types/data/cities.d.ts.map +1 -1
- package/dist/types/data/countries-110m.d.ts +19 -22
- package/dist/types/data/countries-110m.d.ts.map +1 -1
- package/dist/types/src/ExplorerPlugin.d.ts +1 -0
- package/dist/types/src/ExplorerPlugin.d.ts.map +1 -1
- package/dist/types/src/ExplorerPlugin.test.d.ts +2 -0
- package/dist/types/src/ExplorerPlugin.test.d.ts.map +1 -0
- package/dist/types/src/capabilities/create-object.d.ts +11 -0
- package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
- package/dist/types/src/capabilities/index.d.ts +6 -0
- package/dist/types/src/capabilities/index.d.ts.map +1 -1
- package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
- package/dist/types/src/components/Chart/Chart.d.ts +1 -1
- package/dist/types/src/components/Chart/Chart.d.ts.map +1 -1
- package/dist/types/src/components/Chart/Chart.stories.d.ts +4 -1
- package/dist/types/src/components/Chart/Chart.stories.d.ts.map +1 -1
- package/dist/types/src/components/Globe/Globe.d.ts +1 -1
- package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
- package/dist/types/src/components/Globe/Globe.stories.d.ts +5 -2
- package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
- package/dist/types/src/components/Graph/CanvasForceGraph.d.ts +13 -0
- package/dist/types/src/components/Graph/CanvasForceGraph.d.ts.map +1 -0
- package/dist/types/src/components/Graph/{D3ForceGraph.stories.d.ts → CanvasForceGraph.stories.d.ts} +3 -3
- package/dist/types/src/components/Graph/CanvasForceGraph.stories.d.ts.map +1 -0
- package/dist/types/src/components/Graph/ForceGraph.d.ts +12 -5
- package/dist/types/src/components/Graph/ForceGraph.d.ts.map +1 -1
- package/dist/types/src/components/Graph/ForceGraph.stories.d.ts +3 -1
- package/dist/types/src/components/Graph/ForceGraph.stories.d.ts.map +1 -1
- package/dist/types/src/components/Graph/{adapter.d.ts → graph-adapter.d.ts} +1 -1
- package/dist/types/src/components/Graph/graph-adapter.d.ts.map +1 -0
- package/dist/types/src/components/Graph/index.d.ts +1 -1
- package/dist/types/src/components/Graph/index.d.ts.map +1 -1
- package/dist/types/src/components/Lattice/Lattice.d.ts +20 -0
- package/dist/types/src/components/Lattice/Lattice.d.ts.map +1 -0
- package/dist/types/src/components/Lattice/Lattice.stories.d.ts +8 -0
- package/dist/types/src/components/Lattice/Lattice.stories.d.ts.map +1 -0
- package/dist/types/src/components/Lattice/index.d.ts +2 -0
- package/dist/types/src/components/Lattice/index.d.ts.map +1 -0
- package/dist/types/src/components/Tree/EdgeBundling.stories.d.ts +21 -0
- package/dist/types/src/components/Tree/EdgeBundling.stories.d.ts.map +1 -0
- package/dist/types/src/components/Tree/Tree.d.ts +20 -23
- package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/Tree.stories.d.ts +5 -12
- package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
- package/dist/types/src/components/Tree/index.d.ts +2 -0
- package/dist/types/src/components/Tree/index.d.ts.map +1 -1
- package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts +37 -2
- package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts.map +1 -1
- package/dist/types/src/components/Tree/layout/RadialTree.d.ts +35 -2
- package/dist/types/src/components/Tree/layout/RadialTree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/layout/TidyTree.d.ts +24 -2
- package/dist/types/src/components/Tree/layout/TidyTree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/layout/hierarchy.d.ts +17 -0
- package/dist/types/src/components/Tree/layout/hierarchy.d.ts.map +1 -0
- package/dist/types/src/components/Tree/layout/index.d.ts +5 -4
- package/dist/types/src/components/Tree/layout/index.d.ts.map +1 -1
- package/dist/types/src/components/Tree/layout/slots.d.ts +7 -0
- package/dist/types/src/components/Tree/layout/slots.d.ts.map +1 -0
- package/dist/types/src/components/Tree/layout/useContainerSize.d.ts +15 -0
- package/dist/types/src/components/Tree/layout/useContainerSize.d.ts.map +1 -0
- package/dist/types/src/components/Tree/types/tree.d.ts +41 -20
- package/dist/types/src/components/Tree/types/tree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/types/types.d.ts +14 -4
- package/dist/types/src/components/Tree/types/types.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts +8 -0
- package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts.map +1 -0
- package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts +15 -0
- package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/ExplorerArticle/Visualization.d.ts +18 -0
- package/dist/types/src/containers/ExplorerArticle/Visualization.d.ts.map +1 -0
- package/dist/types/src/containers/ExplorerArticle/index.d.ts +2 -0
- package/dist/types/src/containers/ExplorerArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/ExplorerArticle/variants.d.ts +9 -0
- package/dist/types/src/containers/ExplorerArticle/variants.d.ts.map +1 -0
- package/dist/types/src/containers/index.d.ts +1 -1
- package/dist/types/src/containers/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useGraphModel.d.ts +2 -2
- package/dist/types/src/hooks/useGraphModel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/meta.d.ts +1 -1
- package/dist/types/src/meta.d.ts.map +1 -1
- package/dist/types/src/plugin.d.ts +3 -0
- package/dist/types/src/plugin.d.ts.map +1 -0
- package/dist/types/src/{components/Tree/testing → testing}/generator.d.ts +1 -1
- package/dist/types/src/testing/generator.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +4 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/relations.d.ts +32 -0
- package/dist/types/src/testing/relations.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +17 -17
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/Graph.d.ts +5 -6
- package/dist/types/src/types/Graph.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +3 -0
- package/dist/types/src/util/index.d.ts.map +1 -0
- package/dist/types/src/util/node-color.d.ts +13 -0
- package/dist/types/src/util/node-color.d.ts.map +1 -0
- package/dist/types/src/{components → util}/plot.d.ts +1 -1
- package/dist/types/src/util/plot.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +102 -68
- package/src/ExplorerPlugin.test.ts +26 -0
- package/src/ExplorerPlugin.tsx +11 -34
- package/src/capabilities/create-object.ts +36 -0
- package/src/capabilities/index.ts +1 -0
- package/src/capabilities/react-surface.tsx +2 -2
- package/src/components/Chart/Chart.stories.tsx +14 -20
- package/src/components/Chart/Chart.tsx +1 -1
- package/src/components/Globe/Globe.stories.tsx +17 -19
- package/src/components/Globe/Globe.tsx +1 -1
- package/src/components/Graph/CanvasForceGraph.stories.tsx +97 -0
- package/src/components/Graph/CanvasForceGraph.tsx +124 -0
- package/src/components/Graph/ForceGraph.stories.tsx +88 -38
- package/src/components/Graph/ForceGraph.tsx +105 -85
- package/src/components/Graph/index.ts +1 -1
- package/src/components/Lattice/Lattice.stories.tsx +104 -0
- package/src/components/Lattice/Lattice.tsx +182 -0
- package/src/components/Lattice/index.ts +5 -0
- package/src/components/Tree/EdgeBundling.stories.tsx +144 -0
- package/src/components/Tree/Tree.stories.tsx +17 -38
- package/src/components/Tree/Tree.tsx +69 -100
- package/src/components/Tree/index.ts +2 -0
- package/src/components/Tree/layout/HierarchicalEdgeBundling.tsx +335 -0
- package/src/components/Tree/layout/RadialTree.tsx +242 -0
- package/src/components/Tree/layout/TidyTree.tsx +246 -0
- package/src/components/Tree/layout/hierarchy.ts +32 -0
- package/src/components/Tree/layout/index.ts +5 -5
- package/src/components/Tree/layout/slots.ts +19 -0
- package/src/components/Tree/layout/useContainerSize.ts +43 -0
- package/src/components/Tree/types/tree.test.ts +2 -2
- package/src/components/Tree/types/tree.ts +23 -28
- package/src/components/Tree/types/types.ts +38 -29
- package/src/components/index.ts +1 -0
- package/src/containers/ExplorerArticle/ExplorerArticle.stories.tsx +152 -0
- package/src/containers/ExplorerArticle/ExplorerArticle.tsx +120 -0
- package/src/containers/ExplorerArticle/Visualization.tsx +523 -0
- package/src/containers/ExplorerArticle/index.ts +5 -0
- package/src/containers/ExplorerArticle/variants.ts +47 -0
- package/src/containers/index.ts +1 -1
- package/src/hooks/useGraphModel.ts +10 -6
- package/src/index.ts +1 -4
- package/src/meta.ts +26 -7
- package/src/plugin.ts +9 -0
- package/src/{components/Tree/testing → testing}/generator.ts +3 -3
- package/src/testing/index.ts +9 -0
- package/src/testing/relations.ts +117 -0
- package/src/translations.ts +1 -1
- package/src/types/ExplorerAction.ts +1 -1
- package/src/types/Graph.ts +7 -15
- package/src/util/index.ts +6 -0
- package/src/util/node-color.ts +23 -0
- package/src/{components → util}/plot.ts +16 -4
- package/src/vite-env.d.ts +10 -0
- package/dist/lib/browser/chunk-LSUP47BZ.mjs +0 -24
- package/dist/lib/browser/chunk-LSUP47BZ.mjs.map +0 -7
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/types/index.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-EN3JZNEY.mjs +0 -26
- package/dist/lib/node-esm/chunk-EN3JZNEY.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
- package/dist/lib/node-esm/index.mjs +0 -11375
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/meta.mjs +0 -9
- package/dist/lib/node-esm/types/index.mjs +0 -71
- package/dist/lib/node-esm/types/index.mjs.map +0 -7
- package/dist/types/src/components/Graph/D3ForceGraph.d.ts +0 -15
- package/dist/types/src/components/Graph/D3ForceGraph.d.ts.map +0 -1
- package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts.map +0 -1
- package/dist/types/src/components/Graph/adapter.d.ts.map +0 -1
- package/dist/types/src/components/Graph/testing.d.ts +0 -14
- package/dist/types/src/components/Graph/testing.d.ts.map +0 -1
- package/dist/types/src/components/Tree/testing/generator.d.ts.map +0 -1
- package/dist/types/src/components/Tree/testing/index.d.ts +0 -2
- package/dist/types/src/components/Tree/testing/index.d.ts.map +0 -1
- package/dist/types/src/components/plot.d.ts.map +0 -1
- package/dist/types/src/containers/ExplorerContainer/ExplorerContainer.d.ts +0 -6
- package/dist/types/src/containers/ExplorerContainer/ExplorerContainer.d.ts.map +0 -1
- package/dist/types/src/containers/ExplorerContainer/index.d.ts +0 -2
- package/dist/types/src/containers/ExplorerContainer/index.d.ts.map +0 -1
- package/src/components/Graph/D3ForceGraph.stories.tsx +0 -83
- package/src/components/Graph/D3ForceGraph.tsx +0 -108
- package/src/components/Graph/testing.ts +0 -58
- package/src/components/Tree/layout/HierarchicalEdgeBundling.ts +0 -162
- package/src/components/Tree/layout/RadialTree.ts +0 -94
- package/src/components/Tree/layout/TidyTree.ts +0 -101
- package/src/components/Tree/testing/index.ts +0 -5
- package/src/containers/ExplorerContainer/ExplorerContainer.tsx +0 -53
- package/src/containers/ExplorerContainer/index.ts +0 -5
- /package/dist/lib/{browser/chunk-J5LGTIGS.mjs.map → neutral/ExplorerPlugin.mjs.map} +0 -0
- /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs +0 -0
- /package/dist/lib/{browser/meta.mjs.map → neutral/chunk-J5LGTIGS.mjs.map} +0 -0
- /package/dist/lib/{node-esm/chunk-HSLMI22Q.mjs.map → neutral/index.mjs.map} +0 -0
- /package/dist/lib/{node-esm → neutral}/meta.mjs.map +0 -0
- /package/src/components/Graph/{adapter.ts → graph-adapter.ts} +0 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
// Copyright 2024 Observable, Inc.
|
|
4
|
+
//
|
|
5
|
+
|
|
6
|
+
import { curveBumpX, link as d3Link, select, tree as d3Tree } from 'd3';
|
|
7
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
10
|
+
import { mx } from '@dxos/ui-theme';
|
|
11
|
+
|
|
12
|
+
import { type TreeNode } from '../types';
|
|
13
|
+
import { buildHierarchy, isCollapsed, isLeaf } from './hierarchy';
|
|
14
|
+
import { type TreeLayoutSlots, defaultTreeLayoutSlots } from './slots';
|
|
15
|
+
import { useContainerSize } from './useContainerSize';
|
|
16
|
+
|
|
17
|
+
const TRANSITION_MS = 350;
|
|
18
|
+
|
|
19
|
+
export type TidyTreeProps = ThemedClassName<{
|
|
20
|
+
data: TreeNode;
|
|
21
|
+
label?: (d: TreeNode) => string;
|
|
22
|
+
slots?: TreeLayoutSlots;
|
|
23
|
+
/** Node radius. */
|
|
24
|
+
r?: number;
|
|
25
|
+
/** Margin in screen pixels reserved around the layout. */
|
|
26
|
+
margin?: number;
|
|
27
|
+
/** Initial set of collapsed node ids. */
|
|
28
|
+
initialCollapsed?: Iterable<string>;
|
|
29
|
+
/** Notified when the user clicks a node. */
|
|
30
|
+
onNodeClick?: (node: TreeNode) => void;
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Tidy (horizontal) tree layout based on the D3 reference component.
|
|
35
|
+
* https://observablehq.com/@d3/tree-component
|
|
36
|
+
*
|
|
37
|
+
* Click a node with children to toggle collapse / expand.
|
|
38
|
+
*/
|
|
39
|
+
export const TidyTree = ({
|
|
40
|
+
classNames,
|
|
41
|
+
data,
|
|
42
|
+
label = (d) => d.label ?? d.id,
|
|
43
|
+
slots = defaultTreeLayoutSlots,
|
|
44
|
+
r = 4,
|
|
45
|
+
margin = 24,
|
|
46
|
+
initialCollapsed,
|
|
47
|
+
onNodeClick,
|
|
48
|
+
}: TidyTreeProps) => {
|
|
49
|
+
const svgRef = useRef<SVGSVGElement | null>(null);
|
|
50
|
+
const { setRef, width, height } = useContainerSize();
|
|
51
|
+
|
|
52
|
+
const [collapsed, setCollapsed] = useState<Set<string>>(() => new Set(initialCollapsed ?? []));
|
|
53
|
+
const toggle = useCallback((id: string) => {
|
|
54
|
+
setCollapsed((prev) => {
|
|
55
|
+
const next = new Set(prev);
|
|
56
|
+
if (next.has(id)) {
|
|
57
|
+
next.delete(id);
|
|
58
|
+
} else {
|
|
59
|
+
next.add(id);
|
|
60
|
+
}
|
|
61
|
+
return next;
|
|
62
|
+
});
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
// Stable click handler ref so the d3 render fn doesn't need to be re-bound.
|
|
66
|
+
const handleClickRef = useRef<(node: TreeNode) => void>(() => {});
|
|
67
|
+
handleClickRef.current = (node: TreeNode) => {
|
|
68
|
+
onNodeClick?.(node);
|
|
69
|
+
if (node.children?.length) {
|
|
70
|
+
toggle(node.id);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const root = useMemo(() => buildHierarchy(data, collapsed), [data, collapsed]);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!svgRef.current || !width || !height) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
renderTidyTree(svgRef.current, root, {
|
|
82
|
+
width,
|
|
83
|
+
height,
|
|
84
|
+
r,
|
|
85
|
+
margin,
|
|
86
|
+
label,
|
|
87
|
+
slots,
|
|
88
|
+
collapsed,
|
|
89
|
+
onNodeClick: (n) => handleClickRef.current(n),
|
|
90
|
+
});
|
|
91
|
+
}, [root, width, height, r, margin, label, slots, collapsed]);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div ref={setRef} className={mx('dx-expander relative', classNames)}>
|
|
95
|
+
{width > 0 && height > 0 && (
|
|
96
|
+
<svg
|
|
97
|
+
ref={svgRef}
|
|
98
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
99
|
+
width={width}
|
|
100
|
+
height={height}
|
|
101
|
+
viewBox={`${-width / 2} ${-height / 2} ${width} ${height}`}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
type RenderOptions = {
|
|
109
|
+
width: number;
|
|
110
|
+
height: number;
|
|
111
|
+
r: number;
|
|
112
|
+
margin: number;
|
|
113
|
+
label: (d: TreeNode) => string;
|
|
114
|
+
slots: TreeLayoutSlots;
|
|
115
|
+
collapsed: Set<string>;
|
|
116
|
+
onNodeClick: (node: TreeNode) => void;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const renderTidyTree = (svgElement: SVGSVGElement, root: any, options: RenderOptions) => {
|
|
120
|
+
const { width, height, r, margin, label, slots, collapsed, onNodeClick } = options;
|
|
121
|
+
const svg = select(svgElement);
|
|
122
|
+
|
|
123
|
+
// Compute layout: nodeSize gives a stable, content-driven scale.
|
|
124
|
+
// dx = vertical spacing between siblings; dy = horizontal spacing between depths.
|
|
125
|
+
const dx = 18;
|
|
126
|
+
const dy = Math.max(60, (width - margin * 2) / Math.max(1, root.height + 1));
|
|
127
|
+
d3Tree<TreeNode>().nodeSize([dx, dy])(root);
|
|
128
|
+
|
|
129
|
+
// Center the tree vertically; lay it out horizontally from left.
|
|
130
|
+
let x0 = Infinity;
|
|
131
|
+
let x1 = -x0;
|
|
132
|
+
root.each((d: any) => {
|
|
133
|
+
if (d.x > x1) {
|
|
134
|
+
x1 = d.x;
|
|
135
|
+
}
|
|
136
|
+
if (d.x < x0) {
|
|
137
|
+
x0 = d.x;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const treeWidth = width - margin * 2;
|
|
142
|
+
const treeHeight = x1 - x0;
|
|
143
|
+
const scaleY = treeHeight > 0 ? Math.min(1, (height - margin * 2) / treeHeight) : 1;
|
|
144
|
+
const offsetX = -treeWidth / 2;
|
|
145
|
+
const offsetY = -(x0 + x1) / 2;
|
|
146
|
+
|
|
147
|
+
// Root group ensures consistent transforms on re-renders.
|
|
148
|
+
const g = svg.selectAll<SVGGElement, null>('g.dx-tidy-root').data([null]).join('g').classed('dx-tidy-root', true);
|
|
149
|
+
|
|
150
|
+
// Links layer.
|
|
151
|
+
const linksLayer = g
|
|
152
|
+
.selectAll<SVGGElement, null>('g.dx-tidy-links')
|
|
153
|
+
.data([null])
|
|
154
|
+
.join('g')
|
|
155
|
+
.classed('dx-tidy-links', true);
|
|
156
|
+
|
|
157
|
+
// Nodes layer (rendered above links).
|
|
158
|
+
const nodesLayer = g
|
|
159
|
+
.selectAll<SVGGElement, null>('g.dx-tidy-nodes')
|
|
160
|
+
.data([null])
|
|
161
|
+
.join('g')
|
|
162
|
+
.classed('dx-tidy-nodes', true);
|
|
163
|
+
|
|
164
|
+
// Links.
|
|
165
|
+
const linkPath = d3Link<any, any>(curveBumpX)
|
|
166
|
+
.x((d: any) => offsetX + d.y)
|
|
167
|
+
.y((d: any) => (d.x + offsetY) * scaleY);
|
|
168
|
+
|
|
169
|
+
linksLayer
|
|
170
|
+
.selectAll<SVGPathElement, any>('path')
|
|
171
|
+
.data(root.links(), (d: any) => `${d.source.data.id}->${d.target.data.id}`)
|
|
172
|
+
.join(
|
|
173
|
+
(enter) =>
|
|
174
|
+
enter
|
|
175
|
+
.append('path')
|
|
176
|
+
.attr('class', slots.path ?? '')
|
|
177
|
+
.attr('fill', 'none')
|
|
178
|
+
.attr('opacity', 0),
|
|
179
|
+
(update) => update,
|
|
180
|
+
(exit) => exit.transition().duration(TRANSITION_MS).attr('opacity', 0).remove(),
|
|
181
|
+
)
|
|
182
|
+
.transition()
|
|
183
|
+
.duration(TRANSITION_MS)
|
|
184
|
+
.attr('opacity', 1)
|
|
185
|
+
.attr('d', linkPath);
|
|
186
|
+
|
|
187
|
+
// Nodes.
|
|
188
|
+
const node = nodesLayer.selectAll<SVGGElement, any>('g.dx-tidy-node').data(root.descendants(), (d: any) => d.data.id);
|
|
189
|
+
|
|
190
|
+
const nodeEnter = node
|
|
191
|
+
.enter()
|
|
192
|
+
.append('g')
|
|
193
|
+
.classed('dx-tidy-node', true)
|
|
194
|
+
.attr('transform', (d: any) => `translate(${offsetX + d.y},${(d.x + offsetY) * scaleY})`)
|
|
195
|
+
.attr('opacity', 0)
|
|
196
|
+
.style('cursor', (d: any) => (d.data.children?.length ? 'pointer' : 'default'))
|
|
197
|
+
.on('click', (_, d: any) => onNodeClick(d.data));
|
|
198
|
+
|
|
199
|
+
nodeEnter.append('circle').attr('r', r);
|
|
200
|
+
nodeEnter
|
|
201
|
+
.append('text')
|
|
202
|
+
.attr('dy', '0.32em')
|
|
203
|
+
.attr('x', (d: any) => (d.children ? -(r + 4) : r + 4))
|
|
204
|
+
.attr('text-anchor', (d: any) => (d.children ? 'end' : 'start'))
|
|
205
|
+
.text((d: any) => label(d.data));
|
|
206
|
+
|
|
207
|
+
const nodeMerge = nodeEnter.merge(node as any);
|
|
208
|
+
|
|
209
|
+
nodeMerge
|
|
210
|
+
.transition()
|
|
211
|
+
.duration(TRANSITION_MS)
|
|
212
|
+
.attr('opacity', 1)
|
|
213
|
+
.attr('transform', (d: any) => `translate(${offsetX + d.y},${(d.x + offsetY) * scaleY})`);
|
|
214
|
+
|
|
215
|
+
// Circle: filled when collapsed (has hidden children), outlined when expanded or leaf.
|
|
216
|
+
nodeMerge
|
|
217
|
+
.select<SVGCircleElement>('circle')
|
|
218
|
+
.attr('class', (d: any) => {
|
|
219
|
+
const collapsedHere = isCollapsed(d.data, collapsed);
|
|
220
|
+
const leaf = isLeaf(d.data);
|
|
221
|
+
return [slots.node ?? '', collapsedHere ? 'dx-collapsed' : leaf ? 'dx-leaf' : 'dx-branch']
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
.join(' ');
|
|
224
|
+
})
|
|
225
|
+
.attr('r', r);
|
|
226
|
+
|
|
227
|
+
nodeMerge
|
|
228
|
+
.select<SVGTextElement>('text')
|
|
229
|
+
.attr('class', slots.text ?? '')
|
|
230
|
+
.attr('x', (d: any) => (d.children ? -(r + 4) : r + 4))
|
|
231
|
+
.attr('text-anchor', (d: any) => (d.children ? 'end' : 'start'))
|
|
232
|
+
.text((d: any) => label(d.data));
|
|
233
|
+
|
|
234
|
+
node
|
|
235
|
+
.exit()
|
|
236
|
+
.each(function () {
|
|
237
|
+
// Cancel any in-flight transitions so .remove() actually fires.
|
|
238
|
+
select(this).interrupt();
|
|
239
|
+
})
|
|
240
|
+
.transition()
|
|
241
|
+
.duration(TRANSITION_MS)
|
|
242
|
+
.attr('opacity', 0)
|
|
243
|
+
.remove();
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export default TidyTree;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type HierarchyNode, hierarchy as d3Hierarchy } from 'd3';
|
|
6
|
+
|
|
7
|
+
import { type TreeNode } from '../types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build a d3 hierarchy from a TreeNode, pruning children of nodes whose ids are in `collapsed`.
|
|
11
|
+
* Nodes that have children but are collapsed retain their identity in the hierarchy and can be
|
|
12
|
+
* distinguished by `node._children` (the original list).
|
|
13
|
+
*/
|
|
14
|
+
export const buildHierarchy = (data: TreeNode, collapsed: Set<string> = new Set()): HierarchyNode<TreeNode> => {
|
|
15
|
+
return d3Hierarchy<TreeNode>(data, (d) => {
|
|
16
|
+
if (!d.children?.length) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
return collapsed.has(d.id) ? undefined : d.children;
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* True when the node has children that have been hidden via collapse.
|
|
25
|
+
*/
|
|
26
|
+
export const isCollapsed = (data: TreeNode, collapsed: Set<string>): boolean =>
|
|
27
|
+
Boolean(data.children?.length) && collapsed.has(data.id);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* True when the node has no children at all (a real leaf, not a collapsed branch).
|
|
31
|
+
*/
|
|
32
|
+
export const isLeaf = (data: TreeNode): boolean => !data.children?.length;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export
|
|
5
|
+
export * from './HierarchicalEdgeBundling';
|
|
6
|
+
export * from './RadialTree';
|
|
7
|
+
export * from './TidyTree';
|
|
8
|
+
export * from './hierarchy';
|
|
9
|
+
export * from './slots';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
export type TreeLayoutSlots = {
|
|
6
|
+
node?: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
text?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const defaultTreeLayoutSlots: TreeLayoutSlots = {
|
|
12
|
+
// Cursor + transition so the hover swap reads clearly; SVG circles support the `:hover` pseudo-class
|
|
13
|
+
// via Tailwind variants exactly like HTML elements.
|
|
14
|
+
node: 'fill-blue-600 hover:fill-orange-500 cursor-pointer transition-colors',
|
|
15
|
+
// 0.5px is fine on a white background, but on a dark Storybook background the lines disappear.
|
|
16
|
+
// Use stroke-1 with opacity 50% so they read in both themes; dx-bundle-dim/out/in further tune on hover.
|
|
17
|
+
path: 'fill-none stroke-blue-500/50 stroke-[1px] dark:stroke-blue-400/60',
|
|
18
|
+
text: 'fill-neutral-700 dark:fill-neutral-300 text-xs hover:fill-orange-500 cursor-pointer transition-colors',
|
|
19
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useEffect, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Track an element's content-box size via ResizeObserver.
|
|
9
|
+
* Returns the most recently observed `{ width, height }` plus a callback ref to attach to the element.
|
|
10
|
+
*
|
|
11
|
+
* Why not `react-resize-detector` directly: its `targetRef` API doesn't pick up a ref whose
|
|
12
|
+
* `.current` is set later than the hook runs, and the returned-`ref` API forces the consumer to
|
|
13
|
+
* forward a callback ref through their own component — which is awkward for class refs.
|
|
14
|
+
* This hook returns a setter so the consumer assigns it directly to `<div ref={setRef}>`.
|
|
15
|
+
*/
|
|
16
|
+
export const useContainerSize = (): {
|
|
17
|
+
setRef: (el: HTMLDivElement | null) => void;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
} => {
|
|
21
|
+
const [el, setEl] = useState<HTMLDivElement | null>(null);
|
|
22
|
+
const [size, setSize] = useState<{ width: number; height: number }>({ width: 0, height: 0 });
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!el) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const rect = el.getBoundingClientRect();
|
|
29
|
+
setSize({ width: rect.width, height: rect.height });
|
|
30
|
+
const observer = new ResizeObserver((entries) => {
|
|
31
|
+
const entry = entries[0];
|
|
32
|
+
if (!entry) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const { width, height } = entry.contentRect;
|
|
36
|
+
setSize((prev) => (prev.width === width && prev.height === height ? prev : { width, height }));
|
|
37
|
+
});
|
|
38
|
+
observer.observe(el);
|
|
39
|
+
return () => observer.disconnect();
|
|
40
|
+
}, [el]);
|
|
41
|
+
|
|
42
|
+
return { setRef: setEl, width: size.width, height: size.height };
|
|
43
|
+
};
|
|
@@ -8,7 +8,7 @@ import { Obj, Ref } from '@dxos/echo';
|
|
|
8
8
|
import { random } from '@dxos/random';
|
|
9
9
|
import { Task } from '@dxos/types';
|
|
10
10
|
|
|
11
|
-
import { createTree } from '
|
|
11
|
+
import { createTree } from '../../../testing';
|
|
12
12
|
import { type Tree } from './tree';
|
|
13
13
|
|
|
14
14
|
random.seed(0);
|
|
@@ -128,7 +128,7 @@ describe('tree', () => {
|
|
|
128
128
|
|
|
129
129
|
const tree = createTree();
|
|
130
130
|
const node = tree.addNode(tree.root);
|
|
131
|
-
Obj.
|
|
131
|
+
Obj.update(tree.tree, () => {
|
|
132
132
|
node.ref = Ref.make(task);
|
|
133
133
|
});
|
|
134
134
|
});
|
|
@@ -4,39 +4,34 @@
|
|
|
4
4
|
|
|
5
5
|
import * as Schema from 'effect/Schema';
|
|
6
6
|
|
|
7
|
-
import { Key, Obj, Ref, Type } from '@dxos/echo';
|
|
7
|
+
import { DXN, Key, Obj, Ref, Type } from '@dxos/echo';
|
|
8
8
|
import { TestSchema } from '@dxos/echo/testing';
|
|
9
9
|
import { invariant } from '@dxos/invariant';
|
|
10
10
|
|
|
11
11
|
// TODO(burdon): Reconcile with @dxos/graph (i.e., common types).
|
|
12
12
|
|
|
13
13
|
export const TreeNodeType = Schema.Struct({
|
|
14
|
-
id: Key.
|
|
15
|
-
children: Schema.mutable(Schema.Array(Key.
|
|
14
|
+
id: Key.EntityId,
|
|
15
|
+
children: Schema.mutable(Schema.Array(Key.EntityId)),
|
|
16
16
|
data: Schema.mutable(Schema.Record({ key: Schema.String, value: Schema.Any })),
|
|
17
17
|
ref: Schema.optional(Ref.Ref(TestSchema.Expando)),
|
|
18
18
|
}).pipe(Schema.mutable);
|
|
19
19
|
|
|
20
|
-
export
|
|
20
|
+
export type TreeNodeType = Schema.Schema.Type<typeof TreeNodeType>;
|
|
21
21
|
|
|
22
22
|
export const TreeType = Schema.Struct({
|
|
23
|
-
root: Key.
|
|
24
|
-
nodes: Schema.mutable(Schema.Record({ key: Key.
|
|
25
|
-
}).pipe(
|
|
26
|
-
Type.object({
|
|
27
|
-
typename: 'org.dxos.type.tree',
|
|
28
|
-
version: '0.1.0',
|
|
29
|
-
}),
|
|
30
|
-
);
|
|
23
|
+
root: Key.EntityId,
|
|
24
|
+
nodes: Schema.mutable(Schema.Record({ key: Key.EntityId, value: TreeNodeType })),
|
|
25
|
+
}).pipe(Type.makeObject(DXN.make('org.dxos.type.tree', '0.1.0')));
|
|
31
26
|
|
|
32
|
-
export
|
|
27
|
+
export type TreeType = Type.InstanceType<typeof TreeType>;
|
|
33
28
|
|
|
34
29
|
/**
|
|
35
30
|
* Wrapper object for tree.
|
|
36
31
|
*/
|
|
37
32
|
export class Tree {
|
|
38
33
|
static create = (): TreeType => {
|
|
39
|
-
const id = Key.
|
|
34
|
+
const id = Key.EntityId.random();
|
|
40
35
|
return Obj.make(TreeType, {
|
|
41
36
|
root: id,
|
|
42
37
|
nodes: {
|
|
@@ -77,7 +72,7 @@ export class Tree {
|
|
|
77
72
|
*/
|
|
78
73
|
tranverse<T>(
|
|
79
74
|
callback: (node: TreeNodeType, depth: number) => T | void,
|
|
80
|
-
root: Key.
|
|
75
|
+
root: Key.EntityId = this._tree.root,
|
|
81
76
|
depth = 0,
|
|
82
77
|
): T | void {
|
|
83
78
|
const node = this._tree.nodes[root];
|
|
@@ -94,7 +89,7 @@ export class Tree {
|
|
|
94
89
|
}
|
|
95
90
|
}
|
|
96
91
|
|
|
97
|
-
getNode(id: Key.
|
|
92
|
+
getNode(id: Key.EntityId): TreeNodeType {
|
|
98
93
|
const node = this._tree.nodes[id];
|
|
99
94
|
invariant(node);
|
|
100
95
|
return node;
|
|
@@ -185,7 +180,7 @@ export class Tree {
|
|
|
185
180
|
clear(): void {
|
|
186
181
|
const root = this._tree.nodes[this._tree.root];
|
|
187
182
|
root.children.length = 0;
|
|
188
|
-
Obj.
|
|
183
|
+
Obj.update(this._tree, (obj) => {
|
|
189
184
|
obj.nodes = {
|
|
190
185
|
[root.id]: root,
|
|
191
186
|
};
|
|
@@ -197,12 +192,12 @@ export class Tree {
|
|
|
197
192
|
*/
|
|
198
193
|
addNode(parent: TreeNodeType, node?: TreeNodeType, index?: number): TreeNodeType {
|
|
199
194
|
if (!node) {
|
|
200
|
-
const id = Key.
|
|
195
|
+
const id = Key.EntityId.random();
|
|
201
196
|
node = { id, children: [], data: { text: '' } }; // TODO(burdon): Generic.
|
|
202
197
|
}
|
|
203
198
|
|
|
204
199
|
const nodeToAdd = node;
|
|
205
|
-
Obj.
|
|
200
|
+
Obj.update(this._tree, (obj) => {
|
|
206
201
|
obj.nodes[nodeToAdd.id] = nodeToAdd;
|
|
207
202
|
parent.children.splice(index ?? parent.children.length, 0, nodeToAdd.id);
|
|
208
203
|
});
|
|
@@ -212,18 +207,18 @@ export class Tree {
|
|
|
212
207
|
/**
|
|
213
208
|
* Delete node.
|
|
214
209
|
*/
|
|
215
|
-
deleteNode(parent: TreeNodeType, id: Key.
|
|
210
|
+
deleteNode(parent: TreeNodeType, id: Key.EntityId): TreeNodeType | undefined {
|
|
216
211
|
const node = this._tree.nodes[id];
|
|
217
212
|
if (!node) {
|
|
218
213
|
return undefined;
|
|
219
214
|
}
|
|
220
215
|
|
|
221
|
-
Obj.
|
|
216
|
+
Obj.update(this._tree, (obj) => {
|
|
222
217
|
delete obj.nodes[node.id];
|
|
223
218
|
});
|
|
224
219
|
const idx = parent.children.findIndex((child) => child === id);
|
|
225
220
|
if (idx !== -1) {
|
|
226
|
-
Obj.
|
|
221
|
+
Obj.update(this._tree, () => {
|
|
227
222
|
parent.children.splice(idx, 1);
|
|
228
223
|
});
|
|
229
224
|
}
|
|
@@ -242,7 +237,7 @@ export class Tree {
|
|
|
242
237
|
}
|
|
243
238
|
|
|
244
239
|
const child = node.children[from];
|
|
245
|
-
Obj.
|
|
240
|
+
Obj.update(this._tree, () => {
|
|
246
241
|
node.children.splice(from, 1);
|
|
247
242
|
node.children.splice(to, 0, child);
|
|
248
243
|
});
|
|
@@ -264,7 +259,7 @@ export class Tree {
|
|
|
264
259
|
}
|
|
265
260
|
|
|
266
261
|
const previous = this.getNode(parent.children[idx - 1]);
|
|
267
|
-
Obj.
|
|
262
|
+
Obj.update(this._tree, () => {
|
|
268
263
|
parent.children.splice(idx, 1);
|
|
269
264
|
previous.children.push(node.id);
|
|
270
265
|
});
|
|
@@ -286,20 +281,20 @@ export class Tree {
|
|
|
286
281
|
|
|
287
282
|
// Remove node from parent and get following siblings.
|
|
288
283
|
const nodeIdx = parent.children.findIndex((id) => id === node.id);
|
|
289
|
-
let rest: Key.
|
|
290
|
-
Obj.
|
|
284
|
+
let rest: Key.EntityId[] = [];
|
|
285
|
+
Obj.update(this._tree, () => {
|
|
291
286
|
const removed = parent.children.splice(nodeIdx, parent.children.length - nodeIdx);
|
|
292
287
|
rest = removed.slice(1); // Skip the node itself.
|
|
293
288
|
});
|
|
294
289
|
|
|
295
290
|
// Add to ancestor.
|
|
296
291
|
const parentIdx = this.getChildNodes(ancestor).findIndex((n) => n.id === parent.id);
|
|
297
|
-
Obj.
|
|
292
|
+
Obj.update(this._tree, () => {
|
|
298
293
|
ancestor.children.splice(parentIdx + 1, 0, node.id);
|
|
299
294
|
});
|
|
300
295
|
|
|
301
296
|
// Transplant following siblings to current node.
|
|
302
|
-
Obj.
|
|
297
|
+
Obj.update(this._tree, () => {
|
|
303
298
|
node.children.push(...rest);
|
|
304
299
|
});
|
|
305
300
|
}
|
|
@@ -2,40 +2,49 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type
|
|
5
|
+
import { type Key } from '@dxos/echo';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import { type TreeType } from './tree';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* In-memory tree shape used by the d3 layouts.
|
|
11
|
+
* `data` carries through to layout callbacks (e.g. hover/inspect) — typically an ECHO object on leaves.
|
|
12
|
+
*/
|
|
13
|
+
export type TreeNode<TData = unknown> = {
|
|
8
14
|
id: string;
|
|
9
15
|
label?: string;
|
|
10
|
-
|
|
16
|
+
data?: TData;
|
|
17
|
+
children?: TreeNode<TData>[];
|
|
11
18
|
};
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Convert an ECHO `TreeType` (id-keyed node map) into a nested `TreeNode` hierarchy.
|
|
22
|
+
* Returns `undefined` if the root id is missing — the tree is then incomplete and shouldn't render.
|
|
23
|
+
*/
|
|
24
|
+
export const treeTypeToTreeNode = (
|
|
25
|
+
tree: TreeType,
|
|
26
|
+
rootId: Key.EntityId = tree.root,
|
|
27
|
+
visited: Set<string> = new Set(),
|
|
28
|
+
): TreeNode | undefined => {
|
|
29
|
+
const node = tree.nodes[rootId];
|
|
30
|
+
if (!node) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (visited.has(rootId)) {
|
|
34
|
+
return { id: rootId, label: labelOf(node), data: node.data };
|
|
35
|
+
}
|
|
36
|
+
visited.add(rootId);
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// }
|
|
38
|
-
// }
|
|
38
|
+
return {
|
|
39
|
+
id: rootId,
|
|
40
|
+
label: labelOf(node),
|
|
41
|
+
data: node.data,
|
|
42
|
+
children: node.children
|
|
43
|
+
.map((childId) => treeTypeToTreeNode(tree, childId, visited))
|
|
44
|
+
.filter((c): c is TreeNode => Boolean(c)),
|
|
45
|
+
};
|
|
46
|
+
};
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
const labelOf = (node: { data: Record<string, any> }): string | undefined => {
|
|
49
|
+
return typeof node.data?.text === 'string' ? node.data.text : undefined;
|
|
41
50
|
};
|