@principal-ai/principal-view-react 0.6.6
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 +111 -0
- package/dist/components/ConfigurationSelector.d.ts +37 -0
- package/dist/components/ConfigurationSelector.d.ts.map +1 -0
- package/dist/components/ConfigurationSelector.js +67 -0
- package/dist/components/ConfigurationSelector.js.map +1 -0
- package/dist/components/EdgeInfoPanel.d.ts +16 -0
- package/dist/components/EdgeInfoPanel.d.ts.map +1 -0
- package/dist/components/EdgeInfoPanel.js +85 -0
- package/dist/components/EdgeInfoPanel.js.map +1 -0
- package/dist/components/EventLog.d.ts +20 -0
- package/dist/components/EventLog.d.ts.map +1 -0
- package/dist/components/EventLog.js +13 -0
- package/dist/components/EventLog.js.map +1 -0
- package/dist/components/EventLog.test.d.ts +2 -0
- package/dist/components/EventLog.test.d.ts.map +1 -0
- package/dist/components/EventLog.test.js +73 -0
- package/dist/components/EventLog.test.js.map +1 -0
- package/dist/components/GraphRenderer.d.ts +121 -0
- package/dist/components/GraphRenderer.d.ts.map +1 -0
- package/dist/components/GraphRenderer.js +809 -0
- package/dist/components/GraphRenderer.js.map +1 -0
- package/dist/components/GraphRenderer.test.d.ts +2 -0
- package/dist/components/GraphRenderer.test.d.ts.map +1 -0
- package/dist/components/GraphRenderer.test.js +88 -0
- package/dist/components/GraphRenderer.test.js.map +1 -0
- package/dist/components/MetricsDashboard.d.ts +14 -0
- package/dist/components/MetricsDashboard.d.ts.map +1 -0
- package/dist/components/MetricsDashboard.js +13 -0
- package/dist/components/MetricsDashboard.js.map +1 -0
- package/dist/components/NodeInfoPanel.d.ts +21 -0
- package/dist/components/NodeInfoPanel.d.ts.map +1 -0
- package/dist/components/NodeInfoPanel.js +217 -0
- package/dist/components/NodeInfoPanel.js.map +1 -0
- package/dist/edges/CustomEdge.d.ts +16 -0
- package/dist/edges/CustomEdge.d.ts.map +1 -0
- package/dist/edges/CustomEdge.js +200 -0
- package/dist/edges/CustomEdge.js.map +1 -0
- package/dist/edges/GenericEdge.d.ts +18 -0
- package/dist/edges/GenericEdge.d.ts.map +1 -0
- package/dist/edges/GenericEdge.js +14 -0
- package/dist/edges/GenericEdge.js.map +1 -0
- package/dist/hooks/usePathBasedEvents.d.ts +42 -0
- package/dist/hooks/usePathBasedEvents.d.ts.map +1 -0
- package/dist/hooks/usePathBasedEvents.js +122 -0
- package/dist/hooks/usePathBasedEvents.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes/CustomNode.d.ts +18 -0
- package/dist/nodes/CustomNode.d.ts.map +1 -0
- package/dist/nodes/CustomNode.js +298 -0
- package/dist/nodes/CustomNode.js.map +1 -0
- package/dist/nodes/GenericNode.d.ts +20 -0
- package/dist/nodes/GenericNode.d.ts.map +1 -0
- package/dist/nodes/GenericNode.js +24 -0
- package/dist/nodes/GenericNode.js.map +1 -0
- package/dist/utils/animationMapping.d.ts +53 -0
- package/dist/utils/animationMapping.d.ts.map +1 -0
- package/dist/utils/animationMapping.js +133 -0
- package/dist/utils/animationMapping.js.map +1 -0
- package/dist/utils/graphConverter.d.ts +22 -0
- package/dist/utils/graphConverter.d.ts.map +1 -0
- package/dist/utils/graphConverter.js +176 -0
- package/dist/utils/graphConverter.js.map +1 -0
- package/dist/utils/iconResolver.d.ts +29 -0
- package/dist/utils/iconResolver.d.ts.map +1 -0
- package/dist/utils/iconResolver.js +68 -0
- package/dist/utils/iconResolver.js.map +1 -0
- package/package.json +61 -0
- package/src/components/ConfigurationSelector.tsx +147 -0
- package/src/components/EdgeInfoPanel.tsx +198 -0
- package/src/components/EventLog.test.tsx +85 -0
- package/src/components/EventLog.tsx +51 -0
- package/src/components/GraphRenderer.test.tsx +118 -0
- package/src/components/GraphRenderer.tsx +1222 -0
- package/src/components/MetricsDashboard.tsx +40 -0
- package/src/components/NodeInfoPanel.tsx +425 -0
- package/src/edges/CustomEdge.tsx +344 -0
- package/src/edges/GenericEdge.tsx +40 -0
- package/src/hooks/usePathBasedEvents.ts +182 -0
- package/src/index.ts +67 -0
- package/src/nodes/CustomNode.tsx +432 -0
- package/src/nodes/GenericNode.tsx +54 -0
- package/src/stories/AnimationWorkshop.stories.tsx +608 -0
- package/src/stories/EventDrivenAnimations.stories.tsx +499 -0
- package/src/stories/EventLog.stories.tsx +161 -0
- package/src/stories/GraphRenderer.stories.tsx +628 -0
- package/src/stories/Introduction.mdx +51 -0
- package/src/stories/MetricsDashboard.stories.tsx +227 -0
- package/src/stories/MultiConfig.stories.tsx +531 -0
- package/src/stories/MultiDirectionalConnections.stories.tsx +345 -0
- package/src/stories/NodeShapes.stories.tsx +769 -0
- package/src/utils/animationMapping.ts +170 -0
- package/src/utils/graphConverter.ts +218 -0
- package/src/utils/iconResolver.tsx +49 -0
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GraphRenderer = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const react_2 = require("@xyflow/react");
|
|
7
|
+
require("@xyflow/react/dist/style.css");
|
|
8
|
+
const principal_view_core_1 = require("@principal-ai/principal-view-core");
|
|
9
|
+
const CustomNode_1 = require("../nodes/CustomNode");
|
|
10
|
+
const CustomEdge_1 = require("../edges/CustomEdge");
|
|
11
|
+
const graphConverter_1 = require("../utils/graphConverter");
|
|
12
|
+
const EdgeInfoPanel_1 = require("./EdgeInfoPanel");
|
|
13
|
+
const NodeInfoPanel_1 = require("./NodeInfoPanel");
|
|
14
|
+
// Define custom node types
|
|
15
|
+
const nodeTypes = {
|
|
16
|
+
custom: CustomNode_1.CustomNode,
|
|
17
|
+
};
|
|
18
|
+
// Define custom edge types
|
|
19
|
+
const edgeTypes = {
|
|
20
|
+
custom: CustomEdge_1.CustomEdge,
|
|
21
|
+
};
|
|
22
|
+
const createEmptyEditState = () => ({
|
|
23
|
+
positionChanges: new Map(),
|
|
24
|
+
nodeUpdates: new Map(),
|
|
25
|
+
deletedNodeIds: new Set(),
|
|
26
|
+
createdEdges: [],
|
|
27
|
+
deletedEdges: [],
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Inner component that uses ReactFlow hooks
|
|
31
|
+
*/
|
|
32
|
+
const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges, violations = [], configName: _configName, showMinimap = true, showControls = true, showBackground = true, events = [], onEventProcessed, editable = false, onPendingChangesChange, onEditStateChange, editStateRef, }) => {
|
|
33
|
+
const { fitView } = (0, react_2.useReactFlow)();
|
|
34
|
+
// Track active animations
|
|
35
|
+
const [animationState, setAnimationState] = (0, react_1.useState)({
|
|
36
|
+
nodeAnimations: {},
|
|
37
|
+
edgeAnimations: {},
|
|
38
|
+
});
|
|
39
|
+
// Track selected edge for info panel
|
|
40
|
+
const [selectedEdgeId, setSelectedEdgeId] = (0, react_1.useState)(null);
|
|
41
|
+
// Track selected node for info panel
|
|
42
|
+
const [selectedNodeId, setSelectedNodeId] = (0, react_1.useState)(null);
|
|
43
|
+
// Track pending connection for edge type picker
|
|
44
|
+
const [pendingConnection, setPendingConnection] = (0, react_1.useState)(null);
|
|
45
|
+
// ============================================
|
|
46
|
+
// INTERNAL EDIT STATE
|
|
47
|
+
// ============================================
|
|
48
|
+
// Local copies of nodes and edges for editing
|
|
49
|
+
const [localNodes, setLocalNodes] = (0, react_1.useState)(propNodes);
|
|
50
|
+
const [localEdges, setLocalEdges] = (0, react_1.useState)(propEdges);
|
|
51
|
+
// Track the prop values to detect external changes
|
|
52
|
+
const propNodesKeyRef = (0, react_1.useRef)(propNodes.map(n => n.id).sort().join(','));
|
|
53
|
+
const propEdgesKeyRef = (0, react_1.useRef)(propEdges.map(e => e.id).sort().join(','));
|
|
54
|
+
// Sync local state with props when props change (e.g., config reload)
|
|
55
|
+
// This only happens when the structure changes, not during editing
|
|
56
|
+
(0, react_1.useEffect)(() => {
|
|
57
|
+
const newNodesKey = propNodes.map(n => n.id).sort().join(',');
|
|
58
|
+
const newEdgesKey = propEdges.map(e => e.id).sort().join(',');
|
|
59
|
+
if (newNodesKey !== propNodesKeyRef.current || newEdgesKey !== propEdgesKeyRef.current) {
|
|
60
|
+
propNodesKeyRef.current = newNodesKey;
|
|
61
|
+
propEdgesKeyRef.current = newEdgesKey;
|
|
62
|
+
setLocalNodes(propNodes);
|
|
63
|
+
setLocalEdges(propEdges);
|
|
64
|
+
// Reset edit state when props change
|
|
65
|
+
editStateRef.current = createEmptyEditState();
|
|
66
|
+
onEditStateChange?.(editStateRef.current);
|
|
67
|
+
onPendingChangesChange?.(false);
|
|
68
|
+
}
|
|
69
|
+
}, [propNodes, propEdges, editStateRef, onEditStateChange, onPendingChangesChange]);
|
|
70
|
+
// Use local state when editable, props when not
|
|
71
|
+
const nodes = editable ? localNodes : propNodes;
|
|
72
|
+
const edges = editable ? localEdges : propEdges;
|
|
73
|
+
// Helper to check if there are pending changes
|
|
74
|
+
const checkHasChanges = (0, react_1.useCallback)((state) => {
|
|
75
|
+
return state.positionChanges.size > 0 ||
|
|
76
|
+
state.nodeUpdates.size > 0 ||
|
|
77
|
+
state.deletedNodeIds.size > 0 ||
|
|
78
|
+
state.createdEdges.length > 0 ||
|
|
79
|
+
state.deletedEdges.length > 0;
|
|
80
|
+
}, []);
|
|
81
|
+
// Helper to update edit state and notify parent
|
|
82
|
+
const updateEditState = (0, react_1.useCallback)((updater) => {
|
|
83
|
+
const newState = updater(editStateRef.current);
|
|
84
|
+
editStateRef.current = newState;
|
|
85
|
+
onEditStateChange?.(newState);
|
|
86
|
+
onPendingChangesChange?.(checkHasChanges(newState));
|
|
87
|
+
}, [editStateRef, onEditStateChange, onPendingChangesChange, checkHasChanges]);
|
|
88
|
+
// ============================================
|
|
89
|
+
// EVENT HANDLERS
|
|
90
|
+
// ============================================
|
|
91
|
+
// Handle edge click
|
|
92
|
+
const onEdgeClick = (0, react_1.useCallback)((_event, edge) => {
|
|
93
|
+
setSelectedEdgeId(edge.id);
|
|
94
|
+
setSelectedNodeId(null);
|
|
95
|
+
}, []);
|
|
96
|
+
// Handle node click
|
|
97
|
+
const onNodeClick = (0, react_1.useCallback)((_event, node) => {
|
|
98
|
+
setSelectedNodeId(node.id);
|
|
99
|
+
setSelectedEdgeId(null);
|
|
100
|
+
}, []);
|
|
101
|
+
// Handle close edge info panel
|
|
102
|
+
const onCloseEdgeInfoPanel = (0, react_1.useCallback)(() => {
|
|
103
|
+
setSelectedEdgeId(null);
|
|
104
|
+
}, []);
|
|
105
|
+
// Handle close node info panel
|
|
106
|
+
const onCloseNodeInfoPanel = (0, react_1.useCallback)(() => {
|
|
107
|
+
setSelectedNodeId(null);
|
|
108
|
+
}, []);
|
|
109
|
+
// Handle node update (internal - updates local state only)
|
|
110
|
+
const handleNodeUpdate = (0, react_1.useCallback)((nodeId, updates) => {
|
|
111
|
+
if (!editable)
|
|
112
|
+
return;
|
|
113
|
+
// Update local nodes
|
|
114
|
+
setLocalNodes(prev => prev.map(node => {
|
|
115
|
+
if (node.id === nodeId) {
|
|
116
|
+
return {
|
|
117
|
+
...node,
|
|
118
|
+
type: updates.type ?? node.type,
|
|
119
|
+
data: updates.data ? { ...node.data, ...updates.data } : node.data,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return node;
|
|
123
|
+
}));
|
|
124
|
+
// Track the change
|
|
125
|
+
updateEditState(prev => {
|
|
126
|
+
const newUpdates = new Map(prev.nodeUpdates);
|
|
127
|
+
const existing = newUpdates.get(nodeId) || {};
|
|
128
|
+
newUpdates.set(nodeId, {
|
|
129
|
+
type: updates.type ?? existing.type,
|
|
130
|
+
data: updates.data ? { ...existing.data, ...updates.data } : existing.data,
|
|
131
|
+
});
|
|
132
|
+
return { ...prev, nodeUpdates: newUpdates };
|
|
133
|
+
});
|
|
134
|
+
}, [editable, updateEditState]);
|
|
135
|
+
// Handle node delete (internal)
|
|
136
|
+
const handleNodeDelete = (0, react_1.useCallback)((nodeId) => {
|
|
137
|
+
if (!editable)
|
|
138
|
+
return;
|
|
139
|
+
// Remove from local state
|
|
140
|
+
setLocalNodes(prev => prev.filter(n => n.id !== nodeId));
|
|
141
|
+
setLocalEdges(prev => prev.filter(e => e.from !== nodeId && e.to !== nodeId));
|
|
142
|
+
// Track the change
|
|
143
|
+
updateEditState(prev => {
|
|
144
|
+
const newDeletedNodes = new Set(prev.deletedNodeIds);
|
|
145
|
+
newDeletedNodes.add(nodeId);
|
|
146
|
+
// Remove any pending updates for this node
|
|
147
|
+
const newUpdates = new Map(prev.nodeUpdates);
|
|
148
|
+
newUpdates.delete(nodeId);
|
|
149
|
+
// Remove any position changes for this node
|
|
150
|
+
const newPositions = new Map(prev.positionChanges);
|
|
151
|
+
newPositions.delete(nodeId);
|
|
152
|
+
// Remove created edges that involve this node
|
|
153
|
+
const newCreatedEdges = prev.createdEdges.filter(e => e.from !== nodeId && e.to !== nodeId);
|
|
154
|
+
return {
|
|
155
|
+
...prev,
|
|
156
|
+
deletedNodeIds: newDeletedNodes,
|
|
157
|
+
nodeUpdates: newUpdates,
|
|
158
|
+
positionChanges: newPositions,
|
|
159
|
+
createdEdges: newCreatedEdges,
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
setSelectedNodeId(null);
|
|
163
|
+
}, [editable, updateEditState]);
|
|
164
|
+
// Handle edge delete (internal)
|
|
165
|
+
const handleEdgeDelete = (0, react_1.useCallback)((edgeId) => {
|
|
166
|
+
if (!editable)
|
|
167
|
+
return;
|
|
168
|
+
// Find the edge before removing it so we can track its full info
|
|
169
|
+
const edgeToDelete = localEdges.find(e => e.id === edgeId);
|
|
170
|
+
// Remove from local state
|
|
171
|
+
setLocalEdges(prev => prev.filter(e => e.id !== edgeId));
|
|
172
|
+
// Track the change
|
|
173
|
+
updateEditState(prev => {
|
|
174
|
+
// Check if this was a newly created edge
|
|
175
|
+
const createdEdgeIndex = prev.createdEdges.findIndex(e => e.id === edgeId);
|
|
176
|
+
if (createdEdgeIndex >= 0) {
|
|
177
|
+
// Just remove it from created edges
|
|
178
|
+
const newCreatedEdges = [...prev.createdEdges];
|
|
179
|
+
newCreatedEdges.splice(createdEdgeIndex, 1);
|
|
180
|
+
return { ...prev, createdEdges: newCreatedEdges };
|
|
181
|
+
}
|
|
182
|
+
// Otherwise mark as deleted with full edge info
|
|
183
|
+
if (edgeToDelete) {
|
|
184
|
+
const newDeletedEdges = [...prev.deletedEdges, {
|
|
185
|
+
id: edgeId,
|
|
186
|
+
from: edgeToDelete.from,
|
|
187
|
+
to: edgeToDelete.to,
|
|
188
|
+
type: edgeToDelete.type
|
|
189
|
+
}];
|
|
190
|
+
return { ...prev, deletedEdges: newDeletedEdges };
|
|
191
|
+
}
|
|
192
|
+
return prev;
|
|
193
|
+
});
|
|
194
|
+
setSelectedEdgeId(null);
|
|
195
|
+
}, [editable, updateEditState, localEdges]);
|
|
196
|
+
// Handle new connection from drag
|
|
197
|
+
const handleConnect = (0, react_1.useCallback)((connection) => {
|
|
198
|
+
if (!editable || !connection.source || !connection.target)
|
|
199
|
+
return;
|
|
200
|
+
// Find source and target node types
|
|
201
|
+
const sourceNode = nodes.find(n => n.id === connection.source);
|
|
202
|
+
const targetNode = nodes.find(n => n.id === connection.target);
|
|
203
|
+
if (!sourceNode || !targetNode)
|
|
204
|
+
return;
|
|
205
|
+
// Find valid edge types for this connection
|
|
206
|
+
const validTypes = configuration.allowedConnections
|
|
207
|
+
.filter(ac => ac.from === sourceNode.type && ac.to === targetNode.type)
|
|
208
|
+
.map(ac => ac.via);
|
|
209
|
+
const uniqueTypes = [...new Set(validTypes)];
|
|
210
|
+
if (uniqueTypes.length === 0) {
|
|
211
|
+
console.warn(`No valid edge types for connection from ${sourceNode.type} to ${targetNode.type}`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (uniqueTypes.length === 1) {
|
|
215
|
+
// Create edge immediately with handle information
|
|
216
|
+
createEdge(connection.source, connection.target, uniqueTypes[0], connection.sourceHandle ?? undefined, connection.targetHandle ?? undefined);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// Show picker
|
|
220
|
+
setPendingConnection({
|
|
221
|
+
from: connection.source,
|
|
222
|
+
to: connection.target,
|
|
223
|
+
sourceHandle: connection.sourceHandle ?? undefined,
|
|
224
|
+
targetHandle: connection.targetHandle ?? undefined,
|
|
225
|
+
validTypes: uniqueTypes,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}, [editable, nodes, configuration.allowedConnections]);
|
|
229
|
+
// Create edge helper
|
|
230
|
+
const createEdge = (0, react_1.useCallback)((from, to, type, sourceHandle, targetHandle) => {
|
|
231
|
+
const edgeId = `${from}-${to}-${type}-${Date.now()}`;
|
|
232
|
+
// Add to local state with handle information
|
|
233
|
+
const newEdge = {
|
|
234
|
+
id: edgeId,
|
|
235
|
+
type,
|
|
236
|
+
from,
|
|
237
|
+
to,
|
|
238
|
+
data: {},
|
|
239
|
+
createdAt: Date.now(),
|
|
240
|
+
updatedAt: Date.now(),
|
|
241
|
+
sourceHandle,
|
|
242
|
+
targetHandle,
|
|
243
|
+
};
|
|
244
|
+
setLocalEdges(prev => [...prev, newEdge]);
|
|
245
|
+
// Track the change
|
|
246
|
+
updateEditState(prev => ({
|
|
247
|
+
...prev,
|
|
248
|
+
createdEdges: [...prev.createdEdges, { id: edgeId, from, to, type, sourceHandle, targetHandle }],
|
|
249
|
+
}));
|
|
250
|
+
}, [updateEditState]);
|
|
251
|
+
// Handle edge type selection from picker
|
|
252
|
+
const handleEdgeTypeSelect = (0, react_1.useCallback)((type) => {
|
|
253
|
+
if (!pendingConnection)
|
|
254
|
+
return;
|
|
255
|
+
createEdge(pendingConnection.from, pendingConnection.to, type, pendingConnection.sourceHandle, pendingConnection.targetHandle);
|
|
256
|
+
setPendingConnection(null);
|
|
257
|
+
}, [pendingConnection, createEdge]);
|
|
258
|
+
// Cancel edge type picker
|
|
259
|
+
const handleCancelEdgeTypePicker = (0, react_1.useCallback)(() => {
|
|
260
|
+
setPendingConnection(null);
|
|
261
|
+
}, []);
|
|
262
|
+
// Track whether reconnection succeeded
|
|
263
|
+
const edgeReconnectSuccessful = (0, react_1.useRef)(true);
|
|
264
|
+
// Called when user starts dragging an edge endpoint
|
|
265
|
+
const handleReconnectStart = (0, react_1.useCallback)(() => {
|
|
266
|
+
edgeReconnectSuccessful.current = false;
|
|
267
|
+
}, []);
|
|
268
|
+
// Handle edge reconnection (dragging edge endpoint to new node)
|
|
269
|
+
const handleReconnect = (0, react_1.useCallback)((oldEdge, newConnection) => {
|
|
270
|
+
if (!editable || !newConnection.source || !newConnection.target)
|
|
271
|
+
return;
|
|
272
|
+
// Find the original edge in our local state
|
|
273
|
+
const originalEdge = localEdges.find(e => e.id === oldEdge.id);
|
|
274
|
+
if (!originalEdge)
|
|
275
|
+
return;
|
|
276
|
+
// Find source and target node types for validation
|
|
277
|
+
const sourceNode = nodes.find(n => n.id === newConnection.source);
|
|
278
|
+
const targetNode = nodes.find(n => n.id === newConnection.target);
|
|
279
|
+
if (!sourceNode || !targetNode)
|
|
280
|
+
return;
|
|
281
|
+
// Check if the new connection is valid for this edge type
|
|
282
|
+
const isValidConnection = configuration.allowedConnections.some(ac => ac.from === sourceNode.type && ac.to === targetNode.type && ac.via === originalEdge.type);
|
|
283
|
+
if (!isValidConnection) {
|
|
284
|
+
console.warn(`Cannot reconnect: ${originalEdge.type} edge not allowed from ${sourceNode.type} to ${targetNode.type}`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// Mark as successful before updating
|
|
288
|
+
edgeReconnectSuccessful.current = true;
|
|
289
|
+
// Update local edges - manually update the edge to preserve its type and id
|
|
290
|
+
setLocalEdges(prev => prev.map(edge => {
|
|
291
|
+
if (edge.id === oldEdge.id) {
|
|
292
|
+
return {
|
|
293
|
+
...edge,
|
|
294
|
+
from: newConnection.source,
|
|
295
|
+
to: newConnection.target,
|
|
296
|
+
sourceHandle: newConnection.sourceHandle ?? undefined,
|
|
297
|
+
targetHandle: newConnection.targetHandle ?? undefined,
|
|
298
|
+
updatedAt: Date.now(),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return edge;
|
|
302
|
+
}));
|
|
303
|
+
// Track the change - remove old edge and add new one
|
|
304
|
+
updateEditState(prev => {
|
|
305
|
+
// Check if this was a newly created edge
|
|
306
|
+
const createdEdgeIndex = prev.createdEdges.findIndex(e => e.id === oldEdge.id);
|
|
307
|
+
if (createdEdgeIndex >= 0) {
|
|
308
|
+
// Update the created edge entry
|
|
309
|
+
const newCreatedEdges = [...prev.createdEdges];
|
|
310
|
+
newCreatedEdges[createdEdgeIndex] = {
|
|
311
|
+
...newCreatedEdges[createdEdgeIndex],
|
|
312
|
+
from: newConnection.source,
|
|
313
|
+
to: newConnection.target,
|
|
314
|
+
sourceHandle: newConnection.sourceHandle ?? undefined,
|
|
315
|
+
targetHandle: newConnection.targetHandle ?? undefined,
|
|
316
|
+
};
|
|
317
|
+
return { ...prev, createdEdges: newCreatedEdges };
|
|
318
|
+
}
|
|
319
|
+
// For existing edges, track as delete + create
|
|
320
|
+
const newDeletedEdges = [...prev.deletedEdges, {
|
|
321
|
+
id: oldEdge.id,
|
|
322
|
+
from: originalEdge.from,
|
|
323
|
+
to: originalEdge.to,
|
|
324
|
+
type: originalEdge.type,
|
|
325
|
+
}];
|
|
326
|
+
const newCreatedEdges = [...prev.createdEdges, {
|
|
327
|
+
id: oldEdge.id,
|
|
328
|
+
from: newConnection.source,
|
|
329
|
+
to: newConnection.target,
|
|
330
|
+
type: originalEdge.type,
|
|
331
|
+
sourceHandle: newConnection.sourceHandle ?? undefined,
|
|
332
|
+
targetHandle: newConnection.targetHandle ?? undefined,
|
|
333
|
+
}];
|
|
334
|
+
return { ...prev, deletedEdges: newDeletedEdges, createdEdges: newCreatedEdges };
|
|
335
|
+
});
|
|
336
|
+
}, [editable, localEdges, nodes, configuration.allowedConnections, updateEditState]);
|
|
337
|
+
// Called when reconnection ends (whether successful or not)
|
|
338
|
+
const handleReconnectEnd = (0, react_1.useCallback)(() => {
|
|
339
|
+
// If reconnection wasn't successful, the edge was dropped in empty space
|
|
340
|
+
// We need to keep the original edge (do nothing, it's still in localEdges)
|
|
341
|
+
// Edge is still in localEdges, no action needed - ReactFlow will re-render with it
|
|
342
|
+
edgeReconnectSuccessful.current = true;
|
|
343
|
+
}, []);
|
|
344
|
+
// ============================================
|
|
345
|
+
// SELECTED ITEMS
|
|
346
|
+
// ============================================
|
|
347
|
+
const selectedEdge = (0, react_1.useMemo)(() => {
|
|
348
|
+
if (!selectedEdgeId)
|
|
349
|
+
return null;
|
|
350
|
+
return edges.find(e => e.id === selectedEdgeId);
|
|
351
|
+
}, [selectedEdgeId, edges]);
|
|
352
|
+
const selectedEdgeTypeDefinition = (0, react_1.useMemo)(() => {
|
|
353
|
+
if (!selectedEdge)
|
|
354
|
+
return null;
|
|
355
|
+
return configuration.edgeTypes[selectedEdge.type];
|
|
356
|
+
}, [selectedEdge, configuration.edgeTypes]);
|
|
357
|
+
const selectedNode = (0, react_1.useMemo)(() => {
|
|
358
|
+
if (!selectedNodeId)
|
|
359
|
+
return null;
|
|
360
|
+
return nodes.find(n => n.id === selectedNodeId);
|
|
361
|
+
}, [selectedNodeId, nodes]);
|
|
362
|
+
const selectedNodeTypeDefinition = (0, react_1.useMemo)(() => {
|
|
363
|
+
if (!selectedNode)
|
|
364
|
+
return null;
|
|
365
|
+
return configuration.nodeTypes[selectedNode.type];
|
|
366
|
+
}, [selectedNode, configuration.nodeTypes]);
|
|
367
|
+
// ============================================
|
|
368
|
+
// ANIMATIONS
|
|
369
|
+
// ============================================
|
|
370
|
+
(0, react_1.useEffect)(() => {
|
|
371
|
+
if (events.length === 0)
|
|
372
|
+
return;
|
|
373
|
+
const latestEvent = events[events.length - 1];
|
|
374
|
+
if (latestEvent.operation === 'animate' && latestEvent.category === 'edge') {
|
|
375
|
+
const edgeEvent = latestEvent.payload;
|
|
376
|
+
const edgeId = edgeEvent.edgeId;
|
|
377
|
+
const animation = edgeEvent.animation;
|
|
378
|
+
if (animation && edgeId) {
|
|
379
|
+
setAnimationState(prev => ({
|
|
380
|
+
...prev,
|
|
381
|
+
edgeAnimations: {
|
|
382
|
+
...prev.edgeAnimations,
|
|
383
|
+
[edgeId]: {
|
|
384
|
+
type: 'flow',
|
|
385
|
+
duration: animation.duration || 1000,
|
|
386
|
+
direction: animation.direction || 'forward',
|
|
387
|
+
timestamp: Date.now(),
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
}));
|
|
391
|
+
const duration = animation.duration || 1000;
|
|
392
|
+
setTimeout(() => {
|
|
393
|
+
setAnimationState(prev => {
|
|
394
|
+
const newEdgeAnimations = { ...prev.edgeAnimations };
|
|
395
|
+
delete newEdgeAnimations[edgeId];
|
|
396
|
+
return { ...prev, edgeAnimations: newEdgeAnimations };
|
|
397
|
+
});
|
|
398
|
+
}, duration);
|
|
399
|
+
onEventProcessed?.(latestEvent);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (latestEvent.category === 'state') {
|
|
403
|
+
const stateEvent = latestEvent.payload;
|
|
404
|
+
const nodeId = stateEvent.nodeId;
|
|
405
|
+
const newState = stateEvent.newState;
|
|
406
|
+
if (nodeId && newState) {
|
|
407
|
+
const stateToAnimation = {
|
|
408
|
+
processing: 'pulse',
|
|
409
|
+
completed: 'flash',
|
|
410
|
+
error: 'shake',
|
|
411
|
+
};
|
|
412
|
+
const animationType = stateToAnimation[newState];
|
|
413
|
+
if (animationType) {
|
|
414
|
+
const duration = animationType === 'pulse' ? 1500 : animationType === 'flash' ? 1000 : 500;
|
|
415
|
+
setAnimationState(prev => ({
|
|
416
|
+
...prev,
|
|
417
|
+
nodeAnimations: {
|
|
418
|
+
...prev.nodeAnimations,
|
|
419
|
+
[nodeId]: { type: animationType, duration, timestamp: Date.now() },
|
|
420
|
+
},
|
|
421
|
+
}));
|
|
422
|
+
if (animationType !== 'pulse') {
|
|
423
|
+
setTimeout(() => {
|
|
424
|
+
setAnimationState(prev => {
|
|
425
|
+
const newNodeAnimations = { ...prev.nodeAnimations };
|
|
426
|
+
delete newNodeAnimations[nodeId];
|
|
427
|
+
return { ...prev, nodeAnimations: newNodeAnimations };
|
|
428
|
+
});
|
|
429
|
+
}, duration);
|
|
430
|
+
}
|
|
431
|
+
onEventProcessed?.(latestEvent);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (latestEvent.category === 'node' && latestEvent.operation === 'create') {
|
|
436
|
+
const nodeEvent = latestEvent.payload;
|
|
437
|
+
const nodeId = nodeEvent.nodeId;
|
|
438
|
+
if (nodeId) {
|
|
439
|
+
setAnimationState(prev => ({
|
|
440
|
+
...prev,
|
|
441
|
+
nodeAnimations: {
|
|
442
|
+
...prev.nodeAnimations,
|
|
443
|
+
[nodeId]: { type: 'entry', duration: 600, timestamp: Date.now() },
|
|
444
|
+
},
|
|
445
|
+
}));
|
|
446
|
+
setTimeout(() => {
|
|
447
|
+
setAnimationState(prev => {
|
|
448
|
+
const newNodeAnimations = { ...prev.nodeAnimations };
|
|
449
|
+
delete newNodeAnimations[nodeId];
|
|
450
|
+
return { ...prev, nodeAnimations: newNodeAnimations };
|
|
451
|
+
});
|
|
452
|
+
}, 600);
|
|
453
|
+
onEventProcessed?.(latestEvent);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}, [events, onEventProcessed]);
|
|
457
|
+
// ============================================
|
|
458
|
+
// XYFLOW CONVERSION
|
|
459
|
+
// ============================================
|
|
460
|
+
const xyflowNodesBase = (0, react_1.useMemo)(() => {
|
|
461
|
+
const converted = (0, graphConverter_1.convertToXYFlowNodes)(nodes, configuration, violations);
|
|
462
|
+
const layoutType = configuration.display?.layout || 'hierarchical';
|
|
463
|
+
const positioned = (0, graphConverter_1.autoLayoutNodes)(converted, [], layoutType);
|
|
464
|
+
return positioned.map(node => {
|
|
465
|
+
const animation = animationState.nodeAnimations[node.id];
|
|
466
|
+
// Apply any pending position changes
|
|
467
|
+
const pendingPosition = editStateRef.current.positionChanges.get(node.id);
|
|
468
|
+
return {
|
|
469
|
+
...node,
|
|
470
|
+
...(pendingPosition ? { position: pendingPosition } : {}),
|
|
471
|
+
data: {
|
|
472
|
+
...node.data,
|
|
473
|
+
editable,
|
|
474
|
+
...(animation ? {
|
|
475
|
+
animationType: animation.type,
|
|
476
|
+
animationDuration: animation.duration,
|
|
477
|
+
} : {}),
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
});
|
|
481
|
+
}, [nodes, configuration, violations, animationState.nodeAnimations, editable, editStateRef]);
|
|
482
|
+
const baseNodesKey = (0, react_1.useMemo)(() => {
|
|
483
|
+
return nodes.map(n => n.id).sort().join(',');
|
|
484
|
+
}, [nodes]);
|
|
485
|
+
// Local xyflow nodes state for dragging
|
|
486
|
+
const [xyflowLocalNodes, setXyflowLocalNodes] = (0, react_1.useState)(xyflowNodesBase);
|
|
487
|
+
// Sync when base changes
|
|
488
|
+
const prevBaseNodesKeyRef = (0, react_1.useRef)(baseNodesKey);
|
|
489
|
+
(0, react_1.useEffect)(() => {
|
|
490
|
+
if (prevBaseNodesKeyRef.current !== baseNodesKey) {
|
|
491
|
+
prevBaseNodesKeyRef.current = baseNodesKey;
|
|
492
|
+
setXyflowLocalNodes(xyflowNodesBase);
|
|
493
|
+
}
|
|
494
|
+
}, [baseNodesKey, xyflowNodesBase]);
|
|
495
|
+
// Also sync when entering edit mode or when base nodes change content
|
|
496
|
+
const prevEditableRef = (0, react_1.useRef)(editable);
|
|
497
|
+
(0, react_1.useEffect)(() => {
|
|
498
|
+
if (editable && !prevEditableRef.current) {
|
|
499
|
+
// Entering edit mode - sync positions
|
|
500
|
+
setXyflowLocalNodes(xyflowNodesBase);
|
|
501
|
+
}
|
|
502
|
+
prevEditableRef.current = editable;
|
|
503
|
+
}, [editable, xyflowNodesBase]);
|
|
504
|
+
const xyflowNodes = editable ? xyflowLocalNodes : xyflowNodesBase;
|
|
505
|
+
// Handle node changes (drag events)
|
|
506
|
+
const handleNodesChange = (0, react_1.useCallback)((changes) => {
|
|
507
|
+
if (!editable)
|
|
508
|
+
return;
|
|
509
|
+
setXyflowLocalNodes(nds => (0, react_2.applyNodeChanges)(changes, nds));
|
|
510
|
+
// Track position changes on drag end
|
|
511
|
+
const positionChanges = changes
|
|
512
|
+
.filter((change) => change.type === 'position' &&
|
|
513
|
+
'position' in change &&
|
|
514
|
+
change.position !== undefined &&
|
|
515
|
+
'dragging' in change &&
|
|
516
|
+
change.dragging === false);
|
|
517
|
+
if (positionChanges.length > 0) {
|
|
518
|
+
updateEditState(prev => {
|
|
519
|
+
const newPositions = new Map(prev.positionChanges);
|
|
520
|
+
for (const change of positionChanges) {
|
|
521
|
+
newPositions.set(change.id, {
|
|
522
|
+
x: Math.round(change.position.x),
|
|
523
|
+
y: Math.round(change.position.y),
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return { ...prev, positionChanges: newPositions };
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}, [editable, updateEditState]);
|
|
530
|
+
const xyflowEdges = (0, react_1.useMemo)(() => {
|
|
531
|
+
const converted = (0, graphConverter_1.convertToXYFlowEdges)(edges, configuration, violations);
|
|
532
|
+
return converted.map(edge => {
|
|
533
|
+
const animation = animationState.edgeAnimations[edge.id];
|
|
534
|
+
if (animation) {
|
|
535
|
+
return {
|
|
536
|
+
...edge,
|
|
537
|
+
data: {
|
|
538
|
+
...edge.data,
|
|
539
|
+
animationType: animation.type,
|
|
540
|
+
animationDuration: animation.duration,
|
|
541
|
+
animationDirection: animation.direction,
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
return edge;
|
|
546
|
+
});
|
|
547
|
+
}, [edges, configuration, violations, animationState.edgeAnimations]);
|
|
548
|
+
// Fit view on mount and structure changes
|
|
549
|
+
(0, react_1.useEffect)(() => {
|
|
550
|
+
const timeoutId = setTimeout(() => {
|
|
551
|
+
fitView({
|
|
552
|
+
padding: 0.2,
|
|
553
|
+
includeHiddenNodes: false,
|
|
554
|
+
minZoom: 0.1,
|
|
555
|
+
maxZoom: 1.5,
|
|
556
|
+
duration: 200,
|
|
557
|
+
});
|
|
558
|
+
}, 100);
|
|
559
|
+
return () => clearTimeout(timeoutId);
|
|
560
|
+
}, [baseNodesKey, fitView]);
|
|
561
|
+
// ============================================
|
|
562
|
+
// RENDER
|
|
563
|
+
// ============================================
|
|
564
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(react_2.ReactFlow, { nodes: xyflowNodes, edges: xyflowEdges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, minZoom: 0.1, maxZoom: 4, defaultEdgeOptions: { type: 'custom' }, onEdgeClick: onEdgeClick, onNodeClick: onNodeClick, proOptions: { hideAttribution: true }, nodesDraggable: editable, elementsSelectable: editable, nodesConnectable: editable, edgesReconnectable: editable, onNodesChange: handleNodesChange, onConnect: handleConnect, onReconnectStart: handleReconnectStart, onReconnect: handleReconnect, onReconnectEnd: handleReconnectEnd, panOnDrag: true, selectionOnDrag: false, children: [showBackground && (0, jsx_runtime_1.jsx)(react_2.Background, { color: "#e5e5e5", gap: 16, size: 1 }), showControls && (0, jsx_runtime_1.jsx)(react_2.Controls, { showZoom: true, showFitView: true, showInteractive: true }), showMinimap && ((0, jsx_runtime_1.jsx)(react_2.MiniMap, { nodeColor: (node) => {
|
|
565
|
+
const nodeData = node.data;
|
|
566
|
+
return nodeData?.typeDefinition?.color || '#888';
|
|
567
|
+
}, nodeBorderRadius: 2, pannable: true, zoomable: true }))] }, baseNodesKey), selectedEdge && selectedEdgeTypeDefinition && ((0, jsx_runtime_1.jsx)(EdgeInfoPanel_1.EdgeInfoPanel, { edge: selectedEdge, typeDefinition: selectedEdgeTypeDefinition, sourceNodeId: selectedEdge.from, targetNodeId: selectedEdge.to, onClose: onCloseEdgeInfoPanel, onDelete: editable ? handleEdgeDelete : undefined })), selectedNode && selectedNodeTypeDefinition && ((0, jsx_runtime_1.jsx)(NodeInfoPanel_1.NodeInfoPanel, { node: selectedNode, typeDefinition: selectedNodeTypeDefinition, availableNodeTypes: configuration.nodeTypes, onClose: onCloseNodeInfoPanel, onDelete: editable ? handleNodeDelete : undefined, onUpdate: editable ? handleNodeUpdate : undefined })), pendingConnection && ((0, jsx_runtime_1.jsxs)("div", { style: {
|
|
568
|
+
position: 'absolute',
|
|
569
|
+
top: '50%',
|
|
570
|
+
left: '50%',
|
|
571
|
+
transform: 'translate(-50%, -50%)',
|
|
572
|
+
backgroundColor: 'white',
|
|
573
|
+
borderRadius: '8px',
|
|
574
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
575
|
+
padding: '16px',
|
|
576
|
+
minWidth: '200px',
|
|
577
|
+
zIndex: 1000,
|
|
578
|
+
}, children: [(0, jsx_runtime_1.jsx)("div", { style: { fontWeight: 'bold', marginBottom: '12px', fontSize: '14px' }, children: "Select Edge Type" }), (0, jsx_runtime_1.jsxs)("div", { style: { fontSize: '12px', color: '#666', marginBottom: '12px' }, children: [pendingConnection.from, " \u2192 ", pendingConnection.to] }), (0, jsx_runtime_1.jsx)("div", { style: { display: 'flex', flexDirection: 'column', gap: '8px' }, children: pendingConnection.validTypes.map(type => {
|
|
579
|
+
const typeDefinition = configuration.edgeTypes[type];
|
|
580
|
+
return ((0, jsx_runtime_1.jsx)("button", { onClick: () => handleEdgeTypeSelect(type), style: {
|
|
581
|
+
padding: '8px 12px',
|
|
582
|
+
backgroundColor: typeDefinition?.color || '#888',
|
|
583
|
+
color: 'white',
|
|
584
|
+
border: 'none',
|
|
585
|
+
borderRadius: '4px',
|
|
586
|
+
cursor: 'pointer',
|
|
587
|
+
fontSize: '12px',
|
|
588
|
+
fontWeight: 'bold',
|
|
589
|
+
textAlign: 'left',
|
|
590
|
+
}, children: type }, type));
|
|
591
|
+
}) }), (0, jsx_runtime_1.jsx)("button", { onClick: handleCancelEdgeTypePicker, style: {
|
|
592
|
+
marginTop: '12px',
|
|
593
|
+
width: '100%',
|
|
594
|
+
padding: '8px 12px',
|
|
595
|
+
backgroundColor: '#f0f0f0',
|
|
596
|
+
color: '#666',
|
|
597
|
+
border: 'none',
|
|
598
|
+
borderRadius: '4px',
|
|
599
|
+
cursor: 'pointer',
|
|
600
|
+
fontSize: '12px',
|
|
601
|
+
}, children: "Cancel" })] }))] }));
|
|
602
|
+
};
|
|
603
|
+
/**
|
|
604
|
+
* Convert canvas to legacy configuration format for internal use
|
|
605
|
+
*/
|
|
606
|
+
function useCanvasToLegacy(canvas, library) {
|
|
607
|
+
return (0, react_1.useMemo)(() => {
|
|
608
|
+
if (!canvas)
|
|
609
|
+
return null;
|
|
610
|
+
const { nodes, edges } = principal_view_core_1.CanvasConverter.canvasToGraph(canvas);
|
|
611
|
+
// Build GraphConfiguration from canvas
|
|
612
|
+
const nodeTypes = {};
|
|
613
|
+
const edgeTypes = {};
|
|
614
|
+
// First, add node types from library (lowest priority - can be overridden by canvas)
|
|
615
|
+
if (library?.nodeComponents) {
|
|
616
|
+
for (const [id, component] of Object.entries(library.nodeComponents)) {
|
|
617
|
+
nodeTypes[id] = {
|
|
618
|
+
shape: component.shape || 'rectangle',
|
|
619
|
+
icon: component.icon,
|
|
620
|
+
color: component.color,
|
|
621
|
+
size: component.size,
|
|
622
|
+
dataSchema: component.dataSchema || {},
|
|
623
|
+
states: component.states,
|
|
624
|
+
layout: component.layout,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// Then, add edge types from library
|
|
629
|
+
if (library?.edgeComponents) {
|
|
630
|
+
for (const [id, component] of Object.entries(library.edgeComponents)) {
|
|
631
|
+
edgeTypes[id] = {
|
|
632
|
+
style: component.style || 'solid',
|
|
633
|
+
color: component.color,
|
|
634
|
+
width: component.width,
|
|
635
|
+
directed: component.directed,
|
|
636
|
+
animation: component.animation,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// Next, add node types from canvas vv.nodeTypes (overrides library)
|
|
641
|
+
if (canvas.pv?.nodeTypes) {
|
|
642
|
+
for (const [id, def] of Object.entries(canvas.pv.nodeTypes)) {
|
|
643
|
+
nodeTypes[id] = {
|
|
644
|
+
shape: def.shape || 'rectangle',
|
|
645
|
+
icon: def.icon,
|
|
646
|
+
color: def.color,
|
|
647
|
+
dataSchema: {},
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// Then extract node types from canvas nodes (for nodes that define their own types)
|
|
652
|
+
for (const node of canvas.nodes || []) {
|
|
653
|
+
const vv = node.pv;
|
|
654
|
+
const nodeType = vv?.nodeType || node.type;
|
|
655
|
+
if (!nodeTypes[nodeType]) {
|
|
656
|
+
// Color priority: vv.fill > node.color > vv.states.idle.color
|
|
657
|
+
const fillColor = vv?.fill
|
|
658
|
+
|| (typeof node.color === 'string' ? node.color : undefined)
|
|
659
|
+
|| vv?.states?.idle?.color;
|
|
660
|
+
nodeTypes[nodeType] = {
|
|
661
|
+
shape: vv?.shape || 'rectangle',
|
|
662
|
+
icon: vv?.icon,
|
|
663
|
+
color: fillColor,
|
|
664
|
+
stroke: vv?.stroke,
|
|
665
|
+
size: { width: node.width, height: node.height },
|
|
666
|
+
dataSchema: vv?.dataSchema || {},
|
|
667
|
+
states: vv?.states,
|
|
668
|
+
layout: vv?.layout,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// Extract edge types from canvas vv.edgeTypes
|
|
673
|
+
if (canvas.pv?.edgeTypes) {
|
|
674
|
+
for (const [id, def] of Object.entries(canvas.pv.edgeTypes)) {
|
|
675
|
+
edgeTypes[id] = {
|
|
676
|
+
style: def.style || 'solid',
|
|
677
|
+
color: def.color,
|
|
678
|
+
width: def.width,
|
|
679
|
+
directed: def.directed,
|
|
680
|
+
animation: def.animation,
|
|
681
|
+
label: def.labelConfig,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// Build allowed connections from edges
|
|
686
|
+
const allowedConnections = [];
|
|
687
|
+
for (const edge of canvas.edges || []) {
|
|
688
|
+
const edgeType = edge.pv?.edgeType || 'default';
|
|
689
|
+
// Ensure edge type exists
|
|
690
|
+
if (!edgeTypes[edgeType]) {
|
|
691
|
+
edgeTypes[edgeType] = {
|
|
692
|
+
style: edge.pv?.style || 'solid',
|
|
693
|
+
color: typeof edge.color === 'string' ? edge.color : undefined,
|
|
694
|
+
width: edge.pv?.width,
|
|
695
|
+
directed: true,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
// Find node types for from/to
|
|
699
|
+
const fromNode = canvas.nodes?.find(n => n.id === edge.fromNode);
|
|
700
|
+
const toNode = canvas.nodes?.find(n => n.id === edge.toNode);
|
|
701
|
+
const fromType = fromNode?.pv?.nodeType || edge.fromNode;
|
|
702
|
+
const toType = toNode?.pv?.nodeType || edge.toNode;
|
|
703
|
+
allowedConnections.push({
|
|
704
|
+
from: fromType,
|
|
705
|
+
to: toType,
|
|
706
|
+
via: edgeType,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
// Build display config with required layout field
|
|
710
|
+
const display = canvas.pv?.display
|
|
711
|
+
? {
|
|
712
|
+
layout: canvas.pv.display.layout || 'manual',
|
|
713
|
+
theme: canvas.pv.display.theme,
|
|
714
|
+
animations: canvas.pv.display.animations,
|
|
715
|
+
}
|
|
716
|
+
: { layout: 'manual' };
|
|
717
|
+
const configuration = {
|
|
718
|
+
metadata: {
|
|
719
|
+
name: canvas.pv?.name || 'Untitled',
|
|
720
|
+
version: canvas.pv?.version || '1.0.0',
|
|
721
|
+
description: canvas.pv?.description,
|
|
722
|
+
},
|
|
723
|
+
nodeTypes,
|
|
724
|
+
edgeTypes,
|
|
725
|
+
allowedConnections,
|
|
726
|
+
display,
|
|
727
|
+
};
|
|
728
|
+
return { configuration, nodes, edges };
|
|
729
|
+
}, [canvas, library]);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Core graph visualization component using xyflow.
|
|
733
|
+
*
|
|
734
|
+
* Accepts an ExtendedCanvas document for rendering.
|
|
735
|
+
*
|
|
736
|
+
* When `editable` is true, the component manages its own edit state internally.
|
|
737
|
+
* Use the ref to get pending changes when the user wants to save:
|
|
738
|
+
*
|
|
739
|
+
* ```tsx
|
|
740
|
+
* <GraphRenderer canvas={myCanvas} />
|
|
741
|
+
*
|
|
742
|
+
* // With edit mode
|
|
743
|
+
* const graphRef = useRef<GraphRendererHandle>(null);
|
|
744
|
+
* <GraphRenderer
|
|
745
|
+
* ref={graphRef}
|
|
746
|
+
* canvas={myCanvas}
|
|
747
|
+
* editable={isEditMode}
|
|
748
|
+
* onPendingChangesChange={setHasUnsavedChanges}
|
|
749
|
+
* />
|
|
750
|
+
* ```
|
|
751
|
+
*/
|
|
752
|
+
exports.GraphRenderer = (0, react_1.forwardRef)((props, ref) => {
|
|
753
|
+
const { canvas, library, className, width = '100%', height = '100%' } = props;
|
|
754
|
+
// Convert canvas to internal format (merging library types if provided)
|
|
755
|
+
const canvasData = useCanvasToLegacy(canvas, library);
|
|
756
|
+
// Validate we have required data
|
|
757
|
+
if (!canvasData) {
|
|
758
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: className, style: { width, height, display: 'flex', alignItems: 'center', justifyContent: 'center' }, children: (0, jsx_runtime_1.jsx)("p", { style: { color: '#666' }, children: "No canvas data provided." }) }));
|
|
759
|
+
}
|
|
760
|
+
const { configuration, nodes, edges } = canvasData;
|
|
761
|
+
// Internal edit state ref
|
|
762
|
+
const editStateRef = (0, react_1.useRef)(createEmptyEditState());
|
|
763
|
+
// Expose imperative handle
|
|
764
|
+
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
765
|
+
getPendingChanges: () => {
|
|
766
|
+
const state = editStateRef.current;
|
|
767
|
+
return {
|
|
768
|
+
positionChanges: Array.from(state.positionChanges.entries()).map(([nodeId, position]) => ({
|
|
769
|
+
nodeId,
|
|
770
|
+
position,
|
|
771
|
+
})),
|
|
772
|
+
nodeUpdates: Array.from(state.nodeUpdates.entries()).map(([nodeId, updates]) => ({
|
|
773
|
+
nodeId,
|
|
774
|
+
updates,
|
|
775
|
+
})),
|
|
776
|
+
deletedNodeIds: Array.from(state.deletedNodeIds),
|
|
777
|
+
createdEdges: state.createdEdges.map(e => ({
|
|
778
|
+
from: e.from,
|
|
779
|
+
to: e.to,
|
|
780
|
+
type: e.type,
|
|
781
|
+
sourceHandle: e.sourceHandle,
|
|
782
|
+
targetHandle: e.targetHandle,
|
|
783
|
+
})),
|
|
784
|
+
deletedEdges: state.deletedEdges.map(e => ({ from: e.from, to: e.to, type: e.type })),
|
|
785
|
+
hasChanges: state.positionChanges.size > 0 ||
|
|
786
|
+
state.nodeUpdates.size > 0 ||
|
|
787
|
+
state.deletedNodeIds.size > 0 ||
|
|
788
|
+
state.createdEdges.length > 0 ||
|
|
789
|
+
state.deletedEdges.length > 0,
|
|
790
|
+
};
|
|
791
|
+
},
|
|
792
|
+
resetEditState: () => {
|
|
793
|
+
editStateRef.current = createEmptyEditState();
|
|
794
|
+
},
|
|
795
|
+
hasUnsavedChanges: () => {
|
|
796
|
+
const state = editStateRef.current;
|
|
797
|
+
return state.positionChanges.size > 0 ||
|
|
798
|
+
state.nodeUpdates.size > 0 ||
|
|
799
|
+
state.deletedNodeIds.size > 0 ||
|
|
800
|
+
state.createdEdges.length > 0 ||
|
|
801
|
+
state.deletedEdges.length > 0;
|
|
802
|
+
},
|
|
803
|
+
}), []);
|
|
804
|
+
// Extract only the props that inner component needs
|
|
805
|
+
const { violations, configName, showMinimap, showControls, showBackground, events, onEventProcessed, editable, onPendingChangesChange, } = props;
|
|
806
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: className, style: { width, height, position: 'relative' }, children: (0, jsx_runtime_1.jsx)(react_2.ReactFlowProvider, { children: (0, jsx_runtime_1.jsx)(GraphRendererInner, { configuration: configuration, nodes: nodes, edges: edges, violations: violations, configName: configName, showMinimap: showMinimap, showControls: showControls, showBackground: showBackground, events: events, onEventProcessed: onEventProcessed, editable: editable, onPendingChangesChange: onPendingChangesChange, editStateRef: editStateRef }) }) }));
|
|
807
|
+
});
|
|
808
|
+
exports.GraphRenderer.displayName = 'GraphRenderer';
|
|
809
|
+
//# sourceMappingURL=GraphRenderer.js.map
|