@principal-ai/principal-view-react 0.6.9 → 0.6.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/README.md +2 -5
- package/dist/components/ConfigurationSelector.js +4 -2
- package/dist/components/ConfigurationSelector.js.map +1 -1
- package/dist/components/EdgeInfoPanel.d.ts.map +1 -1
- package/dist/components/EdgeInfoPanel.js +43 -13
- package/dist/components/EdgeInfoPanel.js.map +1 -1
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +135 -82
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/components/NodeInfoPanel.d.ts.map +1 -1
- package/dist/components/NodeInfoPanel.js +143 -45
- package/dist/components/NodeInfoPanel.js.map +1 -1
- package/dist/edges/CustomEdge.d.ts.map +1 -1
- package/dist/edges/CustomEdge.js +2 -2
- package/dist/edges/CustomEdge.js.map +1 -1
- package/dist/edges/GenericEdge.d.ts.map +1 -1
- package/dist/edges/GenericEdge.js +2 -2
- package/dist/edges/GenericEdge.js.map +1 -1
- package/dist/hooks/usePathBasedEvents.d.ts +1 -1
- package/dist/hooks/usePathBasedEvents.d.ts.map +1 -1
- package/dist/hooks/usePathBasedEvents.js +9 -9
- package/dist/hooks/usePathBasedEvents.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/nodes/CustomNode.d.ts.map +1 -1
- package/dist/nodes/CustomNode.js +61 -44
- package/dist/nodes/CustomNode.js.map +1 -1
- package/dist/nodes/GenericNode.d.ts.map +1 -1
- package/dist/nodes/GenericNode.js.map +1 -1
- package/dist/utils/animationMapping.d.ts.map +1 -1
- package/dist/utils/animationMapping.js +12 -12
- package/dist/utils/animationMapping.js.map +1 -1
- package/dist/utils/graphConverter.d.ts.map +1 -1
- package/dist/utils/graphConverter.js +23 -17
- package/dist/utils/graphConverter.js.map +1 -1
- package/dist/utils/iconResolver.d.ts.map +1 -1
- package/dist/utils/iconResolver.js +1 -1
- package/dist/utils/iconResolver.js.map +1 -1
- package/package.json +2 -1
- package/src/components/ConfigurationSelector.tsx +5 -5
- package/src/components/EdgeInfoPanel.tsx +79 -37
- package/src/components/GraphRenderer.tsx +528 -364
- package/src/components/NodeInfoPanel.tsx +209 -86
- package/src/edges/CustomEdge.tsx +6 -4
- package/src/edges/GenericEdge.tsx +2 -6
- package/src/hooks/usePathBasedEvents.ts +54 -45
- package/src/index.ts +11 -2
- package/src/nodes/CustomNode.tsx +132 -106
- package/src/nodes/GenericNode.tsx +4 -3
- package/src/stories/AnimationWorkshop.stories.tsx +131 -12
- package/src/stories/CanvasNodeTypes.stories.tsx +898 -0
- package/src/stories/ColorPriority.stories.tsx +20 -10
- package/src/stories/EventDrivenAnimations.stories.tsx +8 -0
- package/src/stories/EventLog.stories.tsx +1 -1
- package/src/stories/GraphRenderer.stories.tsx +23 -10
- package/src/stories/IndustryThemes.stories.tsx +481 -0
- package/src/stories/MultiConfig.stories.tsx +8 -0
- package/src/stories/MultiDirectionalConnections.stories.tsx +8 -0
- package/src/stories/NodeFieldsAudit.stories.tsx +124 -37
- package/src/stories/NodeShapes.stories.tsx +73 -59
- package/src/utils/animationMapping.ts +19 -23
- package/src/utils/graphConverter.ts +35 -19
- package/src/utils/iconResolver.tsx +5 -1
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useMemo,
|
|
3
|
+
useState,
|
|
4
|
+
useEffect,
|
|
5
|
+
useCallback,
|
|
6
|
+
useRef,
|
|
7
|
+
useImperativeHandle,
|
|
8
|
+
forwardRef,
|
|
9
|
+
} from 'react';
|
|
2
10
|
import {
|
|
3
11
|
ReactFlow,
|
|
4
12
|
Background,
|
|
@@ -13,13 +21,26 @@ import {
|
|
|
13
21
|
type Connection,
|
|
14
22
|
} from '@xyflow/react';
|
|
15
23
|
import '@xyflow/react/dist/style.css';
|
|
16
|
-
import type {
|
|
24
|
+
import type {
|
|
25
|
+
GraphConfiguration,
|
|
26
|
+
NodeState,
|
|
27
|
+
EdgeState,
|
|
28
|
+
Violation,
|
|
29
|
+
GraphEvent,
|
|
30
|
+
ExtendedCanvas,
|
|
31
|
+
ComponentLibrary,
|
|
32
|
+
} from '@principal-ai/principal-view-core';
|
|
17
33
|
import { CanvasConverter } from '@principal-ai/principal-view-core';
|
|
34
|
+
import { useTheme } from '@principal-ade/industry-theme';
|
|
18
35
|
import { CustomNode } from '../nodes/CustomNode';
|
|
19
36
|
import type { CustomNodeData } from '../nodes/CustomNode';
|
|
20
37
|
import { CustomEdge } from '../edges/CustomEdge';
|
|
21
38
|
import type { CustomEdgeData } from '../edges/CustomEdge';
|
|
22
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
convertToXYFlowNodes,
|
|
41
|
+
convertToXYFlowEdges,
|
|
42
|
+
autoLayoutNodes,
|
|
43
|
+
} from '../utils/graphConverter';
|
|
23
44
|
import { EdgeInfoPanel } from './EdgeInfoPanel';
|
|
24
45
|
import { NodeInfoPanel } from './NodeInfoPanel';
|
|
25
46
|
|
|
@@ -34,11 +55,20 @@ export interface PendingChanges {
|
|
|
34
55
|
/** Node position changes */
|
|
35
56
|
positionChanges: NodePositionChange[];
|
|
36
57
|
/** Node updates (type, data changes) */
|
|
37
|
-
nodeUpdates: Array<{
|
|
58
|
+
nodeUpdates: Array<{
|
|
59
|
+
nodeId: string;
|
|
60
|
+
updates: { type?: string; data?: Record<string, unknown> };
|
|
61
|
+
}>;
|
|
38
62
|
/** Deleted node IDs */
|
|
39
63
|
deletedNodeIds: string[];
|
|
40
64
|
/** New edges created (with optional handle info for connection points) */
|
|
41
|
-
createdEdges: Array<{
|
|
65
|
+
createdEdges: Array<{
|
|
66
|
+
from: string;
|
|
67
|
+
to: string;
|
|
68
|
+
type: string;
|
|
69
|
+
sourceHandle?: string;
|
|
70
|
+
targetHandle?: string;
|
|
71
|
+
}>;
|
|
42
72
|
/** Deleted edges (with full connection info for config removal) */
|
|
43
73
|
deletedEdges: Array<{ from: string; to: string; type: string }>;
|
|
44
74
|
/** Whether there are any changes */
|
|
@@ -127,8 +157,19 @@ const edgeTypes = {
|
|
|
127
157
|
|
|
128
158
|
// Animation state for nodes and edges
|
|
129
159
|
interface AnimationState {
|
|
130
|
-
nodeAnimations: Record<
|
|
131
|
-
|
|
160
|
+
nodeAnimations: Record<
|
|
161
|
+
string,
|
|
162
|
+
{ type: 'pulse' | 'flash' | 'shake' | 'entry'; duration: number; timestamp: number }
|
|
163
|
+
>;
|
|
164
|
+
edgeAnimations: Record<
|
|
165
|
+
string,
|
|
166
|
+
{
|
|
167
|
+
type: 'flow' | 'particle' | 'pulse' | 'glow';
|
|
168
|
+
duration: number;
|
|
169
|
+
direction?: 'forward' | 'backward' | 'bidirectional';
|
|
170
|
+
timestamp: number;
|
|
171
|
+
}
|
|
172
|
+
>;
|
|
132
173
|
}
|
|
133
174
|
|
|
134
175
|
// Internal edit state tracking
|
|
@@ -136,7 +177,14 @@ interface EditState {
|
|
|
136
177
|
positionChanges: Map<string, { x: number; y: number }>;
|
|
137
178
|
nodeUpdates: Map<string, { type?: string; data?: Record<string, unknown> }>;
|
|
138
179
|
deletedNodeIds: Set<string>;
|
|
139
|
-
createdEdges: Array<{
|
|
180
|
+
createdEdges: Array<{
|
|
181
|
+
id: string;
|
|
182
|
+
from: string;
|
|
183
|
+
to: string;
|
|
184
|
+
type: string;
|
|
185
|
+
sourceHandle?: string;
|
|
186
|
+
targetHandle?: string;
|
|
187
|
+
}>;
|
|
140
188
|
deletedEdges: Array<{ id: string; from: string; to: string; type: string }>;
|
|
141
189
|
}
|
|
142
190
|
|
|
@@ -186,6 +234,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
186
234
|
editStateRef,
|
|
187
235
|
}) => {
|
|
188
236
|
const { fitView } = useReactFlow();
|
|
237
|
+
const { theme } = useTheme();
|
|
189
238
|
|
|
190
239
|
// Track active animations
|
|
191
240
|
const [animationState, setAnimationState] = useState<AnimationState>({
|
|
@@ -217,14 +266,30 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
217
266
|
const [localEdges, setLocalEdges] = useState<EdgeState[]>(propEdges);
|
|
218
267
|
|
|
219
268
|
// Track the prop values to detect external changes
|
|
220
|
-
const propNodesKeyRef = useRef(
|
|
221
|
-
|
|
269
|
+
const propNodesKeyRef = useRef(
|
|
270
|
+
propNodes
|
|
271
|
+
.map((n) => n.id)
|
|
272
|
+
.sort()
|
|
273
|
+
.join(',')
|
|
274
|
+
);
|
|
275
|
+
const propEdgesKeyRef = useRef(
|
|
276
|
+
propEdges
|
|
277
|
+
.map((e) => e.id)
|
|
278
|
+
.sort()
|
|
279
|
+
.join(',')
|
|
280
|
+
);
|
|
222
281
|
|
|
223
282
|
// Sync local state with props when props change (e.g., config reload)
|
|
224
283
|
// This only happens when the structure changes, not during editing
|
|
225
284
|
useEffect(() => {
|
|
226
|
-
const newNodesKey = propNodes
|
|
227
|
-
|
|
285
|
+
const newNodesKey = propNodes
|
|
286
|
+
.map((n) => n.id)
|
|
287
|
+
.sort()
|
|
288
|
+
.join(',');
|
|
289
|
+
const newEdgesKey = propEdges
|
|
290
|
+
.map((e) => e.id)
|
|
291
|
+
.sort()
|
|
292
|
+
.join(',');
|
|
228
293
|
|
|
229
294
|
if (newNodesKey !== propNodesKeyRef.current || newEdgesKey !== propEdgesKeyRef.current) {
|
|
230
295
|
propNodesKeyRef.current = newNodesKey;
|
|
@@ -245,20 +310,25 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
245
310
|
|
|
246
311
|
// Helper to check if there are pending changes
|
|
247
312
|
const checkHasChanges = useCallback((state: EditState): boolean => {
|
|
248
|
-
return
|
|
313
|
+
return (
|
|
314
|
+
state.positionChanges.size > 0 ||
|
|
249
315
|
state.nodeUpdates.size > 0 ||
|
|
250
316
|
state.deletedNodeIds.size > 0 ||
|
|
251
317
|
state.createdEdges.length > 0 ||
|
|
252
|
-
state.deletedEdges.length > 0
|
|
318
|
+
state.deletedEdges.length > 0
|
|
319
|
+
);
|
|
253
320
|
}, []);
|
|
254
321
|
|
|
255
322
|
// Helper to update edit state and notify parent
|
|
256
|
-
const updateEditState = useCallback(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
323
|
+
const updateEditState = useCallback(
|
|
324
|
+
(updater: (prev: EditState) => EditState) => {
|
|
325
|
+
const newState = updater(editStateRef.current);
|
|
326
|
+
editStateRef.current = newState;
|
|
327
|
+
onEditStateChange?.(newState);
|
|
328
|
+
onPendingChangesChange?.(checkHasChanges(newState));
|
|
329
|
+
},
|
|
330
|
+
[editStateRef, onEditStateChange, onPendingChangesChange, checkHasChanges]
|
|
331
|
+
);
|
|
262
332
|
|
|
263
333
|
// ============================================
|
|
264
334
|
// EVENT HANDLERS
|
|
@@ -287,180 +357,210 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
287
357
|
}, []);
|
|
288
358
|
|
|
289
359
|
// Handle node update (internal - updates local state only)
|
|
290
|
-
const handleNodeUpdate = useCallback(
|
|
291
|
-
|
|
360
|
+
const handleNodeUpdate = useCallback(
|
|
361
|
+
(nodeId: string, updates: { type?: string; data?: Record<string, unknown> }) => {
|
|
362
|
+
if (!editable) return;
|
|
363
|
+
|
|
364
|
+
// Update local nodes
|
|
365
|
+
setLocalNodes((prev) =>
|
|
366
|
+
prev.map((node) => {
|
|
367
|
+
if (node.id === nodeId) {
|
|
368
|
+
return {
|
|
369
|
+
...node,
|
|
370
|
+
type: updates.type ?? node.type,
|
|
371
|
+
data: updates.data ? { ...node.data, ...updates.data } : node.data,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
return node;
|
|
375
|
+
})
|
|
376
|
+
);
|
|
292
377
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
type: updates.type ??
|
|
299
|
-
data: updates.data ? { ...
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
return node;
|
|
303
|
-
}));
|
|
304
|
-
|
|
305
|
-
// Track the change
|
|
306
|
-
updateEditState(prev => {
|
|
307
|
-
const newUpdates = new Map(prev.nodeUpdates);
|
|
308
|
-
const existing = newUpdates.get(nodeId) || {};
|
|
309
|
-
newUpdates.set(nodeId, {
|
|
310
|
-
type: updates.type ?? existing.type,
|
|
311
|
-
data: updates.data ? { ...existing.data, ...updates.data } : existing.data,
|
|
378
|
+
// Track the change
|
|
379
|
+
updateEditState((prev) => {
|
|
380
|
+
const newUpdates = new Map(prev.nodeUpdates);
|
|
381
|
+
const existing = newUpdates.get(nodeId) || {};
|
|
382
|
+
newUpdates.set(nodeId, {
|
|
383
|
+
type: updates.type ?? existing.type,
|
|
384
|
+
data: updates.data ? { ...existing.data, ...updates.data } : existing.data,
|
|
385
|
+
});
|
|
386
|
+
return { ...prev, nodeUpdates: newUpdates };
|
|
312
387
|
});
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
388
|
+
},
|
|
389
|
+
[editable, updateEditState]
|
|
390
|
+
);
|
|
316
391
|
|
|
317
392
|
// Handle node delete (internal)
|
|
318
|
-
const handleNodeDelete = useCallback(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
393
|
+
const handleNodeDelete = useCallback(
|
|
394
|
+
(nodeId: string) => {
|
|
395
|
+
if (!editable) return;
|
|
396
|
+
|
|
397
|
+
// Remove from local state
|
|
398
|
+
setLocalNodes((prev) => prev.filter((n) => n.id !== nodeId));
|
|
399
|
+
setLocalEdges((prev) => prev.filter((e) => e.from !== nodeId && e.to !== nodeId));
|
|
400
|
+
|
|
401
|
+
// Track the change
|
|
402
|
+
updateEditState((prev) => {
|
|
403
|
+
const newDeletedNodes = new Set(prev.deletedNodeIds);
|
|
404
|
+
newDeletedNodes.add(nodeId);
|
|
405
|
+
// Remove any pending updates for this node
|
|
406
|
+
const newUpdates = new Map(prev.nodeUpdates);
|
|
407
|
+
newUpdates.delete(nodeId);
|
|
408
|
+
// Remove any position changes for this node
|
|
409
|
+
const newPositions = new Map(prev.positionChanges);
|
|
410
|
+
newPositions.delete(nodeId);
|
|
411
|
+
// Remove created edges that involve this node
|
|
412
|
+
const newCreatedEdges = prev.createdEdges.filter(
|
|
413
|
+
(e) => e.from !== nodeId && e.to !== nodeId
|
|
414
|
+
);
|
|
415
|
+
return {
|
|
416
|
+
...prev,
|
|
417
|
+
deletedNodeIds: newDeletedNodes,
|
|
418
|
+
nodeUpdates: newUpdates,
|
|
419
|
+
positionChanges: newPositions,
|
|
420
|
+
createdEdges: newCreatedEdges,
|
|
421
|
+
};
|
|
422
|
+
});
|
|
345
423
|
|
|
346
|
-
|
|
347
|
-
|
|
424
|
+
setSelectedNodeId(null);
|
|
425
|
+
},
|
|
426
|
+
[editable, updateEditState]
|
|
427
|
+
);
|
|
348
428
|
|
|
349
429
|
// Handle edge delete (internal)
|
|
350
|
-
const handleEdgeDelete = useCallback(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
430
|
+
const handleEdgeDelete = useCallback(
|
|
431
|
+
(edgeId: string) => {
|
|
432
|
+
if (!editable) return;
|
|
433
|
+
|
|
434
|
+
// Find the edge before removing it so we can track its full info
|
|
435
|
+
const edgeToDelete = localEdges.find((e) => e.id === edgeId);
|
|
436
|
+
|
|
437
|
+
// Remove from local state
|
|
438
|
+
setLocalEdges((prev) => prev.filter((e) => e.id !== edgeId));
|
|
439
|
+
|
|
440
|
+
// Track the change
|
|
441
|
+
updateEditState((prev) => {
|
|
442
|
+
// Check if this was a newly created edge
|
|
443
|
+
const createdEdgeIndex = prev.createdEdges.findIndex((e) => e.id === edgeId);
|
|
444
|
+
if (createdEdgeIndex >= 0) {
|
|
445
|
+
// Just remove it from created edges
|
|
446
|
+
const newCreatedEdges = [...prev.createdEdges];
|
|
447
|
+
newCreatedEdges.splice(createdEdgeIndex, 1);
|
|
448
|
+
return { ...prev, createdEdges: newCreatedEdges };
|
|
449
|
+
}
|
|
450
|
+
// Otherwise mark as deleted with full edge info
|
|
451
|
+
if (edgeToDelete) {
|
|
452
|
+
const newDeletedEdges = [
|
|
453
|
+
...prev.deletedEdges,
|
|
454
|
+
{
|
|
455
|
+
id: edgeId,
|
|
456
|
+
from: edgeToDelete.from,
|
|
457
|
+
to: edgeToDelete.to,
|
|
458
|
+
type: edgeToDelete.type,
|
|
459
|
+
},
|
|
460
|
+
];
|
|
461
|
+
return { ...prev, deletedEdges: newDeletedEdges };
|
|
462
|
+
}
|
|
463
|
+
return prev;
|
|
464
|
+
});
|
|
381
465
|
|
|
382
|
-
|
|
383
|
-
|
|
466
|
+
setSelectedEdgeId(null);
|
|
467
|
+
},
|
|
468
|
+
[editable, updateEditState, localEdges]
|
|
469
|
+
);
|
|
384
470
|
|
|
385
471
|
// Handle new connection from drag
|
|
386
|
-
const handleConnect = useCallback(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
472
|
+
const handleConnect = useCallback(
|
|
473
|
+
(connection: Connection) => {
|
|
474
|
+
if (!editable || !connection.source || !connection.target) return;
|
|
475
|
+
|
|
476
|
+
// Find source and target node types
|
|
477
|
+
const sourceNode = nodes.find((n) => n.id === connection.source);
|
|
478
|
+
const targetNode = nodes.find((n) => n.id === connection.target);
|
|
479
|
+
if (!sourceNode || !targetNode) return;
|
|
480
|
+
|
|
481
|
+
// Find valid edge types for this connection
|
|
482
|
+
const validTypes = configuration.allowedConnections
|
|
483
|
+
.filter((ac) => ac.from === sourceNode.type && ac.to === targetNode.type)
|
|
484
|
+
.map((ac) => ac.via);
|
|
485
|
+
|
|
486
|
+
const uniqueTypes = [...new Set(validTypes)];
|
|
487
|
+
|
|
488
|
+
if (uniqueTypes.length === 0) {
|
|
489
|
+
console.warn(
|
|
490
|
+
`No valid edge types for connection from ${sourceNode.type} to ${targetNode.type}`
|
|
491
|
+
);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
405
494
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
495
|
+
if (uniqueTypes.length === 1) {
|
|
496
|
+
// Create edge immediately with handle information
|
|
497
|
+
createEdge(
|
|
498
|
+
connection.source,
|
|
499
|
+
connection.target,
|
|
500
|
+
uniqueTypes[0],
|
|
501
|
+
connection.sourceHandle ?? undefined,
|
|
502
|
+
connection.targetHandle ?? undefined
|
|
503
|
+
);
|
|
504
|
+
} else {
|
|
505
|
+
// Show picker
|
|
506
|
+
setPendingConnection({
|
|
507
|
+
from: connection.source,
|
|
508
|
+
to: connection.target,
|
|
509
|
+
sourceHandle: connection.sourceHandle ?? undefined,
|
|
510
|
+
targetHandle: connection.targetHandle ?? undefined,
|
|
511
|
+
validTypes: uniqueTypes,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
[editable, nodes, configuration.allowedConnections]
|
|
516
|
+
);
|
|
426
517
|
|
|
427
518
|
// Create edge helper
|
|
428
|
-
const createEdge = useCallback(
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
519
|
+
const createEdge = useCallback(
|
|
520
|
+
(from: string, to: string, type: string, sourceHandle?: string, targetHandle?: string) => {
|
|
521
|
+
const edgeId = `${from}-${to}-${type}-${Date.now()}`;
|
|
522
|
+
|
|
523
|
+
// Add to local state with handle information
|
|
524
|
+
const newEdge: EdgeState & { sourceHandle?: string; targetHandle?: string } = {
|
|
525
|
+
id: edgeId,
|
|
526
|
+
type,
|
|
527
|
+
from,
|
|
528
|
+
to,
|
|
529
|
+
data: {},
|
|
530
|
+
createdAt: Date.now(),
|
|
531
|
+
updatedAt: Date.now(),
|
|
532
|
+
sourceHandle,
|
|
533
|
+
targetHandle,
|
|
534
|
+
};
|
|
535
|
+
setLocalEdges((prev) => [...prev, newEdge]);
|
|
444
536
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
537
|
+
// Track the change
|
|
538
|
+
updateEditState((prev) => ({
|
|
539
|
+
...prev,
|
|
540
|
+
createdEdges: [
|
|
541
|
+
...prev.createdEdges,
|
|
542
|
+
{ id: edgeId, from, to, type, sourceHandle, targetHandle },
|
|
543
|
+
],
|
|
544
|
+
}));
|
|
545
|
+
},
|
|
546
|
+
[updateEditState]
|
|
547
|
+
);
|
|
451
548
|
|
|
452
549
|
// Handle edge type selection from picker
|
|
453
|
-
const handleEdgeTypeSelect = useCallback(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
550
|
+
const handleEdgeTypeSelect = useCallback(
|
|
551
|
+
(type: string) => {
|
|
552
|
+
if (!pendingConnection) return;
|
|
553
|
+
createEdge(
|
|
554
|
+
pendingConnection.from,
|
|
555
|
+
pendingConnection.to,
|
|
556
|
+
type,
|
|
557
|
+
pendingConnection.sourceHandle,
|
|
558
|
+
pendingConnection.targetHandle
|
|
559
|
+
);
|
|
560
|
+
setPendingConnection(null);
|
|
561
|
+
},
|
|
562
|
+
[pendingConnection, createEdge]
|
|
563
|
+
);
|
|
464
564
|
|
|
465
565
|
// Cancel edge type picker
|
|
466
566
|
const handleCancelEdgeTypePicker = useCallback(() => {
|
|
@@ -476,84 +576,98 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
476
576
|
}, []);
|
|
477
577
|
|
|
478
578
|
// Handle edge reconnection (dragging edge endpoint to new node)
|
|
479
|
-
const handleReconnect = useCallback(
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
579
|
+
const handleReconnect = useCallback(
|
|
580
|
+
(oldEdge: Edge, newConnection: Connection) => {
|
|
581
|
+
if (!editable || !newConnection.source || !newConnection.target) return;
|
|
582
|
+
|
|
583
|
+
// Find the original edge in our local state
|
|
584
|
+
const originalEdge = localEdges.find((e) => e.id === oldEdge.id);
|
|
585
|
+
if (!originalEdge) return;
|
|
586
|
+
|
|
587
|
+
// Find source and target node types for validation
|
|
588
|
+
const sourceNode = nodes.find((n) => n.id === newConnection.source);
|
|
589
|
+
const targetNode = nodes.find((n) => n.id === newConnection.target);
|
|
590
|
+
if (!sourceNode || !targetNode) return;
|
|
591
|
+
|
|
592
|
+
// Check if the new connection is valid for this edge type
|
|
593
|
+
const isValidConnection = configuration.allowedConnections.some(
|
|
594
|
+
(ac) =>
|
|
595
|
+
ac.from === sourceNode.type && ac.to === targetNode.type && ac.via === originalEdge.type
|
|
596
|
+
);
|
|
490
597
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
598
|
+
if (!isValidConnection) {
|
|
599
|
+
console.warn(
|
|
600
|
+
`Cannot reconnect: ${originalEdge.type} edge not allowed from ${sourceNode.type} to ${targetNode.type}`
|
|
601
|
+
);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
495
604
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
605
|
+
// Mark as successful before updating
|
|
606
|
+
edgeReconnectSuccessful.current = true;
|
|
607
|
+
|
|
608
|
+
// Update local edges - manually update the edge to preserve its type and id
|
|
609
|
+
setLocalEdges((prev) =>
|
|
610
|
+
prev.map((edge) => {
|
|
611
|
+
if (edge.id === oldEdge.id) {
|
|
612
|
+
return {
|
|
613
|
+
...edge,
|
|
614
|
+
from: newConnection.source!,
|
|
615
|
+
to: newConnection.target!,
|
|
616
|
+
sourceHandle: newConnection.sourceHandle ?? undefined,
|
|
617
|
+
targetHandle: newConnection.targetHandle ?? undefined,
|
|
618
|
+
updatedAt: Date.now(),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
return edge;
|
|
622
|
+
})
|
|
623
|
+
);
|
|
500
624
|
|
|
501
|
-
|
|
502
|
-
|
|
625
|
+
// Track the change - remove old edge and add new one
|
|
626
|
+
updateEditState((prev) => {
|
|
627
|
+
// Check if this was a newly created edge
|
|
628
|
+
const createdEdgeIndex = prev.createdEdges.findIndex((e) => e.id === oldEdge.id);
|
|
629
|
+
|
|
630
|
+
if (createdEdgeIndex >= 0) {
|
|
631
|
+
// Update the created edge entry
|
|
632
|
+
const newCreatedEdges = [...prev.createdEdges];
|
|
633
|
+
newCreatedEdges[createdEdgeIndex] = {
|
|
634
|
+
...newCreatedEdges[createdEdgeIndex],
|
|
635
|
+
from: newConnection.source!,
|
|
636
|
+
to: newConnection.target!,
|
|
637
|
+
sourceHandle: newConnection.sourceHandle ?? undefined,
|
|
638
|
+
targetHandle: newConnection.targetHandle ?? undefined,
|
|
639
|
+
};
|
|
640
|
+
return { ...prev, createdEdges: newCreatedEdges };
|
|
641
|
+
}
|
|
503
642
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const newCreatedEdges = [...prev.createdEdges];
|
|
527
|
-
newCreatedEdges[createdEdgeIndex] = {
|
|
528
|
-
...newCreatedEdges[createdEdgeIndex],
|
|
529
|
-
from: newConnection.source!,
|
|
530
|
-
to: newConnection.target!,
|
|
531
|
-
sourceHandle: newConnection.sourceHandle ?? undefined,
|
|
532
|
-
targetHandle: newConnection.targetHandle ?? undefined,
|
|
533
|
-
};
|
|
534
|
-
return { ...prev, createdEdges: newCreatedEdges };
|
|
535
|
-
}
|
|
643
|
+
// For existing edges, track as delete + create
|
|
644
|
+
const newDeletedEdges = [
|
|
645
|
+
...prev.deletedEdges,
|
|
646
|
+
{
|
|
647
|
+
id: oldEdge.id,
|
|
648
|
+
from: originalEdge.from,
|
|
649
|
+
to: originalEdge.to,
|
|
650
|
+
type: originalEdge.type,
|
|
651
|
+
},
|
|
652
|
+
];
|
|
653
|
+
|
|
654
|
+
const newCreatedEdges = [
|
|
655
|
+
...prev.createdEdges,
|
|
656
|
+
{
|
|
657
|
+
id: oldEdge.id,
|
|
658
|
+
from: newConnection.source!,
|
|
659
|
+
to: newConnection.target!,
|
|
660
|
+
type: originalEdge.type,
|
|
661
|
+
sourceHandle: newConnection.sourceHandle ?? undefined,
|
|
662
|
+
targetHandle: newConnection.targetHandle ?? undefined,
|
|
663
|
+
},
|
|
664
|
+
];
|
|
536
665
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
type: originalEdge.type,
|
|
543
|
-
}];
|
|
544
|
-
|
|
545
|
-
const newCreatedEdges = [...prev.createdEdges, {
|
|
546
|
-
id: oldEdge.id,
|
|
547
|
-
from: newConnection.source!,
|
|
548
|
-
to: newConnection.target!,
|
|
549
|
-
type: originalEdge.type,
|
|
550
|
-
sourceHandle: newConnection.sourceHandle ?? undefined,
|
|
551
|
-
targetHandle: newConnection.targetHandle ?? undefined,
|
|
552
|
-
}];
|
|
553
|
-
|
|
554
|
-
return { ...prev, deletedEdges: newDeletedEdges, createdEdges: newCreatedEdges };
|
|
555
|
-
});
|
|
556
|
-
}, [editable, localEdges, nodes, configuration.allowedConnections, updateEditState]);
|
|
666
|
+
return { ...prev, deletedEdges: newDeletedEdges, createdEdges: newCreatedEdges };
|
|
667
|
+
});
|
|
668
|
+
},
|
|
669
|
+
[editable, localEdges, nodes, configuration.allowedConnections, updateEditState]
|
|
670
|
+
);
|
|
557
671
|
|
|
558
672
|
// Called when reconnection ends (whether successful or not)
|
|
559
673
|
const handleReconnectEnd = useCallback(() => {
|
|
@@ -569,7 +683,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
569
683
|
|
|
570
684
|
const selectedEdge = useMemo(() => {
|
|
571
685
|
if (!selectedEdgeId) return null;
|
|
572
|
-
return edges.find(e => e.id === selectedEdgeId);
|
|
686
|
+
return edges.find((e) => e.id === selectedEdgeId);
|
|
573
687
|
}, [selectedEdgeId, edges]);
|
|
574
688
|
|
|
575
689
|
const selectedEdgeTypeDefinition = useMemo(() => {
|
|
@@ -579,7 +693,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
579
693
|
|
|
580
694
|
const selectedNode = useMemo(() => {
|
|
581
695
|
if (!selectedNodeId) return null;
|
|
582
|
-
return nodes.find(n => n.id === selectedNodeId);
|
|
696
|
+
return nodes.find((n) => n.id === selectedNodeId);
|
|
583
697
|
}, [selectedNodeId, nodes]);
|
|
584
698
|
|
|
585
699
|
const selectedNodeTypeDefinition = useMemo(() => {
|
|
@@ -602,7 +716,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
602
716
|
const animation = edgeEvent.animation;
|
|
603
717
|
|
|
604
718
|
if (animation && edgeId) {
|
|
605
|
-
setAnimationState(prev => ({
|
|
719
|
+
setAnimationState((prev) => ({
|
|
606
720
|
...prev,
|
|
607
721
|
edgeAnimations: {
|
|
608
722
|
...prev.edgeAnimations,
|
|
@@ -617,7 +731,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
617
731
|
|
|
618
732
|
const duration = animation.duration || 1000;
|
|
619
733
|
setTimeout(() => {
|
|
620
|
-
setAnimationState(prev => {
|
|
734
|
+
setAnimationState((prev) => {
|
|
621
735
|
const newEdgeAnimations = { ...prev.edgeAnimations };
|
|
622
736
|
delete newEdgeAnimations[edgeId];
|
|
623
737
|
return { ...prev, edgeAnimations: newEdgeAnimations };
|
|
@@ -635,9 +749,9 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
635
749
|
|
|
636
750
|
if (nodeId && newState) {
|
|
637
751
|
// Update the node's state
|
|
638
|
-
setLocalNodes(prev =>
|
|
639
|
-
node.id === nodeId ? { ...node, state: newState } : node
|
|
640
|
-
)
|
|
752
|
+
setLocalNodes((prev) =>
|
|
753
|
+
prev.map((node) => (node.id === nodeId ? { ...node, state: newState } : node))
|
|
754
|
+
);
|
|
641
755
|
|
|
642
756
|
// Trigger animation based on state
|
|
643
757
|
const stateToAnimation: Record<string, 'pulse' | 'flash' | 'shake'> = {
|
|
@@ -648,9 +762,10 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
648
762
|
|
|
649
763
|
const animationType = stateToAnimation[newState];
|
|
650
764
|
if (animationType) {
|
|
651
|
-
const duration =
|
|
765
|
+
const duration =
|
|
766
|
+
animationType === 'pulse' ? 1500 : animationType === 'flash' ? 1000 : 500;
|
|
652
767
|
|
|
653
|
-
setAnimationState(prev => ({
|
|
768
|
+
setAnimationState((prev) => ({
|
|
654
769
|
...prev,
|
|
655
770
|
nodeAnimations: {
|
|
656
771
|
...prev.nodeAnimations,
|
|
@@ -660,7 +775,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
660
775
|
|
|
661
776
|
if (animationType !== 'pulse') {
|
|
662
777
|
setTimeout(() => {
|
|
663
|
-
setAnimationState(prev => {
|
|
778
|
+
setAnimationState((prev) => {
|
|
664
779
|
const newNodeAnimations = { ...prev.nodeAnimations };
|
|
665
780
|
delete newNodeAnimations[nodeId];
|
|
666
781
|
return { ...prev, nodeAnimations: newNodeAnimations };
|
|
@@ -678,7 +793,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
678
793
|
const nodeId = nodeEvent.nodeId;
|
|
679
794
|
|
|
680
795
|
if (nodeId) {
|
|
681
|
-
setAnimationState(prev => ({
|
|
796
|
+
setAnimationState((prev) => ({
|
|
682
797
|
...prev,
|
|
683
798
|
nodeAnimations: {
|
|
684
799
|
...prev.nodeAnimations,
|
|
@@ -687,7 +802,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
687
802
|
}));
|
|
688
803
|
|
|
689
804
|
setTimeout(() => {
|
|
690
|
-
setAnimationState(prev => {
|
|
805
|
+
setAnimationState((prev) => {
|
|
691
806
|
const newNodeAnimations = { ...prev.nodeAnimations };
|
|
692
807
|
delete newNodeAnimations[nodeId];
|
|
693
808
|
return { ...prev, nodeAnimations: newNodeAnimations };
|
|
@@ -708,7 +823,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
708
823
|
const layoutType = configuration.display?.layout || 'hierarchical';
|
|
709
824
|
const positioned = autoLayoutNodes(converted, [], layoutType);
|
|
710
825
|
|
|
711
|
-
return positioned.map(node => {
|
|
826
|
+
return positioned.map((node) => {
|
|
712
827
|
const animation = animationState.nodeAnimations[node.id];
|
|
713
828
|
// Apply any pending position changes
|
|
714
829
|
const pendingPosition = editStateRef.current.positionChanges.get(node.id);
|
|
@@ -718,17 +833,22 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
718
833
|
data: {
|
|
719
834
|
...node.data,
|
|
720
835
|
editable,
|
|
721
|
-
...(animation
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
836
|
+
...(animation
|
|
837
|
+
? {
|
|
838
|
+
animationType: animation.type,
|
|
839
|
+
animationDuration: animation.duration,
|
|
840
|
+
}
|
|
841
|
+
: {}),
|
|
725
842
|
} as CustomNodeData,
|
|
726
843
|
};
|
|
727
844
|
});
|
|
728
845
|
}, [nodes, configuration, violations, animationState.nodeAnimations, editable, editStateRef]);
|
|
729
846
|
|
|
730
847
|
const baseNodesKey = useMemo(() => {
|
|
731
|
-
return nodes
|
|
848
|
+
return nodes
|
|
849
|
+
.map((n) => n.id)
|
|
850
|
+
.sort()
|
|
851
|
+
.join(',');
|
|
732
852
|
}, [nodes]);
|
|
733
853
|
|
|
734
854
|
// Local xyflow nodes state for dragging
|
|
@@ -756,43 +876,48 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
756
876
|
const xyflowNodes = editable ? xyflowLocalNodes : xyflowNodesBase;
|
|
757
877
|
|
|
758
878
|
// Handle node changes (drag events)
|
|
759
|
-
const handleNodesChange = useCallback(
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
.filter(
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
879
|
+
const handleNodesChange = useCallback(
|
|
880
|
+
(changes: NodeChange[]) => {
|
|
881
|
+
if (!editable) return;
|
|
882
|
+
|
|
883
|
+
setXyflowLocalNodes((nds) => applyNodeChanges(changes, nds) as Node<CustomNodeData>[]);
|
|
884
|
+
|
|
885
|
+
// Track position changes on drag end
|
|
886
|
+
const positionChanges = changes.filter(
|
|
887
|
+
(
|
|
888
|
+
change
|
|
889
|
+
): change is NodeChange & {
|
|
890
|
+
type: 'position';
|
|
891
|
+
position: { x: number; y: number };
|
|
892
|
+
dragging: boolean;
|
|
893
|
+
} =>
|
|
894
|
+
change.type === 'position' &&
|
|
895
|
+
'position' in change &&
|
|
896
|
+
change.position !== undefined &&
|
|
897
|
+
'dragging' in change &&
|
|
898
|
+
change.dragging === false
|
|
776
899
|
);
|
|
777
900
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
901
|
+
if (positionChanges.length > 0) {
|
|
902
|
+
updateEditState((prev) => {
|
|
903
|
+
const newPositions = new Map(prev.positionChanges);
|
|
904
|
+
for (const change of positionChanges) {
|
|
905
|
+
newPositions.set(change.id, {
|
|
906
|
+
x: Math.round(change.position.x),
|
|
907
|
+
y: Math.round(change.position.y),
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
return { ...prev, positionChanges: newPositions };
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
},
|
|
914
|
+
[editable, updateEditState]
|
|
915
|
+
);
|
|
791
916
|
|
|
792
917
|
const xyflowEdges = useMemo(() => {
|
|
793
918
|
const converted = convertToXYFlowEdges(edges, configuration, violations);
|
|
794
919
|
|
|
795
|
-
return converted.map(edge => {
|
|
920
|
+
return converted.map((edge) => {
|
|
796
921
|
const animation = animationState.edgeAnimations[edge.id];
|
|
797
922
|
if (animation) {
|
|
798
923
|
return {
|
|
@@ -854,20 +979,19 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
854
979
|
panOnDrag
|
|
855
980
|
selectionOnDrag={false}
|
|
856
981
|
>
|
|
857
|
-
{showBackground && <Background color=
|
|
982
|
+
{showBackground && <Background color={theme.colors.border} gap={16} size={1} />}
|
|
858
983
|
{showControls && <Controls showZoom showFitView showInteractive />}
|
|
859
984
|
{showMinimap && (
|
|
860
985
|
<MiniMap
|
|
861
986
|
nodeColor={(node) => {
|
|
862
987
|
const nodeData = node.data as CustomNodeData;
|
|
863
|
-
return nodeData?.typeDefinition?.color ||
|
|
988
|
+
return nodeData?.typeDefinition?.color || theme.colors.secondary;
|
|
864
989
|
}}
|
|
865
990
|
nodeBorderRadius={2}
|
|
866
991
|
pannable
|
|
867
992
|
zoomable
|
|
868
993
|
/>
|
|
869
994
|
)}
|
|
870
|
-
|
|
871
995
|
</ReactFlow>
|
|
872
996
|
|
|
873
997
|
{selectedEdge && selectedEdgeTypeDefinition && (
|
|
@@ -899,22 +1023,24 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
899
1023
|
top: '50%',
|
|
900
1024
|
left: '50%',
|
|
901
1025
|
transform: 'translate(-50%, -50%)',
|
|
902
|
-
backgroundColor:
|
|
1026
|
+
backgroundColor: theme.colors.background,
|
|
1027
|
+
color: theme.colors.text,
|
|
903
1028
|
borderRadius: '8px',
|
|
904
1029
|
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
905
1030
|
padding: '16px',
|
|
906
1031
|
minWidth: '200px',
|
|
907
1032
|
zIndex: 1000,
|
|
1033
|
+
border: `1px solid ${theme.colors.border}`,
|
|
908
1034
|
}}
|
|
909
1035
|
>
|
|
910
1036
|
<div style={{ fontWeight: 'bold', marginBottom: '12px', fontSize: '14px' }}>
|
|
911
1037
|
Select Edge Type
|
|
912
1038
|
</div>
|
|
913
|
-
<div style={{ fontSize: '12px', color:
|
|
1039
|
+
<div style={{ fontSize: '12px', color: theme.colors.textSecondary, marginBottom: '12px' }}>
|
|
914
1040
|
{pendingConnection.from} → {pendingConnection.to}
|
|
915
1041
|
</div>
|
|
916
1042
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
917
|
-
{pendingConnection.validTypes.map(type => {
|
|
1043
|
+
{pendingConnection.validTypes.map((type) => {
|
|
918
1044
|
const typeDefinition = configuration.edgeTypes[type];
|
|
919
1045
|
return (
|
|
920
1046
|
<button
|
|
@@ -922,8 +1048,8 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
922
1048
|
onClick={() => handleEdgeTypeSelect(type)}
|
|
923
1049
|
style={{
|
|
924
1050
|
padding: '8px 12px',
|
|
925
|
-
backgroundColor: typeDefinition?.color ||
|
|
926
|
-
color:
|
|
1051
|
+
backgroundColor: typeDefinition?.color || theme.colors.secondary,
|
|
1052
|
+
color: theme.colors.background,
|
|
927
1053
|
border: 'none',
|
|
928
1054
|
borderRadius: '4px',
|
|
929
1055
|
cursor: 'pointer',
|
|
@@ -943,9 +1069,9 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
943
1069
|
marginTop: '12px',
|
|
944
1070
|
width: '100%',
|
|
945
1071
|
padding: '8px 12px',
|
|
946
|
-
backgroundColor:
|
|
947
|
-
color:
|
|
948
|
-
border:
|
|
1072
|
+
backgroundColor: theme.colors.surface,
|
|
1073
|
+
color: theme.colors.textSecondary,
|
|
1074
|
+
border: `1px solid ${theme.colors.border}`,
|
|
949
1075
|
borderRadius: '4px',
|
|
950
1076
|
cursor: 'pointer',
|
|
951
1077
|
fontSize: '12px',
|
|
@@ -962,7 +1088,10 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
962
1088
|
/**
|
|
963
1089
|
* Convert canvas to legacy configuration format for internal use
|
|
964
1090
|
*/
|
|
965
|
-
function useCanvasToLegacy(
|
|
1091
|
+
function useCanvasToLegacy(
|
|
1092
|
+
canvas: ExtendedCanvas | undefined,
|
|
1093
|
+
library?: ComponentLibrary
|
|
1094
|
+
): {
|
|
966
1095
|
configuration: GraphConfiguration;
|
|
967
1096
|
nodes: NodeState[];
|
|
968
1097
|
edges: EdgeState[];
|
|
@@ -980,6 +1109,7 @@ function useCanvasToLegacy(canvas: ExtendedCanvas | undefined, library?: Compone
|
|
|
980
1109
|
if (library?.nodeComponents) {
|
|
981
1110
|
for (const [id, component] of Object.entries(library.nodeComponents)) {
|
|
982
1111
|
nodeTypes[id] = {
|
|
1112
|
+
description: component.description,
|
|
983
1113
|
shape: component.shape || 'rectangle',
|
|
984
1114
|
icon: component.icon,
|
|
985
1115
|
color: component.color,
|
|
@@ -1008,6 +1138,7 @@ function useCanvasToLegacy(canvas: ExtendedCanvas | undefined, library?: Compone
|
|
|
1008
1138
|
if (canvas.pv?.nodeTypes) {
|
|
1009
1139
|
for (const [id, def] of Object.entries(canvas.pv.nodeTypes)) {
|
|
1010
1140
|
nodeTypes[id] = {
|
|
1141
|
+
description: def.description,
|
|
1011
1142
|
shape: def.shape || 'rectangle',
|
|
1012
1143
|
icon: def.icon,
|
|
1013
1144
|
color: def.color,
|
|
@@ -1023,11 +1154,23 @@ function useCanvasToLegacy(canvas: ExtendedCanvas | undefined, library?: Compone
|
|
|
1023
1154
|
|
|
1024
1155
|
if (!nodeTypes[nodeType]) {
|
|
1025
1156
|
// Color priority: vv.fill > node.color > vv.states.idle.color
|
|
1026
|
-
const fillColor =
|
|
1027
|
-
||
|
|
1028
|
-
|
|
1157
|
+
const fillColor =
|
|
1158
|
+
vv?.fill ||
|
|
1159
|
+
(typeof node.color === 'string' ? node.color : undefined) ||
|
|
1160
|
+
vv?.states?.idle?.color;
|
|
1161
|
+
|
|
1162
|
+
// Derive description from text content (everything after line 1) for text nodes
|
|
1163
|
+
let nodeDescription = `${nodeType} node`;
|
|
1164
|
+
if (node.type === 'text' && 'text' in node) {
|
|
1165
|
+
const lines = node.text.split('\n');
|
|
1166
|
+
const descFromText = lines.slice(1).join('\n').trim();
|
|
1167
|
+
if (descFromText) {
|
|
1168
|
+
nodeDescription = descFromText;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1029
1171
|
|
|
1030
1172
|
nodeTypes[nodeType] = {
|
|
1173
|
+
description: nodeDescription,
|
|
1031
1174
|
shape: vv?.shape || 'rectangle',
|
|
1032
1175
|
icon: vv?.icon,
|
|
1033
1176
|
color: fillColor,
|
|
@@ -1070,8 +1213,8 @@ function useCanvasToLegacy(canvas: ExtendedCanvas | undefined, library?: Compone
|
|
|
1070
1213
|
}
|
|
1071
1214
|
|
|
1072
1215
|
// Find node types for from/to
|
|
1073
|
-
const fromNode = canvas.nodes?.find(n => n.id === edge.fromNode);
|
|
1074
|
-
const toNode = canvas.nodes?.find(n => n.id === edge.toNode);
|
|
1216
|
+
const fromNode = canvas.nodes?.find((n) => n.id === edge.fromNode);
|
|
1217
|
+
const toNode = canvas.nodes?.find((n) => n.id === edge.toNode);
|
|
1075
1218
|
const fromType = fromNode?.pv?.nodeType || edge.fromNode;
|
|
1076
1219
|
const toType = toNode?.pv?.nodeType || edge.toNode;
|
|
1077
1220
|
|
|
@@ -1130,66 +1273,87 @@ function useCanvasToLegacy(canvas: ExtendedCanvas | undefined, library?: Compone
|
|
|
1130
1273
|
*/
|
|
1131
1274
|
export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>((props, ref) => {
|
|
1132
1275
|
const { canvas, library, className, width = '100%', height = '100%' } = props;
|
|
1276
|
+
const { theme } = useTheme();
|
|
1133
1277
|
|
|
1134
1278
|
// Convert canvas to internal format (merging library types if provided)
|
|
1135
1279
|
const canvasData = useCanvasToLegacy(canvas, library);
|
|
1136
1280
|
|
|
1281
|
+
// Internal edit state ref - must be before any conditional returns
|
|
1282
|
+
const editStateRef = useRef<EditState>(createEmptyEditState());
|
|
1283
|
+
|
|
1284
|
+
// Expose imperative handle - must be before any conditional returns
|
|
1285
|
+
useImperativeHandle(
|
|
1286
|
+
ref,
|
|
1287
|
+
() => ({
|
|
1288
|
+
getPendingChanges: (): PendingChanges => {
|
|
1289
|
+
const state = editStateRef.current;
|
|
1290
|
+
return {
|
|
1291
|
+
positionChanges: Array.from(state.positionChanges.entries()).map(
|
|
1292
|
+
([nodeId, position]) => ({
|
|
1293
|
+
nodeId,
|
|
1294
|
+
position,
|
|
1295
|
+
})
|
|
1296
|
+
),
|
|
1297
|
+
nodeUpdates: Array.from(state.nodeUpdates.entries()).map(([nodeId, updates]) => ({
|
|
1298
|
+
nodeId,
|
|
1299
|
+
updates,
|
|
1300
|
+
})),
|
|
1301
|
+
deletedNodeIds: Array.from(state.deletedNodeIds),
|
|
1302
|
+
createdEdges: state.createdEdges.map((e) => ({
|
|
1303
|
+
from: e.from,
|
|
1304
|
+
to: e.to,
|
|
1305
|
+
type: e.type,
|
|
1306
|
+
sourceHandle: e.sourceHandle,
|
|
1307
|
+
targetHandle: e.targetHandle,
|
|
1308
|
+
})),
|
|
1309
|
+
deletedEdges: state.deletedEdges.map((e) => ({ from: e.from, to: e.to, type: e.type })),
|
|
1310
|
+
hasChanges:
|
|
1311
|
+
state.positionChanges.size > 0 ||
|
|
1312
|
+
state.nodeUpdates.size > 0 ||
|
|
1313
|
+
state.deletedNodeIds.size > 0 ||
|
|
1314
|
+
state.createdEdges.length > 0 ||
|
|
1315
|
+
state.deletedEdges.length > 0,
|
|
1316
|
+
};
|
|
1317
|
+
},
|
|
1318
|
+
resetEditState: () => {
|
|
1319
|
+
editStateRef.current = createEmptyEditState();
|
|
1320
|
+
},
|
|
1321
|
+
hasUnsavedChanges: (): boolean => {
|
|
1322
|
+
const state = editStateRef.current;
|
|
1323
|
+
return (
|
|
1324
|
+
state.positionChanges.size > 0 ||
|
|
1325
|
+
state.nodeUpdates.size > 0 ||
|
|
1326
|
+
state.deletedNodeIds.size > 0 ||
|
|
1327
|
+
state.createdEdges.length > 0 ||
|
|
1328
|
+
state.deletedEdges.length > 0
|
|
1329
|
+
);
|
|
1330
|
+
},
|
|
1331
|
+
}),
|
|
1332
|
+
[]
|
|
1333
|
+
);
|
|
1334
|
+
|
|
1137
1335
|
// Validate we have required data
|
|
1138
1336
|
if (!canvasData) {
|
|
1139
1337
|
return (
|
|
1140
|
-
<div
|
|
1141
|
-
|
|
1338
|
+
<div
|
|
1339
|
+
className={className}
|
|
1340
|
+
style={{
|
|
1341
|
+
width,
|
|
1342
|
+
height,
|
|
1343
|
+
display: 'flex',
|
|
1344
|
+
alignItems: 'center',
|
|
1345
|
+
justifyContent: 'center',
|
|
1346
|
+
backgroundColor: theme.colors.background,
|
|
1347
|
+
color: theme.colors.textSecondary,
|
|
1348
|
+
}}
|
|
1349
|
+
>
|
|
1350
|
+
<p>No canvas data provided.</p>
|
|
1142
1351
|
</div>
|
|
1143
1352
|
);
|
|
1144
1353
|
}
|
|
1145
1354
|
|
|
1146
1355
|
const { configuration, nodes, edges } = canvasData;
|
|
1147
1356
|
|
|
1148
|
-
// Internal edit state ref
|
|
1149
|
-
const editStateRef = useRef<EditState>(createEmptyEditState());
|
|
1150
|
-
|
|
1151
|
-
// Expose imperative handle
|
|
1152
|
-
useImperativeHandle(ref, () => ({
|
|
1153
|
-
getPendingChanges: (): PendingChanges => {
|
|
1154
|
-
const state = editStateRef.current;
|
|
1155
|
-
return {
|
|
1156
|
-
positionChanges: Array.from(state.positionChanges.entries()).map(([nodeId, position]) => ({
|
|
1157
|
-
nodeId,
|
|
1158
|
-
position,
|
|
1159
|
-
})),
|
|
1160
|
-
nodeUpdates: Array.from(state.nodeUpdates.entries()).map(([nodeId, updates]) => ({
|
|
1161
|
-
nodeId,
|
|
1162
|
-
updates,
|
|
1163
|
-
})),
|
|
1164
|
-
deletedNodeIds: Array.from(state.deletedNodeIds),
|
|
1165
|
-
createdEdges: state.createdEdges.map(e => ({
|
|
1166
|
-
from: e.from,
|
|
1167
|
-
to: e.to,
|
|
1168
|
-
type: e.type,
|
|
1169
|
-
sourceHandle: e.sourceHandle,
|
|
1170
|
-
targetHandle: e.targetHandle,
|
|
1171
|
-
})),
|
|
1172
|
-
deletedEdges: state.deletedEdges.map(e => ({ from: e.from, to: e.to, type: e.type })),
|
|
1173
|
-
hasChanges: state.positionChanges.size > 0 ||
|
|
1174
|
-
state.nodeUpdates.size > 0 ||
|
|
1175
|
-
state.deletedNodeIds.size > 0 ||
|
|
1176
|
-
state.createdEdges.length > 0 ||
|
|
1177
|
-
state.deletedEdges.length > 0,
|
|
1178
|
-
};
|
|
1179
|
-
},
|
|
1180
|
-
resetEditState: () => {
|
|
1181
|
-
editStateRef.current = createEmptyEditState();
|
|
1182
|
-
},
|
|
1183
|
-
hasUnsavedChanges: (): boolean => {
|
|
1184
|
-
const state = editStateRef.current;
|
|
1185
|
-
return state.positionChanges.size > 0 ||
|
|
1186
|
-
state.nodeUpdates.size > 0 ||
|
|
1187
|
-
state.deletedNodeIds.size > 0 ||
|
|
1188
|
-
state.createdEdges.length > 0 ||
|
|
1189
|
-
state.deletedEdges.length > 0;
|
|
1190
|
-
},
|
|
1191
|
-
}), []);
|
|
1192
|
-
|
|
1193
1357
|
// Extract only the props that inner component needs
|
|
1194
1358
|
const {
|
|
1195
1359
|
violations,
|