@canmingir/link 1.2.8 → 1.2.11
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/.github/workflows/publish.yml +64 -7
- package/package.json +1 -1
- package/src/lib/Flow/connectors/DynamicConnector.jsx +247 -0
- package/src/lib/Flow/core/Flow.jsx +79 -0
- package/src/lib/Flow/core/FlowNode.jsx +68 -0
- package/src/lib/Flow/core/FlowViewport.jsx +259 -0
- package/src/lib/Flow/graph/FloatingGraph.jsx +44 -0
- package/src/lib/Flow/hooks/useGraphOperations.js +362 -0
- package/src/lib/Flow/hooks/useNodeStyle.js +56 -0
- package/src/lib/Flow/index.js +1 -1
- package/src/lib/Flow/layouts/InfoNode.jsx +115 -56
- package/src/lib/Flow/nodes/DefaultCard.jsx +107 -0
- package/src/lib/Flow/nodes/DraggableNode.jsx +162 -0
- package/src/lib/Flow/nodes/FlowNodeView.jsx +214 -0
- package/src/lib/Flow/selection/SelectionContext.jsx +259 -0
- package/src/lib/Flow/selection/SelectionOverlay.jsx +31 -0
- package/src/lib/Flow/styles.js +59 -19
- package/src/lib/Flow/utils/flowUtils.js +268 -0
- package/src/lib/index.js +1 -1
- package/.idea/codeStyles/Project.xml +0 -84
- package/.idea/codeStyles/codeStyleConfig.xml +0 -5
- package/.idea/copilot.data.migration.agent.xml +0 -6
- package/.idea/copilot.data.migration.ask.xml +0 -6
- package/.idea/copilot.data.migration.ask2agent.xml +0 -6
- package/.idea/copilot.data.migration.edit.xml +0 -6
- package/.idea/inspectionProfiles/Project_Default.xml +0 -6
- package/.idea/misc.xml +0 -5
- package/.idea/modules.xml +0 -8
- package/.idea/platform.iml +0 -9
- package/.idea/vcs.xml +0 -6
- package/src/lib/Flow/DraggableNode.jsx +0 -62
- package/src/lib/Flow/DynamicConnector.jsx +0 -176
- package/src/lib/Flow/Flow.jsx +0 -40
- package/src/lib/Flow/FlowNode.jsx +0 -371
- package/src/lib/Flow/flowUtils.js +0 -111
package/src/lib/Flow/Flow.jsx
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import FlowNode from "./FlowNode";
|
|
2
|
-
|
|
3
|
-
import React, { useMemo } from "react";
|
|
4
|
-
import { assertLinkedGraph, buildTreeFromLinked } from "./flowUtils";
|
|
5
|
-
|
|
6
|
-
export const Flow = ({ data, variant = "simple", style, plugin }) => {
|
|
7
|
-
const { nodesById, roots } = useMemo(() => assertLinkedGraph(data), [data]);
|
|
8
|
-
|
|
9
|
-
const treeData = useMemo(() => {
|
|
10
|
-
if (!roots?.length)
|
|
11
|
-
return { id: "__empty__", label: "(empty)", children: [] };
|
|
12
|
-
|
|
13
|
-
if (roots.length === 1) {
|
|
14
|
-
return (
|
|
15
|
-
buildTreeFromLinked(roots[0], nodesById) || {
|
|
16
|
-
id: roots[0],
|
|
17
|
-
children: [],
|
|
18
|
-
}
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const children = roots
|
|
23
|
-
.map((r) => buildTreeFromLinked(r, nodesById))
|
|
24
|
-
.filter(Boolean);
|
|
25
|
-
|
|
26
|
-
return { id: "__root__", label: "Root", children };
|
|
27
|
-
}, [nodesById, roots]);
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<FlowNode
|
|
31
|
-
node={treeData}
|
|
32
|
-
variant={variant}
|
|
33
|
-
style={style}
|
|
34
|
-
plugin={plugin}
|
|
35
|
-
isRoot={true}
|
|
36
|
-
/>
|
|
37
|
-
);
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export default Flow;
|
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
import DraggableNode from "./DraggableNode";
|
|
2
|
-
import DynamicConnector from "./DynamicConnector";
|
|
3
|
-
import { getContentParts } from "./flowUtils";
|
|
4
|
-
|
|
5
|
-
import { Box, Card, Typography } from "@mui/material";
|
|
6
|
-
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
7
|
-
import {
|
|
8
|
-
applySemanticTokens,
|
|
9
|
-
getBaseStyleForVariant,
|
|
10
|
-
getDecisionNodeStyle,
|
|
11
|
-
toPxNumber,
|
|
12
|
-
} from "./styles";
|
|
13
|
-
|
|
14
|
-
const NodeContent = ({
|
|
15
|
-
node,
|
|
16
|
-
type,
|
|
17
|
-
variant,
|
|
18
|
-
style,
|
|
19
|
-
plugin,
|
|
20
|
-
registerRef,
|
|
21
|
-
onDrag,
|
|
22
|
-
}) => {
|
|
23
|
-
const baseStyle = getBaseStyleForVariant(variant);
|
|
24
|
-
const hasChildren = Array.isArray(node.children) && node.children.length > 0;
|
|
25
|
-
|
|
26
|
-
const variantTokens =
|
|
27
|
-
variant === "decision" ? getDecisionNodeStyle(node.type) : {};
|
|
28
|
-
|
|
29
|
-
let styleTokens = {};
|
|
30
|
-
if (typeof style === "function") {
|
|
31
|
-
styleTokens = style(node) || {};
|
|
32
|
-
} else if (style && typeof style === "object") {
|
|
33
|
-
styleTokens = style;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
let _plugin = null;
|
|
37
|
-
if (plugin) {
|
|
38
|
-
if (typeof plugin === "function") {
|
|
39
|
-
_plugin = plugin(type, node) || null;
|
|
40
|
-
} else if (typeof plugin === "object") {
|
|
41
|
-
_plugin = plugin;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let pluginTokens = {};
|
|
46
|
-
if (_plugin && typeof _plugin.style === "function") {
|
|
47
|
-
pluginTokens =
|
|
48
|
-
_plugin.style({
|
|
49
|
-
node,
|
|
50
|
-
style: styleTokens,
|
|
51
|
-
}) || {};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const rawNodeStyle = {
|
|
55
|
-
...baseStyle,
|
|
56
|
-
...variantTokens,
|
|
57
|
-
...styleTokens,
|
|
58
|
-
...pluginTokens,
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const nodeStyle = applySemanticTokens(rawNodeStyle, baseStyle);
|
|
62
|
-
|
|
63
|
-
const {
|
|
64
|
-
lineColor = baseStyle.lineColor,
|
|
65
|
-
lineWidth = baseStyle.lineWidth,
|
|
66
|
-
lineStyle = baseStyle.lineStyle,
|
|
67
|
-
gap = baseStyle.gap,
|
|
68
|
-
levelGap = baseStyle.levelGap ?? 2.5,
|
|
69
|
-
nodeSx = {},
|
|
70
|
-
borderWidth,
|
|
71
|
-
borderColor = baseStyle.borderColor,
|
|
72
|
-
cardWidth,
|
|
73
|
-
shape,
|
|
74
|
-
shadowLevel,
|
|
75
|
-
minHeight,
|
|
76
|
-
connectorType = baseStyle.connectorType ?? "default",
|
|
77
|
-
} = nodeStyle;
|
|
78
|
-
|
|
79
|
-
const strokeWidth = toPxNumber(lineWidth, 1.5);
|
|
80
|
-
const dashStyle =
|
|
81
|
-
lineStyle === "dashed" || lineStyle === "dotted" ? lineStyle : "solid";
|
|
82
|
-
|
|
83
|
-
const containerRef = useRef(null);
|
|
84
|
-
const parentRef = useRef(null);
|
|
85
|
-
const childRefs = useRef({});
|
|
86
|
-
const [childElList, setChildElList] = useState([]);
|
|
87
|
-
|
|
88
|
-
const [connectorTick, setConnectorTick] = useState(0);
|
|
89
|
-
|
|
90
|
-
const handleDrag = (newOffset) => {
|
|
91
|
-
setConnectorTick((t) => t + 1);
|
|
92
|
-
if (onDrag) onDrag(newOffset);
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
useLayoutEffect(() => {
|
|
96
|
-
const els = (node.children || [])
|
|
97
|
-
.map((c) => childRefs.current[c.id])
|
|
98
|
-
.filter(Boolean);
|
|
99
|
-
setChildElList(els);
|
|
100
|
-
}, [node.children]);
|
|
101
|
-
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
const t = setTimeout(() => {
|
|
104
|
-
const els = (node.children || [])
|
|
105
|
-
.map((c) => childRefs.current[c.id])
|
|
106
|
-
.filter(Boolean);
|
|
107
|
-
setChildElList(els);
|
|
108
|
-
}, 0);
|
|
109
|
-
return () => clearTimeout(t);
|
|
110
|
-
}, [node.children]);
|
|
111
|
-
|
|
112
|
-
const { title, subtitle, metaEntries } = getContentParts(node);
|
|
113
|
-
|
|
114
|
-
const renderDefaultCard = () => {
|
|
115
|
-
const effectiveWidth = cardWidth || 220;
|
|
116
|
-
const effectiveBorderWidth = borderWidth || 1;
|
|
117
|
-
const effectiveRadius =
|
|
118
|
-
typeof shape === "number" ? shape : baseStyle.shape || 4;
|
|
119
|
-
const effectiveShadow =
|
|
120
|
-
typeof shadowLevel === "number"
|
|
121
|
-
? shadowLevel
|
|
122
|
-
: variant === "card"
|
|
123
|
-
? 2
|
|
124
|
-
: 1;
|
|
125
|
-
const effectiveMinHeight = minHeight || 80;
|
|
126
|
-
|
|
127
|
-
return (
|
|
128
|
-
<Card
|
|
129
|
-
sx={{
|
|
130
|
-
p: 2,
|
|
131
|
-
width: effectiveWidth,
|
|
132
|
-
minHeight: effectiveMinHeight,
|
|
133
|
-
display: "flex",
|
|
134
|
-
flexDirection: "row",
|
|
135
|
-
alignItems: "center",
|
|
136
|
-
justifyContent: "center",
|
|
137
|
-
gap: 1,
|
|
138
|
-
position: "relative",
|
|
139
|
-
borderRadius: effectiveRadius,
|
|
140
|
-
bgcolor: nodeStyle.bg || "background.paper",
|
|
141
|
-
border: `${effectiveBorderWidth}px solid ${
|
|
142
|
-
borderColor || "transparent"
|
|
143
|
-
}`,
|
|
144
|
-
boxShadow: effectiveShadow,
|
|
145
|
-
transition: "background-color 0.3s ease, box-shadow 0.3s ease",
|
|
146
|
-
"&:hover": {
|
|
147
|
-
bgcolor: nodeStyle.hoverBg || nodeStyle.bg || "grey.100",
|
|
148
|
-
boxShadow: effectiveShadow + 1,
|
|
149
|
-
cursor: "pointer",
|
|
150
|
-
},
|
|
151
|
-
...nodeSx,
|
|
152
|
-
}}
|
|
153
|
-
>
|
|
154
|
-
<Box sx={{ textAlign: "left", width: "100%" }}>
|
|
155
|
-
<Typography
|
|
156
|
-
variant="subtitle2"
|
|
157
|
-
sx={{
|
|
158
|
-
textAlign: "center",
|
|
159
|
-
fontWeight: 600,
|
|
160
|
-
fontSize: 13,
|
|
161
|
-
mb: subtitle ? 0.5 : 0,
|
|
162
|
-
}}
|
|
163
|
-
>
|
|
164
|
-
{title}
|
|
165
|
-
</Typography>
|
|
166
|
-
|
|
167
|
-
{subtitle && (
|
|
168
|
-
<Typography
|
|
169
|
-
variant="body2"
|
|
170
|
-
color="text.secondary"
|
|
171
|
-
sx={{
|
|
172
|
-
textAlign: "center",
|
|
173
|
-
fontSize: 11,
|
|
174
|
-
mb: metaEntries.length ? 0.5 : 0,
|
|
175
|
-
}}
|
|
176
|
-
>
|
|
177
|
-
{subtitle}
|
|
178
|
-
</Typography>
|
|
179
|
-
)}
|
|
180
|
-
|
|
181
|
-
{metaEntries.length > 0 && (
|
|
182
|
-
<Box sx={{ mt: 0.25 }}>
|
|
183
|
-
{metaEntries.map(([key, value]) => (
|
|
184
|
-
<Typography
|
|
185
|
-
key={key}
|
|
186
|
-
variant="caption"
|
|
187
|
-
color="text.secondary"
|
|
188
|
-
sx={{
|
|
189
|
-
textAlign: "center",
|
|
190
|
-
display: "block",
|
|
191
|
-
fontSize: 10,
|
|
192
|
-
}}
|
|
193
|
-
>
|
|
194
|
-
{key}: {String(value)}
|
|
195
|
-
</Typography>
|
|
196
|
-
))}
|
|
197
|
-
</Box>
|
|
198
|
-
)}
|
|
199
|
-
</Box>
|
|
200
|
-
</Card>
|
|
201
|
-
);
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
const renderContent = () => {
|
|
205
|
-
if (_plugin && typeof _plugin.node === "function") {
|
|
206
|
-
return _plugin.node({
|
|
207
|
-
node,
|
|
208
|
-
title,
|
|
209
|
-
subtitle,
|
|
210
|
-
metaEntries,
|
|
211
|
-
nodeStyle,
|
|
212
|
-
baseStyle,
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
return renderDefaultCard();
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
return (
|
|
219
|
-
<Box
|
|
220
|
-
ref={containerRef}
|
|
221
|
-
sx={{
|
|
222
|
-
display: "inline-flex",
|
|
223
|
-
flexDirection: "column",
|
|
224
|
-
alignItems: "center",
|
|
225
|
-
position: "relative",
|
|
226
|
-
}}
|
|
227
|
-
>
|
|
228
|
-
<DraggableNode
|
|
229
|
-
registerRef={(el) => {
|
|
230
|
-
parentRef.current = el;
|
|
231
|
-
if (registerRef) registerRef(el);
|
|
232
|
-
}}
|
|
233
|
-
onDrag={handleDrag}
|
|
234
|
-
>
|
|
235
|
-
{renderContent()}
|
|
236
|
-
</DraggableNode>
|
|
237
|
-
|
|
238
|
-
{hasChildren && (
|
|
239
|
-
<>
|
|
240
|
-
<DynamicConnector
|
|
241
|
-
containerEl={containerRef.current}
|
|
242
|
-
parentEl={parentRef.current}
|
|
243
|
-
childEls={childElList}
|
|
244
|
-
stroke={lineColor}
|
|
245
|
-
strokeWidth={strokeWidth}
|
|
246
|
-
lineStyle={dashStyle}
|
|
247
|
-
connectorType={connectorType}
|
|
248
|
-
tick={connectorTick}
|
|
249
|
-
/>
|
|
250
|
-
|
|
251
|
-
<Box
|
|
252
|
-
sx={{
|
|
253
|
-
display: "flex",
|
|
254
|
-
flexDirection: "row",
|
|
255
|
-
columnGap: gap,
|
|
256
|
-
marginTop: levelGap,
|
|
257
|
-
position: "relative",
|
|
258
|
-
alignItems: "flex-start",
|
|
259
|
-
justifyContent: "center",
|
|
260
|
-
}}
|
|
261
|
-
>
|
|
262
|
-
{node.children.map((child) => (
|
|
263
|
-
<FlowNode
|
|
264
|
-
key={child.id}
|
|
265
|
-
node={child}
|
|
266
|
-
type={type}
|
|
267
|
-
variant={variant}
|
|
268
|
-
style={style}
|
|
269
|
-
plugin={plugin}
|
|
270
|
-
registerRef={(el) => (childRefs.current[child.id] = el)}
|
|
271
|
-
onDrag={() => setConnectorTick((t) => t + 1)}
|
|
272
|
-
isRoot={false}
|
|
273
|
-
/>
|
|
274
|
-
))}
|
|
275
|
-
</Box>
|
|
276
|
-
</>
|
|
277
|
-
)}
|
|
278
|
-
</Box>
|
|
279
|
-
);
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
const FlowNode = ({ isRoot = false, ...props }) => {
|
|
283
|
-
if (!isRoot) {
|
|
284
|
-
return <NodeContent {...props} />;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
|
288
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
289
|
-
|
|
290
|
-
const [zoom, setZoom] = useState(1);
|
|
291
|
-
|
|
292
|
-
const clampZoom = (z) => Math.min(2.5, Math.max(0.25, z));
|
|
293
|
-
|
|
294
|
-
useEffect(() => {
|
|
295
|
-
const onWheel = (e) => {
|
|
296
|
-
const wantsZoom = e.ctrlKey || e.metaKey;
|
|
297
|
-
if (!wantsZoom) return;
|
|
298
|
-
|
|
299
|
-
e.preventDefault();
|
|
300
|
-
|
|
301
|
-
const direction = e.deltaY > 0 ? -1 : 1;
|
|
302
|
-
const factor = direction > 0 ? 1.1 : 1 / 1.1;
|
|
303
|
-
|
|
304
|
-
setZoom((z) => clampZoom(z * factor));
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
window.addEventListener("wheel", onWheel, { passive: false });
|
|
308
|
-
return () => window.removeEventListener("wheel", onWheel);
|
|
309
|
-
}, []);
|
|
310
|
-
|
|
311
|
-
const handleCanvasMouseDown = (e) => {
|
|
312
|
-
if (e.target?.closest?.('[data-flow-zoom="true"]')) return;
|
|
313
|
-
|
|
314
|
-
if (e.button !== 0) return;
|
|
315
|
-
|
|
316
|
-
setIsDragging(true);
|
|
317
|
-
|
|
318
|
-
const startX = e.clientX;
|
|
319
|
-
const startY = e.clientY;
|
|
320
|
-
const startOffset = { ...offset };
|
|
321
|
-
|
|
322
|
-
const onMove = (ev) => {
|
|
323
|
-
setOffset({
|
|
324
|
-
x: startOffset.x + (ev.clientX - startX),
|
|
325
|
-
y: startOffset.y + (ev.clientY - startY),
|
|
326
|
-
});
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
const onUp = () => {
|
|
330
|
-
setIsDragging(false);
|
|
331
|
-
window.removeEventListener("mousemove", onMove);
|
|
332
|
-
window.removeEventListener("mouseup", onUp);
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
window.addEventListener("mousemove", onMove);
|
|
336
|
-
window.addEventListener("mouseup", onUp);
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
return (
|
|
340
|
-
<Box
|
|
341
|
-
onMouseDown={handleCanvasMouseDown}
|
|
342
|
-
sx={{
|
|
343
|
-
width: "100vw",
|
|
344
|
-
height: "100vh",
|
|
345
|
-
overflow: "hidden",
|
|
346
|
-
bgcolor: "none",
|
|
347
|
-
cursor: isDragging ? "grabbing" : "default",
|
|
348
|
-
userSelect: "none",
|
|
349
|
-
position: "relative",
|
|
350
|
-
}}
|
|
351
|
-
>
|
|
352
|
-
<Box
|
|
353
|
-
sx={{
|
|
354
|
-
transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom})`,
|
|
355
|
-
transformOrigin: "center center",
|
|
356
|
-
width: "100%",
|
|
357
|
-
height: "100%",
|
|
358
|
-
display: "flex",
|
|
359
|
-
alignItems: "center",
|
|
360
|
-
justifyContent: "center",
|
|
361
|
-
transition: isDragging ? "none" : "transform 0.1s ease-out",
|
|
362
|
-
pointerEvents: "auto",
|
|
363
|
-
}}
|
|
364
|
-
>
|
|
365
|
-
<NodeContent {...props} />
|
|
366
|
-
</Box>
|
|
367
|
-
</Box>
|
|
368
|
-
);
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
export default FlowNode;
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
export function assertLinkedGraph(data) {
|
|
2
|
-
if (!data || typeof data !== "object") {
|
|
3
|
-
throw new Error(
|
|
4
|
-
"FlowChart expected a linked graph object: { nodes, roots? }."
|
|
5
|
-
);
|
|
6
|
-
}
|
|
7
|
-
const { nodes, roots } = data;
|
|
8
|
-
if (!nodes || typeof nodes !== "object") {
|
|
9
|
-
throw new Error("FlowChart expected data.nodes to be an object.");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
let useRoots = Array.isArray(roots) ? [...roots] : null;
|
|
13
|
-
if (!useRoots || useRoots.length === 0) {
|
|
14
|
-
useRoots = Object.keys(nodes).filter((id) => !nodes[id]?.previous);
|
|
15
|
-
}
|
|
16
|
-
if (useRoots.length === 0) {
|
|
17
|
-
const first = Object.keys(nodes)[0];
|
|
18
|
-
if (first) useRoots = [first];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
for (const r of useRoots) {
|
|
22
|
-
if (!nodes[r]) throw new Error(`Root id "${r}" not found in data.nodes.`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return { nodesById: nodes, roots: useRoots };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function buildTreeFromLinked(rootId, nodesById) {
|
|
29
|
-
if (!rootId || !nodesById?.[rootId]) return null;
|
|
30
|
-
const seen = new Set();
|
|
31
|
-
|
|
32
|
-
const cloneNode = (node) => {
|
|
33
|
-
if (!node) return null;
|
|
34
|
-
const { next, previous, children, ...rest } = node;
|
|
35
|
-
return { ...rest, id: node.id, previous, next, children: [] };
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const dfs = (id) => {
|
|
39
|
-
if (!id || seen.has(id) || !nodesById[id]) return null;
|
|
40
|
-
seen.add(id);
|
|
41
|
-
|
|
42
|
-
const node = nodesById[id];
|
|
43
|
-
const out = cloneNode(node);
|
|
44
|
-
|
|
45
|
-
const nextArr = Array.isArray(node.next)
|
|
46
|
-
? node.next
|
|
47
|
-
: node.next != null
|
|
48
|
-
? [node.next]
|
|
49
|
-
: [];
|
|
50
|
-
|
|
51
|
-
for (const nxt of nextArr) {
|
|
52
|
-
const nextId = typeof nxt === "string" ? nxt : nxt?.id;
|
|
53
|
-
if (!nextId || !nodesById[nextId]) continue;
|
|
54
|
-
|
|
55
|
-
const target = nodesById[nextId];
|
|
56
|
-
if (target.previous == null || target.previous === id) {
|
|
57
|
-
const built = dfs(nextId);
|
|
58
|
-
if (built) out.children.push(built);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return out;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
return dfs(rootId);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export const getContentParts = (n) => {
|
|
68
|
-
const entries = Object.entries(n).filter(
|
|
69
|
-
([key]) =>
|
|
70
|
-
key !== "children" && key !== "id" && key !== "previous" && key !== "next"
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
if (entries.length === 0) {
|
|
74
|
-
return {
|
|
75
|
-
title: "(empty)",
|
|
76
|
-
subtitle: null,
|
|
77
|
-
metaEntries: [],
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const preferredTitleKeys = ["label", "title", "name"];
|
|
82
|
-
const titleEntry =
|
|
83
|
-
entries.find(([key]) => preferredTitleKeys.includes(key)) || entries[0];
|
|
84
|
-
const [titleKey, rawTitle] = titleEntry;
|
|
85
|
-
const title = String(rawTitle);
|
|
86
|
-
let remaining = entries.filter(([key]) => key !== titleKey);
|
|
87
|
-
|
|
88
|
-
const preferredSubtitleKeys = ["description", "role", "type", "status"];
|
|
89
|
-
const subtitleEntry =
|
|
90
|
-
remaining.find(([key]) => preferredSubtitleKeys.includes(key)) || null;
|
|
91
|
-
|
|
92
|
-
let subtitle = null;
|
|
93
|
-
if (subtitleEntry) {
|
|
94
|
-
const [subtitleKey, raw] = subtitleEntry;
|
|
95
|
-
subtitle = String(raw);
|
|
96
|
-
remaining = remaining.filter(([key]) => key !== subtitleKey);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const metaEntries = remaining
|
|
100
|
-
.filter(([, value]) => {
|
|
101
|
-
const t = typeof value;
|
|
102
|
-
return (
|
|
103
|
-
(t === "string" || t === "number" || t === "boolean") &&
|
|
104
|
-
value !== "" &&
|
|
105
|
-
value !== null
|
|
106
|
-
);
|
|
107
|
-
})
|
|
108
|
-
.map(([k, v]) => [k, v]);
|
|
109
|
-
|
|
110
|
-
return { title, subtitle, metaEntries };
|
|
111
|
-
};
|