@truedat/core 8.11.6 → 8.11.7
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/package.json +4 -3
- package/src/components/CardGroupsAccordion.js +0 -2
- package/src/components/Graph.js +1410 -158
- package/src/components/__tests__/ColoredEdge.spec.js +372 -0
- package/src/components/__tests__/ConceptNode.spec.js +181 -0
- package/src/components/__tests__/Graph.spec.js +183 -12
- package/src/components/__tests__/LineageEdgeRendering.spec.js +45 -0
- package/src/components/__tests__/LineageGroupNode.spec.js +308 -0
- package/src/components/__tests__/__snapshots__/ConceptNode.spec.js.snap +23 -0
- package/src/components/graph/ColoredEdge.js +715 -79
- package/src/components/graph/ConceptNode.js +21 -4
- package/src/components/graph/FlowPorts.js +1 -0
- package/src/components/graph/LineageGroupNode.js +320 -0
- package/src/components/graph/LineageQueryOrigin.js +27 -0
- package/src/components/graph/edgeLayout.js +7 -4
- package/src/components/graph/lineageNodeIcon.js +17 -0
- package/src/components/graph/lineageQueryOriginMessages.js +10 -0
- package/src/styles/ColoredEdge.less +3 -0
- package/src/styles/CommonGraphs.less +76 -0
- package/src/styles/ConceptGraph.less +6 -0
- package/src/styles/LineageGroupNode.less +563 -0
- package/src/styles/graph.less +20 -25
- package/src/styles/lineageNodeStates.less +111 -0
- /package/src/{components/graph → styles}/ConceptNode.less +0 -0
package/src/components/Graph.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { useIntl } from "react-intl";
|
|
3
3
|
import PropTypes from "prop-types";
|
|
4
|
-
import { Modal, Popup } from "semantic-ui-react";
|
|
4
|
+
import { Icon, Modal, Popup } from "semantic-ui-react";
|
|
5
5
|
import {
|
|
6
6
|
ReactFlow,
|
|
7
7
|
ReactFlowProvider,
|
|
8
8
|
useNodesState,
|
|
9
9
|
useEdgesState,
|
|
10
10
|
useReactFlow,
|
|
11
|
-
getNodesBounds,
|
|
11
|
+
getNodesBounds as getStaticNodesBounds,
|
|
12
12
|
getViewportForBounds,
|
|
13
13
|
Position,
|
|
14
14
|
MarkerType,
|
|
@@ -16,29 +16,118 @@ import {
|
|
|
16
16
|
ControlButton,
|
|
17
17
|
Background,
|
|
18
18
|
BackgroundVariant,
|
|
19
|
+
MiniMap,
|
|
19
20
|
} from "@xyflow/react";
|
|
21
|
+
import {
|
|
22
|
+
Expand,
|
|
23
|
+
Minimize2,
|
|
24
|
+
ZoomIn,
|
|
25
|
+
ZoomOut,
|
|
26
|
+
Maximize,
|
|
27
|
+
Focus,
|
|
28
|
+
} from "lucide-react";
|
|
20
29
|
import ELK from "elkjs/lib/elk.bundled.js";
|
|
21
30
|
import ConceptNode from "./graph/ConceptNode";
|
|
22
31
|
import ColoredEdge from "./graph/ColoredEdge";
|
|
32
|
+
import LineageGroupNode from "./graph/LineageGroupNode";
|
|
33
|
+
import { getTargetSlotOffset } from "./graph/edgeLayout";
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const nodeTypes = { concept: ConceptNode };
|
|
35
|
+
const baseNodeTypes = { concept: ConceptNode, group: LineageGroupNode };
|
|
27
36
|
const edgeTypes = { colored: ColoredEdge };
|
|
28
37
|
|
|
29
38
|
const elk = new ELK();
|
|
30
39
|
const DEFAULT_NODE_WIDTH = 130;
|
|
31
40
|
const DEFAULT_NODE_HEIGHT = 44;
|
|
32
|
-
const
|
|
41
|
+
const TRANSLATE_EXTENT_MIN_PADDING = 8000;
|
|
42
|
+
const TRANSLATE_EXTENT_SIZE_MULTIPLIER = 4;
|
|
43
|
+
const DEFAULT_GRAPH_HEIGHT = "640px";
|
|
33
44
|
const EXPANDED_GRAPH_HEIGHT = "82vh";
|
|
34
|
-
const DEFAULT_MIN_ZOOM = 0.
|
|
35
|
-
const DEFAULT_MAX_ZOOM =
|
|
36
|
-
const FIT_VIEW_PADDING = 0.
|
|
45
|
+
const DEFAULT_MIN_ZOOM = 0.03;
|
|
46
|
+
const DEFAULT_MAX_ZOOM = 2;
|
|
47
|
+
const FIT_VIEW_PADDING = 0.15;
|
|
48
|
+
const FOCUS_VIEW_PADDING = 0.28;
|
|
49
|
+
const FOCUS_MAX_ZOOM = 1.1;
|
|
50
|
+
const DEFAULT_TRANSLATE_EXTENT = [
|
|
51
|
+
[-800, -600],
|
|
52
|
+
[800, 600],
|
|
53
|
+
];
|
|
37
54
|
const CONTROL_TOOLTIP_DELAY = 120;
|
|
38
55
|
const DEFAULT_EDGE_STROKE = "var(--td-graph-edge-stroke, #b0b8c8)";
|
|
56
|
+
const MIN_NODE_GAP = 12;
|
|
57
|
+
const SPACE_KEY = " ";
|
|
58
|
+
const SPACE_CODE = "Space";
|
|
59
|
+
const SPACEBAR_KEY = "Spacebar";
|
|
60
|
+
const WHEEL_DELTA_LINE_HEIGHT = 16;
|
|
61
|
+
const MAX_WHEEL_DELTA_PER_FRAME = 80;
|
|
62
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
63
|
+
|
|
64
|
+
const isSpaceKeyEvent = (event) =>
|
|
65
|
+
event?.key === SPACE_KEY ||
|
|
66
|
+
event?.key === SPACE_CODE ||
|
|
67
|
+
event?.code === SPACE_CODE ||
|
|
68
|
+
event?.key === SPACEBAR_KEY;
|
|
69
|
+
|
|
70
|
+
const isEditableTarget = (target) => {
|
|
71
|
+
if (!target || typeof target.closest !== "function") return false;
|
|
72
|
+
|
|
73
|
+
return Boolean(
|
|
74
|
+
target.closest(
|
|
75
|
+
'input, textarea, select, button, [contenteditable="true"], [role="textbox"]',
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const normalizeWheelDelta = (
|
|
81
|
+
deltaY,
|
|
82
|
+
deltaMode = 0,
|
|
83
|
+
pageHeight = 800,
|
|
84
|
+
) => {
|
|
85
|
+
if (!Number.isFinite(deltaY)) return 0;
|
|
86
|
+
|
|
87
|
+
if (deltaMode === 1) return deltaY * WHEEL_DELTA_LINE_HEIGHT;
|
|
88
|
+
if (deltaMode === 2) return deltaY * pageHeight;
|
|
89
|
+
|
|
90
|
+
return deltaY;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const viewportForWheelZoom = ({
|
|
94
|
+
viewport,
|
|
95
|
+
pointer,
|
|
96
|
+
delta,
|
|
97
|
+
sensitivity,
|
|
98
|
+
minZoom,
|
|
99
|
+
maxZoom,
|
|
100
|
+
}) => {
|
|
101
|
+
const currentZoom = viewport?.zoom || 1;
|
|
102
|
+
const clampedDelta = clamp(
|
|
103
|
+
delta,
|
|
104
|
+
-MAX_WHEEL_DELTA_PER_FRAME,
|
|
105
|
+
MAX_WHEEL_DELTA_PER_FRAME,
|
|
106
|
+
);
|
|
107
|
+
const nextZoom = clamp(
|
|
108
|
+
currentZoom * Math.exp(-clampedDelta * sensitivity),
|
|
109
|
+
minZoom,
|
|
110
|
+
maxZoom,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (nextZoom === currentZoom) return viewport;
|
|
114
|
+
|
|
115
|
+
const flowX = (pointer.x - viewport.x) / currentZoom;
|
|
116
|
+
const flowY = (pointer.y - viewport.y) / currentZoom;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
x: pointer.x - flowX * nextZoom,
|
|
120
|
+
y: pointer.y - flowY * nextZoom,
|
|
121
|
+
zoom: nextZoom,
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
|
|
39
125
|
const getIncomingEdgeGroupKey = (edge, stroke) =>
|
|
40
126
|
`${edge.target}::${stroke || DEFAULT_EDGE_STROKE}`;
|
|
41
127
|
|
|
128
|
+
const getOutgoingEdgeGroupKey = (edge, stroke) =>
|
|
129
|
+
`${edge.source}::${stroke || DEFAULT_EDGE_STROKE}`;
|
|
130
|
+
|
|
42
131
|
const getNodeRelationGroupKey = (edge) =>
|
|
43
132
|
edge?.data?.primaryType || edge?.style?.stroke || DEFAULT_EDGE_STROKE;
|
|
44
133
|
|
|
@@ -151,30 +240,335 @@ const orderNodesByRelationGroup = (nodes = [], edges = [], rootNodeId) => {
|
|
|
151
240
|
});
|
|
152
241
|
};
|
|
153
242
|
|
|
243
|
+
const getNodeHeight = (node) =>
|
|
244
|
+
node.height ?? node.measured?.height ?? DEFAULT_NODE_HEIGHT;
|
|
245
|
+
|
|
246
|
+
const getNodeCenterY = (node) =>
|
|
247
|
+
(node.y ?? node.position?.y ?? 0) + getNodeHeight(node) / 2;
|
|
248
|
+
|
|
249
|
+
const alignNodesToConnectionPoints = (nodes = [], edges = [], rootNodeId) => {
|
|
250
|
+
if (!rootNodeId) return nodes;
|
|
251
|
+
|
|
252
|
+
const levels = buildSignedNodeLevels(nodes, edges, rootNodeId);
|
|
253
|
+
|
|
254
|
+
if (!levels.size) return nodes;
|
|
255
|
+
|
|
256
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
257
|
+
const desiredCentersById = new Map();
|
|
258
|
+
|
|
259
|
+
nodes.forEach((node) => {
|
|
260
|
+
const rank = levels.get(node.id) || 0;
|
|
261
|
+
|
|
262
|
+
if (rank === 0) return;
|
|
263
|
+
|
|
264
|
+
const preferredEdges = edges.filter((edge) => {
|
|
265
|
+
if (rank < 0 && edge.source === node.id) {
|
|
266
|
+
const neighbourRank = levels.get(edge.target);
|
|
267
|
+
return (
|
|
268
|
+
neighbourRank !== undefined &&
|
|
269
|
+
Math.abs(neighbourRank) < Math.abs(rank)
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (rank > 0 && edge.target === node.id) {
|
|
274
|
+
const neighbourRank = levels.get(edge.source);
|
|
275
|
+
return (
|
|
276
|
+
neighbourRank !== undefined &&
|
|
277
|
+
Math.abs(neighbourRank) < Math.abs(rank)
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return false;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (preferredEdges.length !== 1) return;
|
|
285
|
+
|
|
286
|
+
const edge = preferredEdges[0];
|
|
287
|
+
const slotOffset = getTargetSlotOffset(
|
|
288
|
+
edge.data?.targetSlotIndex,
|
|
289
|
+
edge.data?.targetSlotCount,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
if (rank < 0) {
|
|
293
|
+
const targetNode = nodeById.get(edge.target);
|
|
294
|
+
|
|
295
|
+
if (!targetNode) return;
|
|
296
|
+
|
|
297
|
+
desiredCentersById.set(node.id, getNodeCenterY(targetNode) + slotOffset);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const sourceNode = nodeById.get(edge.source);
|
|
302
|
+
|
|
303
|
+
if (!sourceNode) return;
|
|
304
|
+
|
|
305
|
+
desiredCentersById.set(node.id, getNodeCenterY(sourceNode) - slotOffset);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const nodesByRank = new Map();
|
|
309
|
+
|
|
310
|
+
nodes.forEach((node) => {
|
|
311
|
+
const rank = levels.get(node.id) || 0;
|
|
312
|
+
const bucket = nodesByRank.get(rank) || [];
|
|
313
|
+
|
|
314
|
+
bucket.push(node);
|
|
315
|
+
nodesByRank.set(rank, bucket);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const alignedNodes = new Map();
|
|
319
|
+
|
|
320
|
+
nodesByRank.forEach((rankNodes) => {
|
|
321
|
+
const orderedNodes = [...rankNodes].sort((nodeA, nodeB) => {
|
|
322
|
+
const desiredA =
|
|
323
|
+
desiredCentersById.get(nodeA.id) ?? getNodeCenterY(nodeA);
|
|
324
|
+
const desiredB =
|
|
325
|
+
desiredCentersById.get(nodeB.id) ?? getNodeCenterY(nodeB);
|
|
326
|
+
|
|
327
|
+
if (desiredA !== desiredB) return desiredA - desiredB;
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
(nodeA.y ?? nodeA.position?.y ?? 0) -
|
|
331
|
+
(nodeB.y ?? nodeB.position?.y ?? 0)
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
orderedNodes.reduce((prevBottom, node) => {
|
|
336
|
+
const height = getNodeHeight(node);
|
|
337
|
+
const desiredCenter =
|
|
338
|
+
desiredCentersById.get(node.id) ?? getNodeCenterY(node);
|
|
339
|
+
const desiredTop = desiredCenter - height / 2;
|
|
340
|
+
const nextTop = Math.max(desiredTop, prevBottom + MIN_NODE_GAP);
|
|
341
|
+
|
|
342
|
+
alignedNodes.set(node.id, {
|
|
343
|
+
...node,
|
|
344
|
+
y: nextTop,
|
|
345
|
+
position: { ...(node.position || {}), x: node.x, y: nextTop },
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return nextTop + height;
|
|
349
|
+
}, Number.NEGATIVE_INFINITY);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return nodes.map((node) => alignedNodes.get(node.id) || node);
|
|
353
|
+
};
|
|
354
|
+
|
|
154
355
|
export const getMinZoomForDiagram = (
|
|
155
356
|
nodes = [],
|
|
156
357
|
viewportWidth,
|
|
157
358
|
viewportHeight,
|
|
158
359
|
padding = FIT_VIEW_PADDING,
|
|
360
|
+
minZoom = DEFAULT_MIN_ZOOM,
|
|
361
|
+
maxZoom = DEFAULT_MAX_ZOOM,
|
|
362
|
+
getDiagramBounds = getStaticNodesBounds,
|
|
159
363
|
) => {
|
|
160
|
-
if (!nodes.length || !viewportWidth || !viewportHeight)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
364
|
+
if (!nodes.length || !viewportWidth || !viewportHeight) return minZoom;
|
|
365
|
+
|
|
366
|
+
const bounds = getDiagramBounds(nodes);
|
|
367
|
+
|
|
368
|
+
if (!bounds.width || !bounds.height) return minZoom;
|
|
369
|
+
|
|
370
|
+
const fitZoom = getViewportForBounds(
|
|
371
|
+
bounds,
|
|
372
|
+
viewportWidth,
|
|
373
|
+
viewportHeight,
|
|
374
|
+
minZoom,
|
|
375
|
+
maxZoom,
|
|
376
|
+
padding,
|
|
377
|
+
).zoom;
|
|
378
|
+
|
|
379
|
+
return Math.min(fitZoom, minZoom);
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const nodeBoundsForViewport = (node = {}) => {
|
|
383
|
+
const geometry = node.data?.absoluteGeometry;
|
|
384
|
+
const x = geometry?.x ?? node.position?.x ?? node.x ?? 0;
|
|
385
|
+
const y = geometry?.y ?? node.position?.y ?? node.y ?? 0;
|
|
386
|
+
const width =
|
|
387
|
+
geometry?.w ?? node.width ?? node.measured?.width ?? DEFAULT_NODE_WIDTH;
|
|
388
|
+
const height =
|
|
389
|
+
geometry?.h ?? node.height ?? node.measured?.height ?? DEFAULT_NODE_HEIGHT;
|
|
390
|
+
|
|
391
|
+
return { x, y, width, height };
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const nodeCenterForViewport = (node) => {
|
|
395
|
+
const bounds = nodeBoundsForViewport(node);
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
x: bounds.x + bounds.width / 2,
|
|
399
|
+
y: bounds.y + bounds.height / 2,
|
|
400
|
+
};
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const finiteNumber = (value) => {
|
|
404
|
+
const parsed = Number(value);
|
|
405
|
+
|
|
406
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const firstFiniteNumber = (values = [], fallback = 0) =>
|
|
410
|
+
values.map(finiteNumber).find((value) => value !== undefined) ?? fallback;
|
|
411
|
+
|
|
412
|
+
const nodePositionForOverlap = (node = {}) => ({
|
|
413
|
+
x: firstFiniteNumber([node.position?.x, node.x], 0),
|
|
414
|
+
y: firstFiniteNumber([node.position?.y, node.y], 0),
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const nodeSizeForOverlap = (node = {}) => ({
|
|
418
|
+
width: firstFiniteNumber(
|
|
419
|
+
[node.width, node.measured?.width, node.data?.absoluteGeometry?.w],
|
|
420
|
+
DEFAULT_NODE_WIDTH,
|
|
421
|
+
),
|
|
422
|
+
height: firstFiniteNumber(
|
|
423
|
+
[node.height, node.measured?.height, node.data?.absoluteGeometry?.h],
|
|
424
|
+
DEFAULT_NODE_HEIGHT,
|
|
425
|
+
),
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const nodeBoundsForOverlap = (node = {}) => ({
|
|
429
|
+
...nodePositionForOverlap(node),
|
|
430
|
+
...nodeSizeForOverlap(node),
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const boundsOverlap = (boundsA, boundsB, gap = 0) =>
|
|
434
|
+
boundsA.x < boundsB.x + boundsB.width + gap &&
|
|
435
|
+
boundsA.x + boundsA.width + gap > boundsB.x &&
|
|
436
|
+
boundsA.y < boundsB.y + boundsB.height + gap &&
|
|
437
|
+
boundsA.y + boundsA.height + gap > boundsB.y;
|
|
438
|
+
|
|
439
|
+
const siblingDragPosition = (draggedNode, siblingNodes = [], parentNode) => {
|
|
440
|
+
const position = nodePositionForOverlap(draggedNode);
|
|
441
|
+
const size = nodeSizeForOverlap(draggedNode);
|
|
442
|
+
const parentSize = parentNode ? nodeSizeForOverlap(parentNode) : null;
|
|
443
|
+
const maxX = parentSize
|
|
444
|
+
? Math.max(0, parentSize.width - size.width)
|
|
445
|
+
: position.x;
|
|
446
|
+
const maxY = parentSize
|
|
447
|
+
? Math.max(0, parentSize.height - size.height)
|
|
448
|
+
: position.y;
|
|
449
|
+
const x = parentSize ? clamp(position.x, 0, maxX) : position.x;
|
|
450
|
+
const candidateYs = new Set([
|
|
451
|
+
parentSize ? clamp(position.y, 0, maxY) : position.y,
|
|
452
|
+
]);
|
|
453
|
+
|
|
454
|
+
siblingNodes.forEach((siblingNode) => {
|
|
455
|
+
const siblingBounds = nodeBoundsForOverlap(siblingNode);
|
|
456
|
+
|
|
457
|
+
candidateYs.add(siblingBounds.y + siblingBounds.height + MIN_NODE_GAP);
|
|
458
|
+
candidateYs.add(siblingBounds.y - size.height - MIN_NODE_GAP);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
if (parentSize) {
|
|
462
|
+
candidateYs.add(0);
|
|
463
|
+
candidateYs.add(maxY);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const sortedCandidates = [...candidateYs]
|
|
467
|
+
.map((y) => (parentSize ? clamp(y, 0, maxY) : y))
|
|
468
|
+
.sort((yA, yB) => Math.abs(yA - position.y) - Math.abs(yB - position.y));
|
|
469
|
+
const nextY =
|
|
470
|
+
sortedCandidates.find((y) => {
|
|
471
|
+
const candidateBounds = { x, y, ...size };
|
|
472
|
+
|
|
473
|
+
return siblingNodes.every(
|
|
474
|
+
(siblingNode) =>
|
|
475
|
+
!boundsOverlap(
|
|
476
|
+
candidateBounds,
|
|
477
|
+
nodeBoundsForOverlap(siblingNode),
|
|
478
|
+
MIN_NODE_GAP,
|
|
479
|
+
),
|
|
480
|
+
);
|
|
481
|
+
}) ?? (parentSize ? clamp(position.y, 0, maxY) : position.y);
|
|
482
|
+
|
|
483
|
+
return { x, y: nextY };
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const nodeWithUpdatedFixedPosition = (node, position, nodesById) => {
|
|
487
|
+
const size = nodeSizeForOverlap(node);
|
|
488
|
+
const parentGeometry = node.parentId
|
|
489
|
+
? nodesById.get(node.parentId)?.data?.absoluteGeometry
|
|
490
|
+
: undefined;
|
|
491
|
+
const absoluteGeometry = node.data?.absoluteGeometry
|
|
492
|
+
? {
|
|
493
|
+
...node.data.absoluteGeometry,
|
|
494
|
+
x: (parentGeometry?.x ?? 0) + position.x,
|
|
495
|
+
y: (parentGeometry?.y ?? 0) + position.y,
|
|
496
|
+
w: size.width,
|
|
497
|
+
h: size.height,
|
|
498
|
+
}
|
|
499
|
+
: undefined;
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
...node,
|
|
503
|
+
position,
|
|
504
|
+
x: position.x,
|
|
505
|
+
y: position.y,
|
|
506
|
+
data: absoluteGeometry
|
|
507
|
+
? {
|
|
508
|
+
...node.data,
|
|
509
|
+
absoluteGeometry,
|
|
510
|
+
}
|
|
511
|
+
: node.data,
|
|
512
|
+
};
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
export const resolveFixedNodeSiblingOverlap = (nodes = [], draggedNodeId) => {
|
|
516
|
+
const nodesById = new Map(nodes.map((node) => [node.id, node]));
|
|
517
|
+
const draggedNode = nodesById.get(draggedNodeId);
|
|
518
|
+
|
|
519
|
+
if (!draggedNode || draggedNode.draggable === false) return nodes;
|
|
520
|
+
|
|
521
|
+
const parentNode = draggedNode.parentId
|
|
522
|
+
? nodesById.get(draggedNode.parentId)
|
|
523
|
+
: undefined;
|
|
524
|
+
const siblingNodes = nodes.filter(
|
|
525
|
+
(node) =>
|
|
526
|
+
node.id !== draggedNode.id && node.parentId === draggedNode.parentId,
|
|
527
|
+
);
|
|
528
|
+
const nextPosition = siblingDragPosition(
|
|
529
|
+
draggedNode,
|
|
530
|
+
siblingNodes,
|
|
531
|
+
parentNode,
|
|
177
532
|
);
|
|
533
|
+
const currentPosition = nodePositionForOverlap(draggedNode);
|
|
534
|
+
|
|
535
|
+
if (
|
|
536
|
+
nextPosition.x === currentPosition.x &&
|
|
537
|
+
nextPosition.y === currentPosition.y
|
|
538
|
+
) {
|
|
539
|
+
return nodes;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return nodes.map((node) =>
|
|
543
|
+
node.id === draggedNode.id
|
|
544
|
+
? nodeWithUpdatedFixedPosition(node, nextPosition, nodesById)
|
|
545
|
+
: node,
|
|
546
|
+
);
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const getFocusZoomForNode = (
|
|
550
|
+
node,
|
|
551
|
+
viewportWidth,
|
|
552
|
+
viewportHeight,
|
|
553
|
+
minZoom,
|
|
554
|
+
currentZoom,
|
|
555
|
+
) => {
|
|
556
|
+
if (!viewportWidth || !viewportHeight) return currentZoom;
|
|
557
|
+
|
|
558
|
+
const bounds = nodeBoundsForViewport(node);
|
|
559
|
+
|
|
560
|
+
if (!bounds.width || !bounds.height) return currentZoom;
|
|
561
|
+
|
|
562
|
+
const focusZoom = getViewportForBounds(
|
|
563
|
+
bounds,
|
|
564
|
+
viewportWidth,
|
|
565
|
+
viewportHeight,
|
|
566
|
+
DEFAULT_MIN_ZOOM,
|
|
567
|
+
DEFAULT_MAX_ZOOM,
|
|
568
|
+
FOCUS_VIEW_PADDING,
|
|
569
|
+
).zoom;
|
|
570
|
+
|
|
571
|
+
return Math.max(minZoom, Math.min(FOCUS_MAX_ZOOM, focusZoom));
|
|
178
572
|
};
|
|
179
573
|
|
|
180
574
|
const GraphControlTooltip = ({ content, children }) => (
|
|
@@ -192,30 +586,182 @@ GraphControlTooltip.propTypes = {
|
|
|
192
586
|
children: PropTypes.node.isRequired,
|
|
193
587
|
};
|
|
194
588
|
|
|
589
|
+
const getTranslateExtent = (nodes = []) => {
|
|
590
|
+
if (!nodes.length) return DEFAULT_TRANSLATE_EXTENT;
|
|
591
|
+
|
|
592
|
+
const bounds = nodes.reduce(
|
|
593
|
+
(acc, node) => {
|
|
594
|
+
const width = node.measured?.width ?? node.width ?? DEFAULT_NODE_WIDTH;
|
|
595
|
+
const height =
|
|
596
|
+
node.measured?.height ?? node.height ?? DEFAULT_NODE_HEIGHT;
|
|
597
|
+
const x = node.position?.x ?? node.x ?? 0;
|
|
598
|
+
const y = node.position?.y ?? node.y ?? 0;
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
minX: Math.min(acc.minX, x),
|
|
602
|
+
minY: Math.min(acc.minY, y),
|
|
603
|
+
maxX: Math.max(acc.maxX, x + width),
|
|
604
|
+
maxY: Math.max(acc.maxY, y + height),
|
|
605
|
+
};
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
minX: Number.POSITIVE_INFINITY,
|
|
609
|
+
minY: Number.POSITIVE_INFINITY,
|
|
610
|
+
maxX: Number.NEGATIVE_INFINITY,
|
|
611
|
+
maxY: Number.NEGATIVE_INFINITY,
|
|
612
|
+
},
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
const diagramWidth = bounds.maxX - bounds.minX;
|
|
616
|
+
const diagramHeight = bounds.maxY - bounds.minY;
|
|
617
|
+
const padding = Math.max(
|
|
618
|
+
TRANSLATE_EXTENT_MIN_PADDING,
|
|
619
|
+
diagramWidth * TRANSLATE_EXTENT_SIZE_MULTIPLIER,
|
|
620
|
+
diagramHeight * TRANSLATE_EXTENT_SIZE_MULTIPLIER,
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
return [
|
|
624
|
+
[bounds.minX - padding, bounds.minY - padding],
|
|
625
|
+
[bounds.maxX + padding, bounds.maxY + padding],
|
|
626
|
+
];
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
const nodeLayoutSignature = (node = {}) =>
|
|
630
|
+
[
|
|
631
|
+
node.id,
|
|
632
|
+
node.parentId || "",
|
|
633
|
+
node.position?.x ?? node.x ?? 0,
|
|
634
|
+
node.position?.y ?? node.y ?? 0,
|
|
635
|
+
node.width ?? node.measured?.width ?? DEFAULT_NODE_WIDTH,
|
|
636
|
+
node.height ?? node.measured?.height ?? DEFAULT_NODE_HEIGHT,
|
|
637
|
+
].join(":");
|
|
638
|
+
|
|
639
|
+
const graphLayoutSignature = (nodes = [], edges = []) =>
|
|
640
|
+
`${nodes.map(nodeLayoutSignature).join("|")}::${edges
|
|
641
|
+
.map((edge) => edge.id)
|
|
642
|
+
.join("|")}`;
|
|
643
|
+
|
|
644
|
+
const absoluteGeometrySignature = (node = {}) => {
|
|
645
|
+
const geometry = node.data?.absoluteGeometry;
|
|
646
|
+
|
|
647
|
+
if (!geometry) return "";
|
|
648
|
+
|
|
649
|
+
return [geometry.x, geometry.y, geometry.w, geometry.h].join(":");
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const mergeFixedLayoutNodes = (currentNodes = [], nextNodes = []) => {
|
|
653
|
+
const currentById = new Map(currentNodes.map((node) => [node.id, node]));
|
|
654
|
+
|
|
655
|
+
return nextNodes.map((node) => {
|
|
656
|
+
const currentNode = currentById.get(node.id);
|
|
657
|
+
const geometryChanged =
|
|
658
|
+
absoluteGeometrySignature(currentNode) !==
|
|
659
|
+
absoluteGeometrySignature(node);
|
|
660
|
+
|
|
661
|
+
if (!currentNode?.position || node.draggable === false || geometryChanged) {
|
|
662
|
+
return node;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
...node,
|
|
667
|
+
measured: currentNode.measured || node.measured,
|
|
668
|
+
position: currentNode.position,
|
|
669
|
+
x: currentNode.x ?? node.x,
|
|
670
|
+
y: currentNode.y ?? node.y,
|
|
671
|
+
data: currentNode.data?.absoluteGeometry
|
|
672
|
+
? {
|
|
673
|
+
...node.data,
|
|
674
|
+
absoluteGeometry: currentNode.data.absoluteGeometry,
|
|
675
|
+
}
|
|
676
|
+
: node.data,
|
|
677
|
+
};
|
|
678
|
+
});
|
|
679
|
+
};
|
|
680
|
+
|
|
195
681
|
const LayoutFlow = ({
|
|
196
682
|
initialNodes,
|
|
197
683
|
initialEdges,
|
|
198
684
|
onNodeClick,
|
|
685
|
+
onNodeDoubleClick,
|
|
199
686
|
onOpenExpanded,
|
|
687
|
+
onCloseExpanded,
|
|
200
688
|
containerRef,
|
|
201
689
|
rootNodeId,
|
|
690
|
+
onPaneClick,
|
|
691
|
+
topControls,
|
|
692
|
+
afterControls,
|
|
693
|
+
bottomControls,
|
|
694
|
+
onInitialFitComplete,
|
|
695
|
+
graphOptions,
|
|
202
696
|
}) => {
|
|
203
|
-
const {
|
|
697
|
+
const {
|
|
698
|
+
layoutMode,
|
|
699
|
+
extraNodeTypes,
|
|
700
|
+
focusNodeId,
|
|
701
|
+
focusNodeRequestKey,
|
|
702
|
+
minimapNodeColor,
|
|
703
|
+
controlsOrientation = "horizontal",
|
|
704
|
+
fitViewOnLayoutChange,
|
|
705
|
+
fitViewPadding = FIT_VIEW_PADDING,
|
|
706
|
+
defaultFitViewPadding,
|
|
707
|
+
minZoom: minZoomLimit = DEFAULT_MIN_ZOOM,
|
|
708
|
+
maxZoom: maxZoomLimit = DEFAULT_MAX_ZOOM,
|
|
709
|
+
wheelZoomSensitivity,
|
|
710
|
+
preserveUserViewportOnLayoutChange,
|
|
711
|
+
fitViewRequestKey,
|
|
712
|
+
onlyRenderVisibleElements,
|
|
713
|
+
} = graphOptions || {};
|
|
714
|
+
const {
|
|
715
|
+
fitView,
|
|
716
|
+
zoomIn,
|
|
717
|
+
zoomOut,
|
|
718
|
+
getNodes,
|
|
719
|
+
getNodesBounds: getFlowNodesBounds,
|
|
720
|
+
setCenter,
|
|
721
|
+
getViewport,
|
|
722
|
+
setViewport,
|
|
723
|
+
} = useReactFlow();
|
|
204
724
|
const { formatMessage } = useIntl();
|
|
205
725
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
206
|
-
const [
|
|
726
|
+
const [isMinimapCollapsed, setIsMinimapCollapsed] = useState(false);
|
|
727
|
+
const [isMinimapExpanded, setIsMinimapExpanded] = useState(false);
|
|
728
|
+
const [minimapZoom, setMinimapZoom] = useState(1);
|
|
729
|
+
const [spacePanMode, setSpacePanMode] = useState(false);
|
|
730
|
+
const resolvedFitViewPadding = defaultFitViewPadding ?? fitViewPadding;
|
|
731
|
+
const [minZoom, setMinZoom] = useState(minZoomLimit);
|
|
207
732
|
const layoutRunId = useRef(0);
|
|
208
733
|
const fitViewFrameId = useRef();
|
|
734
|
+
const fitRunId = useRef(0);
|
|
735
|
+
const initialFitComplete = useRef(false);
|
|
736
|
+
const initialFitCallback = useRef(onInitialFitComplete);
|
|
737
|
+
initialFitCallback.current = onInitialFitComplete;
|
|
738
|
+
const focusFrameIds = useRef([]);
|
|
739
|
+
const wheelZoomFrameId = useRef();
|
|
740
|
+
const minimapZoomFrameId = useRef();
|
|
741
|
+
const pendingMinimapZoom = useRef(1);
|
|
742
|
+
const pendingWheelZoom = useRef({ delta: 0, pointer: null });
|
|
743
|
+
const userOwnsViewport = useRef(false);
|
|
209
744
|
const lastFitSignature = useRef("");
|
|
745
|
+
const lastFitViewRequestKey = useRef(fitViewRequestKey);
|
|
746
|
+
const suppressSpaceInteraction = useRef(false);
|
|
747
|
+
const focusNodeIdRef = useRef(focusNodeId);
|
|
748
|
+
focusNodeIdRef.current = focusNodeId ?? null;
|
|
749
|
+
const fixedLayout = layoutMode === "fixed";
|
|
750
|
+
const nodeTypes = useMemo(
|
|
751
|
+
() => ({ ...baseNodeTypes, ...(extraNodeTypes || {}) }),
|
|
752
|
+
[extraNodeTypes],
|
|
753
|
+
);
|
|
210
754
|
const parsedNodes = useMemo(
|
|
211
755
|
() =>
|
|
212
756
|
initialNodes.map((node) => ({
|
|
213
757
|
...node,
|
|
214
|
-
position:
|
|
215
|
-
|
|
216
|
-
|
|
758
|
+
position: fixedLayout
|
|
759
|
+
? node.position || { x: 0, y: 0 }
|
|
760
|
+
: { x: 0, y: 0 },
|
|
761
|
+
sourcePosition: node.sourcePosition || Position.Right,
|
|
762
|
+
targetPosition: node.targetPosition || Position.Left,
|
|
217
763
|
})),
|
|
218
|
-
[initialNodes],
|
|
764
|
+
[fixedLayout, initialNodes],
|
|
219
765
|
);
|
|
220
766
|
const parsedEdges = useMemo(() => {
|
|
221
767
|
const edgesWithStroke = initialEdges.map((edge) => {
|
|
@@ -234,60 +780,179 @@ const LayoutFlow = ({
|
|
|
234
780
|
};
|
|
235
781
|
});
|
|
236
782
|
|
|
237
|
-
const
|
|
783
|
+
const groupMetaByEdgeId = new Map();
|
|
784
|
+
const mergeEdgeMeta = (edgeId, meta) => {
|
|
785
|
+
groupMetaByEdgeId.set(edgeId, {
|
|
786
|
+
...groupMetaByEdgeId.get(edgeId),
|
|
787
|
+
...meta,
|
|
788
|
+
});
|
|
789
|
+
};
|
|
238
790
|
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
const targetGroups = groupsByTarget.get(edge.target) || new Map();
|
|
243
|
-
const group = targetGroups.get(groupKey) || [];
|
|
791
|
+
if (fixedLayout) {
|
|
792
|
+
const edgesByTarget = new Map();
|
|
793
|
+
const edgesBySource = new Map();
|
|
244
794
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
});
|
|
795
|
+
edgesWithStroke.forEach((edge) => {
|
|
796
|
+
const targetEdges = edgesByTarget.get(edge.target) || [];
|
|
797
|
+
const sourceEdges = edgesBySource.get(edge.source) || [];
|
|
249
798
|
|
|
250
|
-
|
|
799
|
+
targetEdges.push(edge);
|
|
800
|
+
edgesByTarget.set(edge.target, targetEdges);
|
|
801
|
+
sourceEdges.push(edge);
|
|
802
|
+
edgesBySource.set(edge.source, sourceEdges);
|
|
803
|
+
});
|
|
251
804
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
805
|
+
edgesByTarget.forEach((targetEdges) => {
|
|
806
|
+
const orderedEdges = [...targetEdges].sort((edgeA, edgeB) => {
|
|
807
|
+
const sourceDiff = edgeA.source.localeCompare(edgeB.source);
|
|
808
|
+
|
|
809
|
+
if (sourceDiff !== 0) return sourceDiff;
|
|
810
|
+
|
|
811
|
+
const strokeA = edgeA.style?.stroke || DEFAULT_EDGE_STROKE;
|
|
812
|
+
const strokeB = edgeB.style?.stroke || DEFAULT_EDGE_STROKE;
|
|
813
|
+
const strokeDiff = strokeA.localeCompare(strokeB);
|
|
256
814
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
815
|
+
return strokeDiff || edgeA.id.localeCompare(edgeB.id);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
orderedEdges.forEach((edge, edgeIndex) => {
|
|
819
|
+
mergeEdgeMeta(edge.id, {
|
|
820
|
+
targetSlotCount: orderedEdges.length,
|
|
821
|
+
targetSlotIndex: edgeIndex,
|
|
822
|
+
showTargetArrow: edge.data?.showTargetArrow !== false,
|
|
264
823
|
});
|
|
265
824
|
});
|
|
266
825
|
});
|
|
267
|
-
|
|
826
|
+
|
|
827
|
+
edgesBySource.forEach((sourceEdges) => {
|
|
828
|
+
const orderedEdges = [...sourceEdges].sort((edgeA, edgeB) => {
|
|
829
|
+
const targetDiff = edgeA.target.localeCompare(edgeB.target);
|
|
830
|
+
|
|
831
|
+
if (targetDiff !== 0) return targetDiff;
|
|
832
|
+
|
|
833
|
+
const strokeA = edgeA.style?.stroke || DEFAULT_EDGE_STROKE;
|
|
834
|
+
const strokeB = edgeB.style?.stroke || DEFAULT_EDGE_STROKE;
|
|
835
|
+
const strokeDiff = strokeA.localeCompare(strokeB);
|
|
836
|
+
|
|
837
|
+
return strokeDiff || edgeA.id.localeCompare(edgeB.id);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
orderedEdges.forEach((edge, edgeIndex) => {
|
|
841
|
+
mergeEdgeMeta(edge.id, {
|
|
842
|
+
sourceSlotCount: orderedEdges.length,
|
|
843
|
+
sourceSlotIndex: edgeIndex,
|
|
844
|
+
routeCount: orderedEdges.length,
|
|
845
|
+
routeIndex: edgeIndex,
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
} else {
|
|
850
|
+
const groupsByTarget = new Map();
|
|
851
|
+
const groupsBySource = new Map();
|
|
852
|
+
|
|
853
|
+
edgesWithStroke.forEach((edge) => {
|
|
854
|
+
const stroke = edge.style?.stroke || DEFAULT_EDGE_STROKE;
|
|
855
|
+
const groupKey = getIncomingEdgeGroupKey(edge, stroke);
|
|
856
|
+
const targetGroups = groupsByTarget.get(edge.target) || new Map();
|
|
857
|
+
const group = targetGroups.get(groupKey) || [];
|
|
858
|
+
const sourceGroupKey = getOutgoingEdgeGroupKey(edge, stroke);
|
|
859
|
+
const sourceGroups = groupsBySource.get(edge.source) || new Map();
|
|
860
|
+
const sourceGroup = sourceGroups.get(sourceGroupKey) || [];
|
|
861
|
+
|
|
862
|
+
group.push(edge.id);
|
|
863
|
+
targetGroups.set(groupKey, group);
|
|
864
|
+
groupsByTarget.set(edge.target, targetGroups);
|
|
865
|
+
sourceGroup.push(edge.id);
|
|
866
|
+
sourceGroups.set(sourceGroupKey, sourceGroup);
|
|
867
|
+
groupsBySource.set(edge.source, sourceGroups);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
groupsByTarget.forEach((targetGroups) => {
|
|
871
|
+
const orderedGroups = Array.from(targetGroups.entries()).sort(
|
|
872
|
+
([groupKeyA], [groupKeyB]) => groupKeyA.localeCompare(groupKeyB),
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
orderedGroups.forEach(([groupKey, edgeIds], groupIndex) => {
|
|
876
|
+
edgeIds.forEach((edgeId, edgeIndex) => {
|
|
877
|
+
mergeEdgeMeta(edgeId, {
|
|
878
|
+
targetSlotCount: orderedGroups.length,
|
|
879
|
+
targetSlotIndex: groupIndex,
|
|
880
|
+
showTargetArrow: edgeIndex === edgeIds.length - 1,
|
|
881
|
+
incomingGroupKey: groupKey,
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
groupsBySource.forEach((sourceGroups) => {
|
|
888
|
+
const orderedGroups = Array.from(sourceGroups.entries()).sort(
|
|
889
|
+
([groupKeyA], [groupKeyB]) => groupKeyA.localeCompare(groupKeyB),
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
orderedGroups.forEach(([, edgeIds], groupIndex) => {
|
|
893
|
+
edgeIds.forEach((edgeId) => {
|
|
894
|
+
mergeEdgeMeta(edgeId, {
|
|
895
|
+
sourceSlotCount: orderedGroups.length,
|
|
896
|
+
sourceSlotIndex: groupIndex,
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
});
|
|
900
|
+
});
|
|
901
|
+
}
|
|
268
902
|
|
|
269
903
|
return edgesWithStroke.map((edge) => ({
|
|
270
904
|
...edge,
|
|
271
|
-
markerEnd:
|
|
905
|
+
markerEnd: {
|
|
272
906
|
type: MarkerType.ArrowClosed,
|
|
273
907
|
width: 12,
|
|
274
908
|
height: 12,
|
|
275
909
|
color: edge.style?.stroke || DEFAULT_EDGE_STROKE,
|
|
910
|
+
...(edge.markerEnd || {}),
|
|
276
911
|
},
|
|
277
912
|
data: {
|
|
278
913
|
...edge.data,
|
|
279
914
|
...groupMetaByEdgeId.get(edge.id),
|
|
280
915
|
},
|
|
281
916
|
}));
|
|
282
|
-
}, [initialEdges]);
|
|
917
|
+
}, [fixedLayout, initialEdges]);
|
|
283
918
|
const [nodes, setNodes, onNodesChange] = useNodesState(parsedNodes);
|
|
284
919
|
const [edges, setEdges, onEdgesChange] = useEdgesState(parsedEdges);
|
|
920
|
+
const translateExtent = useMemo(() => getTranslateExtent(nodes), [nodes]);
|
|
921
|
+
const onFixedNodeDragStop = useCallback(
|
|
922
|
+
(_, draggedNode) => {
|
|
923
|
+
if (!fixedLayout || !draggedNode?.id) return;
|
|
924
|
+
|
|
925
|
+
setNodes((currentNodes) => {
|
|
926
|
+
const nodesWithDraggedPosition = currentNodes.map((node) =>
|
|
927
|
+
node.id === draggedNode.id
|
|
928
|
+
? {
|
|
929
|
+
...node,
|
|
930
|
+
...draggedNode,
|
|
931
|
+
data: {
|
|
932
|
+
...node.data,
|
|
933
|
+
...draggedNode.data,
|
|
934
|
+
},
|
|
935
|
+
style: {
|
|
936
|
+
...node.style,
|
|
937
|
+
...draggedNode.style,
|
|
938
|
+
},
|
|
939
|
+
}
|
|
940
|
+
: node,
|
|
941
|
+
);
|
|
942
|
+
|
|
943
|
+
return resolveFixedNodeSiblingOverlap(
|
|
944
|
+
nodesWithDraggedPosition,
|
|
945
|
+
draggedNode.id,
|
|
946
|
+
);
|
|
947
|
+
});
|
|
948
|
+
},
|
|
949
|
+
[fixedLayout, setNodes],
|
|
950
|
+
);
|
|
285
951
|
const layoutInput = useMemo(() => {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
rootNodeId
|
|
290
|
-
);
|
|
952
|
+
const parsedSafeNodes = parsedNodes.filter((node) => node && node.id);
|
|
953
|
+
const safeNodes = fixedLayout
|
|
954
|
+
? parsedSafeNodes
|
|
955
|
+
: orderNodesByRelationGroup(parsedSafeNodes, parsedEdges, rootNodeId);
|
|
291
956
|
const safeNodeIds = new Set(safeNodes.map((node) => node.id));
|
|
292
957
|
const safeEdges = parsedEdges.filter(
|
|
293
958
|
(edge) =>
|
|
@@ -300,44 +965,263 @@ const LayoutFlow = ({
|
|
|
300
965
|
);
|
|
301
966
|
|
|
302
967
|
return { nodes: safeNodes, edges: safeEdges };
|
|
303
|
-
}, [parsedNodes, parsedEdges, rootNodeId]);
|
|
968
|
+
}, [fixedLayout, parsedNodes, parsedEdges, rootNodeId]);
|
|
969
|
+
|
|
970
|
+
const scheduleFitView = useCallback(
|
|
971
|
+
(nodesForFit, edgesForFit) => {
|
|
972
|
+
const signature = fixedLayout
|
|
973
|
+
? graphLayoutSignature(nodesForFit, edgesForFit)
|
|
974
|
+
: `${nodesForFit.map((node) => node.id).join("|")}::${edgesForFit
|
|
975
|
+
.map((edge) => edge.id)
|
|
976
|
+
.join("|")}`;
|
|
977
|
+
const isInitialLoad = lastFitSignature.current === "";
|
|
978
|
+
const changed = signature !== lastFitSignature.current;
|
|
979
|
+
const fitViewRequested =
|
|
980
|
+
fitViewRequestKey !== lastFitViewRequestKey.current;
|
|
981
|
+
lastFitSignature.current = signature;
|
|
982
|
+
lastFitViewRequestKey.current = fitViewRequestKey;
|
|
983
|
+
|
|
984
|
+
if (fixedLayout && !changed && !fitViewRequested) return;
|
|
304
985
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
986
|
+
if (fitViewFrameId.current) {
|
|
987
|
+
cancelAnimationFrame(fitViewFrameId.current);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
fitRunId.current += 1;
|
|
991
|
+
const runId = fitRunId.current;
|
|
992
|
+
const fitWhenMeasured = () => {
|
|
993
|
+
const measuredNodes = getNodes();
|
|
994
|
+
const measuredIds = new Set(
|
|
995
|
+
measuredNodes
|
|
996
|
+
.filter((node) => node.measured?.width && node.measured?.height)
|
|
997
|
+
.map((node) => node.id),
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
if (
|
|
1001
|
+
initialFitCallback.current &&
|
|
1002
|
+
!initialFitComplete.current &&
|
|
1003
|
+
(!containerRef.current?.clientWidth ||
|
|
1004
|
+
!containerRef.current?.clientHeight ||
|
|
1005
|
+
nodesForFit.some((node) => !measuredIds.has(node.id)))
|
|
1006
|
+
) {
|
|
1007
|
+
fitViewFrameId.current = requestAnimationFrame(fitWhenMeasured);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const nextMinZoom = getMinZoomForDiagram(
|
|
1011
|
+
measuredNodes.length ? measuredNodes : nodesForFit,
|
|
1012
|
+
containerRef.current?.clientWidth,
|
|
1013
|
+
containerRef.current?.clientHeight,
|
|
1014
|
+
resolvedFitViewPadding,
|
|
1015
|
+
minZoomLimit,
|
|
1016
|
+
maxZoomLimit,
|
|
1017
|
+
getFlowNodesBounds,
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
setMinZoom(nextMinZoom);
|
|
1021
|
+
const shouldFollowFixedLayoutChange =
|
|
1022
|
+
fixedLayout &&
|
|
1023
|
+
fitViewOnLayoutChange &&
|
|
1024
|
+
(fitViewRequested ||
|
|
1025
|
+
!(preserveUserViewportOnLayoutChange && userOwnsViewport.current));
|
|
1026
|
+
const pendingFocusId = shouldFollowFixedLayoutChange
|
|
1027
|
+
? null
|
|
1028
|
+
: focusNodeIdRef.current;
|
|
1029
|
+
if (
|
|
1030
|
+
pendingFocusId &&
|
|
1031
|
+
(initialFitComplete.current || !initialFitCallback.current)
|
|
1032
|
+
) {
|
|
1033
|
+
const nodesForFocus = measuredNodes.length
|
|
1034
|
+
? measuredNodes
|
|
1035
|
+
: nodesForFit;
|
|
1036
|
+
const targetNode = nodesForFocus.find((n) => n.id === pendingFocusId);
|
|
1037
|
+
|
|
1038
|
+
if (targetNode) {
|
|
1039
|
+
const currentZoom = getViewport().zoom;
|
|
1040
|
+
const focusCenter = nodeCenterForViewport(targetNode);
|
|
1041
|
+
const focusZoom = getFocusZoomForNode(
|
|
1042
|
+
targetNode,
|
|
1043
|
+
containerRef.current?.clientWidth,
|
|
1044
|
+
containerRef.current?.clientHeight,
|
|
1045
|
+
nextMinZoom,
|
|
1046
|
+
currentZoom,
|
|
1047
|
+
);
|
|
1048
|
+
|
|
1049
|
+
setCenter(focusCenter.x, focusCenter.y, {
|
|
1050
|
+
zoom: focusZoom,
|
|
1051
|
+
duration: 300,
|
|
1052
|
+
});
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
316
1055
|
}
|
|
317
1056
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
1057
|
+
const shouldFitGraph =
|
|
1058
|
+
!fixedLayout ||
|
|
1059
|
+
isInitialLoad ||
|
|
1060
|
+
(initialFitCallback.current && !initialFitComplete.current) ||
|
|
1061
|
+
shouldFollowFixedLayoutChange;
|
|
1062
|
+
|
|
1063
|
+
if (!shouldFitGraph) return;
|
|
325
1064
|
|
|
326
|
-
|
|
1065
|
+
Promise.resolve(
|
|
327
1066
|
fitView({
|
|
328
|
-
padding:
|
|
329
|
-
duration:
|
|
1067
|
+
padding: resolvedFitViewPadding,
|
|
1068
|
+
duration:
|
|
1069
|
+
initialFitCallback.current && !initialFitComplete.current
|
|
1070
|
+
? 0
|
|
1071
|
+
: changed || isInitialLoad || fitViewRequested
|
|
1072
|
+
? 500
|
|
1073
|
+
: 0,
|
|
330
1074
|
minZoom: nextMinZoom,
|
|
1075
|
+
}),
|
|
1076
|
+
).then((fitted) => {
|
|
1077
|
+
if (runId !== fitRunId.current || initialFitComplete.current) return;
|
|
1078
|
+
if (fitted === false) {
|
|
1079
|
+
if (initialFitCallback.current)
|
|
1080
|
+
fitViewFrameId.current = requestAnimationFrame(fitWhenMeasured);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
fitViewFrameId.current = requestAnimationFrame(() => {
|
|
1084
|
+
if (runId !== fitRunId.current) return;
|
|
1085
|
+
initialFitComplete.current = true;
|
|
1086
|
+
initialFitCallback.current?.();
|
|
331
1087
|
});
|
|
332
1088
|
});
|
|
333
1089
|
};
|
|
1090
|
+
fitViewFrameId.current = requestAnimationFrame(fitWhenMeasured);
|
|
1091
|
+
},
|
|
1092
|
+
[
|
|
1093
|
+
containerRef,
|
|
1094
|
+
fitView,
|
|
1095
|
+
fitViewRequestKey,
|
|
1096
|
+
fixedLayout,
|
|
1097
|
+
fitViewOnLayoutChange,
|
|
1098
|
+
getFlowNodesBounds,
|
|
1099
|
+
getNodes,
|
|
1100
|
+
getViewport,
|
|
1101
|
+
maxZoomLimit,
|
|
1102
|
+
minZoomLimit,
|
|
1103
|
+
preserveUserViewportOnLayoutChange,
|
|
1104
|
+
resolvedFitViewPadding,
|
|
1105
|
+
setCenter,
|
|
1106
|
+
],
|
|
1107
|
+
);
|
|
1108
|
+
|
|
1109
|
+
const markUserViewportInteraction = useCallback(() => {
|
|
1110
|
+
userOwnsViewport.current = true;
|
|
1111
|
+
}, []);
|
|
1112
|
+
|
|
1113
|
+
const handleMoveStart = useCallback(
|
|
1114
|
+
(event) => {
|
|
1115
|
+
if (event) markUserViewportInteraction();
|
|
1116
|
+
},
|
|
1117
|
+
[markUserViewportInteraction],
|
|
1118
|
+
);
|
|
1119
|
+
|
|
1120
|
+
const handleWheelZoom = useCallback(
|
|
1121
|
+
(event) => {
|
|
1122
|
+
if (spacePanMode) return;
|
|
1123
|
+
if (
|
|
1124
|
+
!Number.isFinite(wheelZoomSensitivity) ||
|
|
1125
|
+
wheelZoomSensitivity <= 0 ||
|
|
1126
|
+
isEditableTarget(event.target)
|
|
1127
|
+
) {
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
event.preventDefault();
|
|
1132
|
+
const bounds = containerRef.current?.getBoundingClientRect();
|
|
1133
|
+
|
|
1134
|
+
if (!bounds) return;
|
|
1135
|
+
|
|
1136
|
+
const delta = normalizeWheelDelta(
|
|
1137
|
+
event.deltaY,
|
|
1138
|
+
event.deltaMode,
|
|
1139
|
+
bounds.height,
|
|
1140
|
+
);
|
|
1141
|
+
|
|
1142
|
+
pendingWheelZoom.current = {
|
|
1143
|
+
delta: pendingWheelZoom.current.delta + delta,
|
|
1144
|
+
pointer: {
|
|
1145
|
+
x: event.clientX - bounds.left,
|
|
1146
|
+
y: event.clientY - bounds.top,
|
|
1147
|
+
},
|
|
1148
|
+
};
|
|
1149
|
+
markUserViewportInteraction();
|
|
1150
|
+
|
|
1151
|
+
if (wheelZoomFrameId.current) return;
|
|
1152
|
+
|
|
1153
|
+
wheelZoomFrameId.current = requestAnimationFrame(() => {
|
|
1154
|
+
const pendingZoom = pendingWheelZoom.current;
|
|
1155
|
+
|
|
1156
|
+
wheelZoomFrameId.current = undefined;
|
|
1157
|
+
pendingWheelZoom.current = { delta: 0, pointer: null };
|
|
1158
|
+
|
|
1159
|
+
if (!pendingZoom.pointer || !pendingZoom.delta) return;
|
|
1160
|
+
|
|
1161
|
+
const nextViewport = viewportForWheelZoom({
|
|
1162
|
+
viewport: getViewport(),
|
|
1163
|
+
pointer: pendingZoom.pointer,
|
|
1164
|
+
delta: pendingZoom.delta,
|
|
1165
|
+
sensitivity: wheelZoomSensitivity,
|
|
1166
|
+
minZoom,
|
|
1167
|
+
maxZoom: maxZoomLimit,
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
setViewport(nextViewport, { duration: 0 });
|
|
1171
|
+
});
|
|
1172
|
+
},
|
|
1173
|
+
[
|
|
1174
|
+
containerRef,
|
|
1175
|
+
getViewport,
|
|
1176
|
+
markUserViewportInteraction,
|
|
1177
|
+
maxZoomLimit,
|
|
1178
|
+
minZoom,
|
|
1179
|
+
setViewport,
|
|
1180
|
+
wheelZoomSensitivity,
|
|
1181
|
+
spacePanMode,
|
|
1182
|
+
],
|
|
1183
|
+
);
|
|
1184
|
+
|
|
1185
|
+
useEffect(() => {
|
|
1186
|
+
const graphContainer = containerRef.current;
|
|
1187
|
+
|
|
1188
|
+
if (
|
|
1189
|
+
!graphContainer ||
|
|
1190
|
+
!Number.isFinite(wheelZoomSensitivity) ||
|
|
1191
|
+
wheelZoomSensitivity <= 0
|
|
1192
|
+
) {
|
|
1193
|
+
return undefined;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
graphContainer.addEventListener("wheel", handleWheelZoom, {
|
|
1197
|
+
capture: true,
|
|
1198
|
+
passive: false,
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
return () => {
|
|
1202
|
+
graphContainer.removeEventListener("wheel", handleWheelZoom, {
|
|
1203
|
+
capture: true,
|
|
1204
|
+
});
|
|
1205
|
+
};
|
|
1206
|
+
}, [containerRef, handleWheelZoom, wheelZoomSensitivity]);
|
|
334
1207
|
|
|
1208
|
+
const getLayoutedElements = useCallback(
|
|
1209
|
+
({ nodes: nodesToLayout, edges: edgesToLayout }) => {
|
|
335
1210
|
if (!nodesToLayout.length) {
|
|
336
1211
|
setNodes([]);
|
|
337
1212
|
setEdges([]);
|
|
338
1213
|
return;
|
|
339
1214
|
}
|
|
340
1215
|
|
|
1216
|
+
if (fixedLayout) {
|
|
1217
|
+
setNodes((currentNodes) =>
|
|
1218
|
+
mergeFixedLayoutNodes(currentNodes, nodesToLayout),
|
|
1219
|
+
);
|
|
1220
|
+
setEdges(edgesToLayout);
|
|
1221
|
+
scheduleFitView(nodesToLayout, edgesToLayout);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
341
1225
|
const currentLayoutRunId = layoutRunId.current + 1;
|
|
342
1226
|
layoutRunId.current = currentLayoutRunId;
|
|
343
1227
|
const layoutOptions = {
|
|
@@ -345,11 +1229,9 @@ const LayoutFlow = ({
|
|
|
345
1229
|
"elk.direction": "RIGHT",
|
|
346
1230
|
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES",
|
|
347
1231
|
"elk.layered.crossingMinimization.forceNodeModelOrder": true,
|
|
348
|
-
"elk.layered.
|
|
349
|
-
"elk.
|
|
350
|
-
"elk.
|
|
351
|
-
"elk.spacing.edgeNodeBetweenLayers": 8,
|
|
352
|
-
"elk.spacing.edgeEdgeBetweenLayers": 8,
|
|
1232
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": 120,
|
|
1233
|
+
"elk.spacing.nodeNode": 70,
|
|
1234
|
+
"elk.padding": "[top=40,left=40,bottom=40,right=40]",
|
|
353
1235
|
};
|
|
354
1236
|
const graph = {
|
|
355
1237
|
id: "root",
|
|
@@ -366,13 +1248,19 @@ const LayoutFlow = ({
|
|
|
366
1248
|
.layout(graph)
|
|
367
1249
|
.then(({ children = [] }) => {
|
|
368
1250
|
if (currentLayoutRunId !== layoutRunId.current) return;
|
|
369
|
-
children.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
});
|
|
1251
|
+
const positionedChildren = children.map((node) => ({
|
|
1252
|
+
...node,
|
|
1253
|
+
position: { x: node.x, y: node.y },
|
|
1254
|
+
}));
|
|
1255
|
+
|
|
1256
|
+
const alignedChildren = alignNodesToConnectionPoints(
|
|
1257
|
+
positionedChildren,
|
|
1258
|
+
edgesToLayout,
|
|
1259
|
+
rootNodeId,
|
|
1260
|
+
);
|
|
373
1261
|
|
|
374
|
-
setNodes(
|
|
375
|
-
scheduleFitView(
|
|
1262
|
+
setNodes(alignedChildren);
|
|
1263
|
+
scheduleFitView(alignedChildren, edgesToLayout);
|
|
376
1264
|
})
|
|
377
1265
|
.catch(() => {
|
|
378
1266
|
if (currentLayoutRunId !== layoutRunId.current) return;
|
|
@@ -382,7 +1270,7 @@ const LayoutFlow = ({
|
|
|
382
1270
|
scheduleFitView(nodesToLayout, edgesToLayout);
|
|
383
1271
|
});
|
|
384
1272
|
},
|
|
385
|
-
[
|
|
1273
|
+
[fixedLayout, rootNodeId, scheduleFitView, setEdges, setNodes],
|
|
386
1274
|
);
|
|
387
1275
|
|
|
388
1276
|
const onInit = useCallback(() => {
|
|
@@ -392,24 +1280,197 @@ const LayoutFlow = ({
|
|
|
392
1280
|
getLayoutedElements(layoutInput);
|
|
393
1281
|
}, [getLayoutedElements, layoutInput, setEdges, setNodes]);
|
|
394
1282
|
|
|
1283
|
+
useEffect(() => {
|
|
1284
|
+
if (isInitialized) setMinimapZoom(getViewport().zoom);
|
|
1285
|
+
}, [getViewport, isInitialized]);
|
|
1286
|
+
|
|
1287
|
+
const suppressCanvasAction = useCallback((event) => {
|
|
1288
|
+
event?.preventDefault?.();
|
|
1289
|
+
event?.stopPropagation?.();
|
|
1290
|
+
}, []);
|
|
1291
|
+
|
|
1292
|
+
const handleNodeClick = useCallback(
|
|
1293
|
+
(event, node) => {
|
|
1294
|
+
if (spacePanMode || suppressSpaceInteraction.current) {
|
|
1295
|
+
suppressCanvasAction(event);
|
|
1296
|
+
suppressSpaceInteraction.current = false;
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
onNodeClick && onNodeClick(event, node);
|
|
1301
|
+
},
|
|
1302
|
+
[onNodeClick, spacePanMode, suppressCanvasAction],
|
|
1303
|
+
);
|
|
1304
|
+
|
|
1305
|
+
const handleNodeDoubleClick = useCallback(
|
|
1306
|
+
(event, node) => {
|
|
1307
|
+
if (spacePanMode || suppressSpaceInteraction.current) {
|
|
1308
|
+
suppressCanvasAction(event);
|
|
1309
|
+
suppressSpaceInteraction.current = false;
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
onNodeDoubleClick && onNodeDoubleClick(event, node);
|
|
1314
|
+
},
|
|
1315
|
+
[onNodeDoubleClick, spacePanMode, suppressCanvasAction],
|
|
1316
|
+
);
|
|
1317
|
+
|
|
1318
|
+
const handlePaneClick = useCallback(
|
|
1319
|
+
(event) => {
|
|
1320
|
+
if (spacePanMode || suppressSpaceInteraction.current) {
|
|
1321
|
+
suppressCanvasAction(event);
|
|
1322
|
+
suppressSpaceInteraction.current = false;
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
onPaneClick && onPaneClick(event);
|
|
1327
|
+
},
|
|
1328
|
+
[onPaneClick, spacePanMode, suppressCanvasAction],
|
|
1329
|
+
);
|
|
1330
|
+
|
|
1331
|
+
useEffect(() => {
|
|
1332
|
+
const container = containerRef.current;
|
|
1333
|
+
if (!container) return undefined;
|
|
1334
|
+
|
|
1335
|
+
const handleMouseDown = () => {
|
|
1336
|
+
if (spacePanMode) {
|
|
1337
|
+
suppressSpaceInteraction.current = true;
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
const suppressAction = (event) => {
|
|
1342
|
+
if (!spacePanMode && !suppressSpaceInteraction.current) return;
|
|
1343
|
+
event.preventDefault();
|
|
1344
|
+
event.stopPropagation();
|
|
1345
|
+
if (event.type === "click") suppressSpaceInteraction.current = false;
|
|
1346
|
+
};
|
|
1347
|
+
container.addEventListener("mousedown", handleMouseDown, true);
|
|
1348
|
+
container.addEventListener("click", suppressAction, true);
|
|
1349
|
+
container.addEventListener("dblclick", suppressAction, true);
|
|
1350
|
+
container.addEventListener("contextmenu", suppressAction, true);
|
|
1351
|
+
return () => {
|
|
1352
|
+
container.removeEventListener("mousedown", handleMouseDown, true);
|
|
1353
|
+
container.removeEventListener("click", suppressAction, true);
|
|
1354
|
+
container.removeEventListener("dblclick", suppressAction, true);
|
|
1355
|
+
container.removeEventListener("contextmenu", suppressAction, true);
|
|
1356
|
+
};
|
|
1357
|
+
}, [containerRef, spacePanMode]);
|
|
1358
|
+
|
|
1359
|
+
useEffect(() => {
|
|
1360
|
+
const deactivateSpacePanMode = () => {
|
|
1361
|
+
setSpacePanMode(false);
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
const handleKeyDown = (event) => {
|
|
1365
|
+
if (!isSpaceKeyEvent(event) || isEditableTarget(event.target)) return;
|
|
1366
|
+
|
|
1367
|
+
event.preventDefault();
|
|
1368
|
+
setSpacePanMode(true);
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
const handleKeyUp = (event) => {
|
|
1372
|
+
if (!isSpaceKeyEvent(event)) return;
|
|
1373
|
+
|
|
1374
|
+
event.preventDefault();
|
|
1375
|
+
deactivateSpacePanMode();
|
|
1376
|
+
};
|
|
1377
|
+
|
|
1378
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
1379
|
+
document.addEventListener("keyup", handleKeyUp);
|
|
1380
|
+
window.addEventListener("blur", deactivateSpacePanMode);
|
|
1381
|
+
|
|
1382
|
+
return () => {
|
|
1383
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
1384
|
+
document.removeEventListener("keyup", handleKeyUp);
|
|
1385
|
+
window.removeEventListener("blur", deactivateSpacePanMode);
|
|
1386
|
+
};
|
|
1387
|
+
}, []);
|
|
1388
|
+
|
|
395
1389
|
useEffect(() => {
|
|
396
1390
|
if (!isInitialized) return;
|
|
397
1391
|
|
|
398
|
-
|
|
1392
|
+
if (fixedLayout) {
|
|
1393
|
+
setNodes((currentNodes) =>
|
|
1394
|
+
mergeFixedLayoutNodes(currentNodes, layoutInput.nodes),
|
|
1395
|
+
);
|
|
1396
|
+
} else {
|
|
1397
|
+
setNodes(layoutInput.nodes);
|
|
1398
|
+
}
|
|
399
1399
|
setEdges(layoutInput.edges);
|
|
400
1400
|
const animationFrameId = requestAnimationFrame(() => {
|
|
401
1401
|
getLayoutedElements(layoutInput);
|
|
402
1402
|
});
|
|
403
1403
|
|
|
404
1404
|
return () => cancelAnimationFrame(animationFrameId);
|
|
405
|
-
}, [
|
|
1405
|
+
}, [
|
|
1406
|
+
fixedLayout,
|
|
1407
|
+
isInitialized,
|
|
1408
|
+
layoutInput,
|
|
1409
|
+
setNodes,
|
|
1410
|
+
setEdges,
|
|
1411
|
+
getLayoutedElements,
|
|
1412
|
+
]);
|
|
1413
|
+
|
|
1414
|
+
useEffect(() => {
|
|
1415
|
+
if (!isInitialized || !focusNodeId) return undefined;
|
|
1416
|
+
|
|
1417
|
+
const focusNode = () => {
|
|
1418
|
+
const targetNode = getNodes().find((node) => node.id === focusNodeId);
|
|
1419
|
+
|
|
1420
|
+
if (!targetNode) return;
|
|
1421
|
+
|
|
1422
|
+
const focusCenter = nodeCenterForViewport(targetNode);
|
|
1423
|
+
const focusZoom = getFocusZoomForNode(
|
|
1424
|
+
targetNode,
|
|
1425
|
+
containerRef.current?.clientWidth,
|
|
1426
|
+
containerRef.current?.clientHeight,
|
|
1427
|
+
minZoom,
|
|
1428
|
+
getViewport().zoom,
|
|
1429
|
+
);
|
|
1430
|
+
|
|
1431
|
+
userOwnsViewport.current = true;
|
|
1432
|
+
setCenter(focusCenter.x, focusCenter.y, {
|
|
1433
|
+
zoom: focusZoom,
|
|
1434
|
+
duration: 300,
|
|
1435
|
+
});
|
|
1436
|
+
};
|
|
1437
|
+
const firstFrameId = requestAnimationFrame(() => {
|
|
1438
|
+
const secondFrameId = requestAnimationFrame(focusNode);
|
|
1439
|
+
|
|
1440
|
+
focusFrameIds.current = [firstFrameId, secondFrameId];
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
focusFrameIds.current = [firstFrameId];
|
|
1444
|
+
|
|
1445
|
+
return () => {
|
|
1446
|
+
focusFrameIds.current.forEach(cancelAnimationFrame);
|
|
1447
|
+
focusFrameIds.current = [];
|
|
1448
|
+
};
|
|
1449
|
+
}, [
|
|
1450
|
+
containerRef,
|
|
1451
|
+
focusNodeId,
|
|
1452
|
+
focusNodeRequestKey,
|
|
1453
|
+
getNodes,
|
|
1454
|
+
getViewport,
|
|
1455
|
+
isInitialized,
|
|
1456
|
+
minZoom,
|
|
1457
|
+
setCenter,
|
|
1458
|
+
]);
|
|
406
1459
|
|
|
407
1460
|
useEffect(
|
|
408
1461
|
() => () => {
|
|
409
1462
|
if (fitViewFrameId.current) {
|
|
410
1463
|
cancelAnimationFrame(fitViewFrameId.current);
|
|
411
1464
|
}
|
|
1465
|
+
if (wheelZoomFrameId.current) {
|
|
1466
|
+
cancelAnimationFrame(wheelZoomFrameId.current);
|
|
1467
|
+
}
|
|
1468
|
+
if (minimapZoomFrameId.current) {
|
|
1469
|
+
cancelAnimationFrame(minimapZoomFrameId.current);
|
|
1470
|
+
}
|
|
1471
|
+
focusFrameIds.current.forEach(cancelAnimationFrame);
|
|
412
1472
|
layoutRunId.current += 1;
|
|
1473
|
+
fitRunId.current += 1;
|
|
413
1474
|
},
|
|
414
1475
|
[],
|
|
415
1476
|
);
|
|
@@ -421,12 +1482,64 @@ const LayoutFlow = ({
|
|
|
421
1482
|
onNodesChange={onNodesChange}
|
|
422
1483
|
onEdgesChange={onEdgesChange}
|
|
423
1484
|
onInit={onInit}
|
|
424
|
-
onNodeClick={
|
|
1485
|
+
onNodeClick={handleNodeClick}
|
|
1486
|
+
onNodeDoubleClick={handleNodeDoubleClick}
|
|
425
1487
|
nodesConnectable={false}
|
|
1488
|
+
nodesDraggable={!spacePanMode}
|
|
1489
|
+
nodesFocusable={!spacePanMode}
|
|
1490
|
+
elementsSelectable={!spacePanMode}
|
|
1491
|
+
onlyRenderVisibleElements={onlyRenderVisibleElements}
|
|
1492
|
+
onNodeDragStop={fixedLayout ? onFixedNodeDragStop : undefined}
|
|
1493
|
+
panOnDrag
|
|
1494
|
+
onMoveStart={handleMoveStart}
|
|
1495
|
+
onMove={(_event, viewport) => {
|
|
1496
|
+
pendingMinimapZoom.current = viewport.zoom;
|
|
1497
|
+
if (minimapZoomFrameId.current) return;
|
|
1498
|
+
|
|
1499
|
+
minimapZoomFrameId.current = requestAnimationFrame(() => {
|
|
1500
|
+
minimapZoomFrameId.current = undefined;
|
|
1501
|
+
setMinimapZoom((currentZoom) =>
|
|
1502
|
+
Math.abs(currentZoom - pendingMinimapZoom.current) < 0.01
|
|
1503
|
+
? currentZoom
|
|
1504
|
+
: pendingMinimapZoom.current,
|
|
1505
|
+
);
|
|
1506
|
+
});
|
|
1507
|
+
}}
|
|
1508
|
+
{...(wheelZoomSensitivity
|
|
1509
|
+
? {
|
|
1510
|
+
zoomOnScroll: false,
|
|
1511
|
+
}
|
|
1512
|
+
: {})}
|
|
1513
|
+
{...(fixedLayout
|
|
1514
|
+
? {
|
|
1515
|
+
zoomOnDoubleClick: false,
|
|
1516
|
+
edgesFocusable: false,
|
|
1517
|
+
edgesReconnectable: false,
|
|
1518
|
+
elevateNodesOnSelect: false,
|
|
1519
|
+
elevateEdgesOnSelect: false,
|
|
1520
|
+
}
|
|
1521
|
+
: {})}
|
|
1522
|
+
{...(spacePanMode
|
|
1523
|
+
? {
|
|
1524
|
+
zoomOnScroll: false,
|
|
1525
|
+
zoomOnPinch: false,
|
|
1526
|
+
zoomOnDoubleClick: false,
|
|
1527
|
+
selectionOnDrag: false,
|
|
1528
|
+
selectionKeyCode: null,
|
|
1529
|
+
multiSelectionKeyCode: null,
|
|
1530
|
+
deleteKeyCode: null,
|
|
1531
|
+
panOnScroll: true,
|
|
1532
|
+
}
|
|
1533
|
+
: {})}
|
|
1534
|
+
translateExtent={translateExtent}
|
|
426
1535
|
nodeTypes={nodeTypes}
|
|
427
1536
|
edgeTypes={edgeTypes}
|
|
428
1537
|
attributionPosition="bottom-left"
|
|
429
|
-
|
|
1538
|
+
proOptions={{ hideAttribution: true }}
|
|
1539
|
+
minZoom={minZoom}
|
|
1540
|
+
maxZoom={maxZoomLimit}
|
|
1541
|
+
onPaneClick={handlePaneClick}
|
|
1542
|
+
className={spacePanMode ? "td-graph__space-pan-active" : undefined}
|
|
430
1543
|
>
|
|
431
1544
|
<Background
|
|
432
1545
|
variant={BackgroundVariant.Dots}
|
|
@@ -435,73 +1548,80 @@ const LayoutFlow = ({
|
|
|
435
1548
|
color="var(--td-graph-node-border, #d0d5e0)"
|
|
436
1549
|
/>
|
|
437
1550
|
<Controls
|
|
1551
|
+
className={`td-graph-controls td-graph-controls--${controlsOrientation}`}
|
|
438
1552
|
position="top-right"
|
|
439
|
-
orientation=
|
|
1553
|
+
orientation={controlsOrientation}
|
|
440
1554
|
showZoom={false}
|
|
441
1555
|
showFitView={false}
|
|
442
1556
|
showInteractive={false}
|
|
443
1557
|
>
|
|
1558
|
+
{topControls}
|
|
444
1559
|
<GraphControlTooltip
|
|
445
1560
|
content={formatMessage({ id: "graph.controls.zoom_in" })}
|
|
446
1561
|
>
|
|
447
1562
|
<ControlButton
|
|
448
|
-
onClick={() =>
|
|
1563
|
+
onClick={() => {
|
|
1564
|
+
markUserViewportInteraction();
|
|
1565
|
+
zoomIn({ duration: 200 });
|
|
1566
|
+
}}
|
|
449
1567
|
aria-label={formatMessage({ id: "graph.controls.zoom_in" })}
|
|
450
1568
|
>
|
|
451
|
-
<
|
|
452
|
-
viewBox="0 0 24 24"
|
|
453
|
-
fill="none"
|
|
454
|
-
stroke="currentColor"
|
|
455
|
-
strokeWidth="2"
|
|
456
|
-
strokeLinecap="round"
|
|
457
|
-
>
|
|
458
|
-
<line x1="12" y1="5" x2="12" y2="19" />
|
|
459
|
-
<line x1="5" y1="12" x2="19" y2="12" />
|
|
460
|
-
</svg>
|
|
1569
|
+
<ZoomIn size={20} strokeWidth={2} />
|
|
461
1570
|
</ControlButton>
|
|
462
1571
|
</GraphControlTooltip>
|
|
463
1572
|
<GraphControlTooltip
|
|
464
1573
|
content={formatMessage({ id: "graph.controls.zoom_out" })}
|
|
465
1574
|
>
|
|
466
1575
|
<ControlButton
|
|
467
|
-
onClick={() =>
|
|
1576
|
+
onClick={() => {
|
|
1577
|
+
markUserViewportInteraction();
|
|
1578
|
+
zoomOut({ duration: 200 });
|
|
1579
|
+
}}
|
|
468
1580
|
aria-label={formatMessage({ id: "graph.controls.zoom_out" })}
|
|
469
1581
|
>
|
|
470
|
-
<
|
|
471
|
-
viewBox="0 0 24 24"
|
|
472
|
-
fill="none"
|
|
473
|
-
stroke="currentColor"
|
|
474
|
-
strokeWidth="2"
|
|
475
|
-
strokeLinecap="round"
|
|
476
|
-
>
|
|
477
|
-
<line x1="5" y1="12" x2="19" y2="12" />
|
|
478
|
-
</svg>
|
|
1582
|
+
<ZoomOut size={20} strokeWidth={2} />
|
|
479
1583
|
</ControlButton>
|
|
480
1584
|
</GraphControlTooltip>
|
|
481
1585
|
<GraphControlTooltip
|
|
482
1586
|
content={formatMessage({ id: "graph.controls.fit_view" })}
|
|
483
1587
|
>
|
|
484
1588
|
<ControlButton
|
|
485
|
-
onClick={() =>
|
|
486
|
-
|
|
487
|
-
|
|
1589
|
+
onClick={() => {
|
|
1590
|
+
userOwnsViewport.current = false;
|
|
1591
|
+
fitView({
|
|
1592
|
+
padding: resolvedFitViewPadding,
|
|
1593
|
+
duration: 300,
|
|
1594
|
+
minZoom,
|
|
1595
|
+
});
|
|
1596
|
+
}}
|
|
488
1597
|
aria-label={formatMessage({ id: "graph.controls.fit_view" })}
|
|
489
1598
|
>
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
strokeLinecap="round"
|
|
496
|
-
strokeLinejoin="round"
|
|
497
|
-
>
|
|
498
|
-
<path d="M8 3H5a2 2 0 0 0-2 2v3" />
|
|
499
|
-
<path d="M21 8V5a2 2 0 0 0-2-2h-3" />
|
|
500
|
-
<path d="M3 16v3a2 2 0 0 0 2 2h3" />
|
|
501
|
-
<path d="M16 21h3a2 2 0 0 0 2-2v-3" />
|
|
502
|
-
</svg>
|
|
1599
|
+
{fixedLayout ? (
|
|
1600
|
+
<Focus size={20} strokeWidth={2} />
|
|
1601
|
+
) : (
|
|
1602
|
+
<Maximize size={20} strokeWidth={2} />
|
|
1603
|
+
)}
|
|
503
1604
|
</ControlButton>
|
|
504
1605
|
</GraphControlTooltip>
|
|
1606
|
+
{onCloseExpanded ? (
|
|
1607
|
+
<GraphControlTooltip
|
|
1608
|
+
content={formatMessage({
|
|
1609
|
+
id: "graph.controls.return_normal",
|
|
1610
|
+
defaultMessage: "Volver a vista normal",
|
|
1611
|
+
})}
|
|
1612
|
+
>
|
|
1613
|
+
<ControlButton
|
|
1614
|
+
className="td-graph-controls-return"
|
|
1615
|
+
onClick={onCloseExpanded}
|
|
1616
|
+
aria-label={formatMessage({
|
|
1617
|
+
id: "graph.controls.return_normal",
|
|
1618
|
+
defaultMessage: "Volver a vista normal",
|
|
1619
|
+
})}
|
|
1620
|
+
>
|
|
1621
|
+
<Minimize2 size={20} strokeWidth={2} />
|
|
1622
|
+
</ControlButton>
|
|
1623
|
+
</GraphControlTooltip>
|
|
1624
|
+
) : null}
|
|
505
1625
|
{onOpenExpanded ? (
|
|
506
1626
|
<GraphControlTooltip
|
|
507
1627
|
content={formatMessage({ id: "graph.controls.open_expanded" })}
|
|
@@ -510,23 +1630,90 @@ const LayoutFlow = ({
|
|
|
510
1630
|
onClick={onOpenExpanded}
|
|
511
1631
|
aria-label={formatMessage({ id: "graph.controls.open_expanded" })}
|
|
512
1632
|
>
|
|
513
|
-
<
|
|
514
|
-
viewBox="0 0 24 24"
|
|
515
|
-
fill="none"
|
|
516
|
-
stroke="currentColor"
|
|
517
|
-
strokeWidth="2"
|
|
518
|
-
strokeLinecap="round"
|
|
519
|
-
strokeLinejoin="round"
|
|
520
|
-
>
|
|
521
|
-
<path d="M15 3h6v6" />
|
|
522
|
-
<path d="M9 21H3v-6" />
|
|
523
|
-
<path d="M21 3l-7 7" />
|
|
524
|
-
<path d="M3 21l7-7" />
|
|
525
|
-
</svg>
|
|
1633
|
+
<Expand size={20} strokeWidth={2} />
|
|
526
1634
|
</ControlButton>
|
|
527
1635
|
</GraphControlTooltip>
|
|
528
1636
|
) : null}
|
|
1637
|
+
{afterControls}
|
|
529
1638
|
</Controls>
|
|
1639
|
+
{minimapNodeColor && (
|
|
1640
|
+
<div
|
|
1641
|
+
className={`td-graph-minimap-shell${
|
|
1642
|
+
isMinimapCollapsed ? " td-graph-minimap-shell--collapsed" : ""
|
|
1643
|
+
}${isMinimapExpanded ? " td-graph-minimap-shell--expanded" : ""}`}
|
|
1644
|
+
>
|
|
1645
|
+
<div className="td-graph-minimap-header">
|
|
1646
|
+
<span className="td-graph-minimap-title">
|
|
1647
|
+
<Icon name="map outline" aria-hidden="true" />
|
|
1648
|
+
Mapa
|
|
1649
|
+
</span>
|
|
1650
|
+
<span className="td-graph-minimap-zoom">
|
|
1651
|
+
{Math.round(minimapZoom * 100)}%
|
|
1652
|
+
</span>
|
|
1653
|
+
<button
|
|
1654
|
+
aria-expanded={!isMinimapCollapsed}
|
|
1655
|
+
aria-label={
|
|
1656
|
+
isMinimapCollapsed
|
|
1657
|
+
? "Expandir mini-mapa"
|
|
1658
|
+
: "Minimizar mini-mapa"
|
|
1659
|
+
}
|
|
1660
|
+
className="td-graph-minimap-toggle"
|
|
1661
|
+
onClick={(event) => {
|
|
1662
|
+
event.stopPropagation();
|
|
1663
|
+
setIsMinimapCollapsed((collapsed) => !collapsed);
|
|
1664
|
+
}}
|
|
1665
|
+
onMouseDown={(event) => event.stopPropagation()}
|
|
1666
|
+
type="button"
|
|
1667
|
+
>
|
|
1668
|
+
<Icon
|
|
1669
|
+
name={isMinimapCollapsed ? "chevron up" : "chevron down"}
|
|
1670
|
+
aria-hidden="true"
|
|
1671
|
+
/>
|
|
1672
|
+
</button>
|
|
1673
|
+
</div>
|
|
1674
|
+
{!isMinimapCollapsed ? (
|
|
1675
|
+
<div className="td-graph-minimap-body">
|
|
1676
|
+
<MiniMap
|
|
1677
|
+
position="bottom-left"
|
|
1678
|
+
pannable
|
|
1679
|
+
zoomable
|
|
1680
|
+
maskColor="rgba(15, 23, 42, 0.08)"
|
|
1681
|
+
nodeColor={minimapNodeColor}
|
|
1682
|
+
nodeStrokeColor="rgba(15, 23, 42, 0.12)"
|
|
1683
|
+
className="td-graph-minimap"
|
|
1684
|
+
/>
|
|
1685
|
+
<button
|
|
1686
|
+
type="button"
|
|
1687
|
+
className="td-graph-minimap-expand"
|
|
1688
|
+
aria-label={
|
|
1689
|
+
isMinimapExpanded ? "Reducir mini-mapa" : "Expandir mini-mapa"
|
|
1690
|
+
}
|
|
1691
|
+
onClick={(event) => {
|
|
1692
|
+
event.stopPropagation();
|
|
1693
|
+
setIsMinimapExpanded((prev) => !prev);
|
|
1694
|
+
}}
|
|
1695
|
+
onMouseDown={(event) => event.stopPropagation()}
|
|
1696
|
+
>
|
|
1697
|
+
{isMinimapExpanded ? (
|
|
1698
|
+
<Minimize2 size={12} strokeWidth={2} />
|
|
1699
|
+
) : (
|
|
1700
|
+
<Expand size={12} strokeWidth={2} />
|
|
1701
|
+
)}
|
|
1702
|
+
</button>
|
|
1703
|
+
</div>
|
|
1704
|
+
) : null}
|
|
1705
|
+
</div>
|
|
1706
|
+
)}
|
|
1707
|
+
{bottomControls && (
|
|
1708
|
+
<Controls
|
|
1709
|
+
position="bottom-right"
|
|
1710
|
+
showZoom={false}
|
|
1711
|
+
showFitView={false}
|
|
1712
|
+
showInteractive={false}
|
|
1713
|
+
>
|
|
1714
|
+
{bottomControls}
|
|
1715
|
+
</Controls>
|
|
1716
|
+
)}
|
|
530
1717
|
</ReactFlow>
|
|
531
1718
|
);
|
|
532
1719
|
};
|
|
@@ -535,24 +1722,41 @@ LayoutFlow.propTypes = {
|
|
|
535
1722
|
initialNodes: PropTypes.array,
|
|
536
1723
|
initialEdges: PropTypes.array,
|
|
537
1724
|
onNodeClick: PropTypes.func,
|
|
1725
|
+
onNodeDoubleClick: PropTypes.func,
|
|
538
1726
|
onOpenExpanded: PropTypes.func,
|
|
1727
|
+
onCloseExpanded: PropTypes.func,
|
|
539
1728
|
containerRef: PropTypes.object,
|
|
540
1729
|
rootNodeId: PropTypes.string,
|
|
1730
|
+
onPaneClick: PropTypes.func,
|
|
1731
|
+
topControls: PropTypes.node,
|
|
1732
|
+
afterControls: PropTypes.node,
|
|
1733
|
+
bottomControls: PropTypes.node,
|
|
1734
|
+
onInitialFitComplete: PropTypes.func,
|
|
1735
|
+
graphOptions: PropTypes.object,
|
|
541
1736
|
};
|
|
542
1737
|
|
|
543
1738
|
const GraphCanvas = ({
|
|
544
1739
|
nodes,
|
|
545
1740
|
edges,
|
|
546
1741
|
onNodeClick,
|
|
1742
|
+
onNodeDoubleClick,
|
|
547
1743
|
height,
|
|
548
1744
|
onOpenExpanded,
|
|
1745
|
+
onCloseExpanded,
|
|
549
1746
|
rootNodeId,
|
|
1747
|
+
onPaneClick,
|
|
1748
|
+
topControls,
|
|
1749
|
+
afterControls,
|
|
1750
|
+
bottomControls,
|
|
1751
|
+
onInitialFitComplete,
|
|
1752
|
+
className,
|
|
1753
|
+
graphOptions,
|
|
550
1754
|
}) => {
|
|
551
1755
|
const containerRef = useRef(null);
|
|
552
1756
|
|
|
553
1757
|
return (
|
|
554
1758
|
<div
|
|
555
|
-
className="td-graph-container"
|
|
1759
|
+
className={["td-graph-container", className].filter(Boolean).join(" ")}
|
|
556
1760
|
style={{ "--td-graph-container-height": height }}
|
|
557
1761
|
ref={containerRef}
|
|
558
1762
|
>
|
|
@@ -561,9 +1765,17 @@ const GraphCanvas = ({
|
|
|
561
1765
|
initialNodes={nodes}
|
|
562
1766
|
initialEdges={edges}
|
|
563
1767
|
onNodeClick={onNodeClick}
|
|
1768
|
+
onNodeDoubleClick={onNodeDoubleClick}
|
|
564
1769
|
onOpenExpanded={onOpenExpanded}
|
|
1770
|
+
onCloseExpanded={onCloseExpanded}
|
|
565
1771
|
containerRef={containerRef}
|
|
566
1772
|
rootNodeId={rootNodeId}
|
|
1773
|
+
onPaneClick={onPaneClick}
|
|
1774
|
+
topControls={topControls}
|
|
1775
|
+
afterControls={afterControls}
|
|
1776
|
+
bottomControls={bottomControls}
|
|
1777
|
+
onInitialFitComplete={onInitialFitComplete}
|
|
1778
|
+
graphOptions={graphOptions}
|
|
567
1779
|
/>
|
|
568
1780
|
</ReactFlowProvider>
|
|
569
1781
|
</div>
|
|
@@ -574,19 +1786,36 @@ GraphCanvas.propTypes = {
|
|
|
574
1786
|
nodes: PropTypes.array,
|
|
575
1787
|
edges: PropTypes.array,
|
|
576
1788
|
onNodeClick: PropTypes.func,
|
|
1789
|
+
onNodeDoubleClick: PropTypes.func,
|
|
577
1790
|
height: PropTypes.string,
|
|
578
1791
|
onOpenExpanded: PropTypes.func,
|
|
1792
|
+
onCloseExpanded: PropTypes.func,
|
|
579
1793
|
rootNodeId: PropTypes.string,
|
|
1794
|
+
onPaneClick: PropTypes.func,
|
|
1795
|
+
topControls: PropTypes.node,
|
|
1796
|
+
afterControls: PropTypes.node,
|
|
1797
|
+
bottomControls: PropTypes.node,
|
|
1798
|
+
onInitialFitComplete: PropTypes.func,
|
|
1799
|
+
className: PropTypes.string,
|
|
1800
|
+
graphOptions: PropTypes.object,
|
|
580
1801
|
};
|
|
581
1802
|
|
|
582
1803
|
export const Graph = ({
|
|
583
1804
|
nodes,
|
|
584
1805
|
edges,
|
|
585
1806
|
onNodeClick,
|
|
1807
|
+
onNodeDoubleClick,
|
|
586
1808
|
allowExpandedView = true,
|
|
1809
|
+
height = DEFAULT_GRAPH_HEIGHT,
|
|
587
1810
|
rootNodeId,
|
|
1811
|
+
onPaneClick,
|
|
1812
|
+
topControls,
|
|
1813
|
+
afterControls,
|
|
1814
|
+
bottomControls,
|
|
1815
|
+
onInitialFitComplete,
|
|
1816
|
+
canvasClassName,
|
|
1817
|
+
graphOptions,
|
|
588
1818
|
}) => {
|
|
589
|
-
const { formatMessage } = useIntl();
|
|
590
1819
|
const [expandedOpen, setExpandedOpen] = useState(false);
|
|
591
1820
|
|
|
592
1821
|
const openExpanded = useCallback(() => setExpandedOpen(true), []);
|
|
@@ -598,9 +1827,17 @@ export const Graph = ({
|
|
|
598
1827
|
nodes={nodes}
|
|
599
1828
|
edges={edges}
|
|
600
1829
|
onNodeClick={onNodeClick}
|
|
601
|
-
|
|
1830
|
+
onNodeDoubleClick={onNodeDoubleClick}
|
|
1831
|
+
height={height}
|
|
602
1832
|
onOpenExpanded={allowExpandedView ? openExpanded : undefined}
|
|
603
1833
|
rootNodeId={rootNodeId}
|
|
1834
|
+
onPaneClick={onPaneClick}
|
|
1835
|
+
topControls={topControls}
|
|
1836
|
+
afterControls={afterControls}
|
|
1837
|
+
bottomControls={bottomControls}
|
|
1838
|
+
onInitialFitComplete={onInitialFitComplete}
|
|
1839
|
+
className={canvasClassName}
|
|
1840
|
+
graphOptions={graphOptions}
|
|
604
1841
|
/>
|
|
605
1842
|
{allowExpandedView ? (
|
|
606
1843
|
<Modal
|
|
@@ -610,18 +1847,24 @@ export const Graph = ({
|
|
|
610
1847
|
size="fullscreen"
|
|
611
1848
|
closeOnDimmerClick
|
|
612
1849
|
closeOnEscape
|
|
613
|
-
className="td-graph-modal"
|
|
1850
|
+
className="td-graph-modal td-graph-modal--expanded"
|
|
614
1851
|
>
|
|
615
|
-
<Modal.Header>
|
|
616
|
-
{formatMessage({ id: "graph.expanded.header" })}
|
|
617
|
-
</Modal.Header>
|
|
618
1852
|
<Modal.Content>
|
|
619
1853
|
<GraphCanvas
|
|
620
1854
|
nodes={nodes}
|
|
621
1855
|
edges={edges}
|
|
622
1856
|
onNodeClick={onNodeClick}
|
|
1857
|
+
onNodeDoubleClick={onNodeDoubleClick}
|
|
1858
|
+
onCloseExpanded={closeExpanded}
|
|
623
1859
|
height={EXPANDED_GRAPH_HEIGHT}
|
|
624
1860
|
rootNodeId={rootNodeId}
|
|
1861
|
+
onPaneClick={onPaneClick}
|
|
1862
|
+
topControls={topControls}
|
|
1863
|
+
afterControls={afterControls}
|
|
1864
|
+
bottomControls={bottomControls}
|
|
1865
|
+
onInitialFitComplete={onInitialFitComplete}
|
|
1866
|
+
className={canvasClassName}
|
|
1867
|
+
graphOptions={graphOptions}
|
|
625
1868
|
/>
|
|
626
1869
|
</Modal.Content>
|
|
627
1870
|
</Modal>
|
|
@@ -634,8 +1877,17 @@ Graph.propTypes = {
|
|
|
634
1877
|
nodes: PropTypes.array,
|
|
635
1878
|
edges: PropTypes.array,
|
|
636
1879
|
onNodeClick: PropTypes.func,
|
|
1880
|
+
onNodeDoubleClick: PropTypes.func,
|
|
637
1881
|
allowExpandedView: PropTypes.bool,
|
|
1882
|
+
height: PropTypes.string,
|
|
638
1883
|
rootNodeId: PropTypes.string,
|
|
1884
|
+
onPaneClick: PropTypes.func,
|
|
1885
|
+
topControls: PropTypes.node,
|
|
1886
|
+
afterControls: PropTypes.node,
|
|
1887
|
+
bottomControls: PropTypes.node,
|
|
1888
|
+
onInitialFitComplete: PropTypes.func,
|
|
1889
|
+
canvasClassName: PropTypes.string,
|
|
1890
|
+
graphOptions: PropTypes.object,
|
|
639
1891
|
};
|
|
640
1892
|
|
|
641
1893
|
export default Graph;
|