@principal-ai/principal-view-react 0.13.21 → 0.13.23
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/dist/components/GraphRenderer.d.ts +8 -0
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +278 -30
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/contexts/GraphEditContext.d.ts +14 -0
- package/dist/contexts/GraphEditContext.d.ts.map +1 -0
- package/dist/contexts/GraphEditContext.js +8 -0
- package/dist/contexts/GraphEditContext.js.map +1 -0
- package/dist/hooks/useUndoRedo.d.ts +75 -0
- package/dist/hooks/useUndoRedo.d.ts.map +1 -0
- package/dist/hooks/useUndoRedo.js +70 -0
- package/dist/hooks/useUndoRedo.js.map +1 -0
- package/dist/nodes/CustomNode.d.ts.map +1 -1
- package/dist/nodes/CustomNode.js +22 -5
- package/dist/nodes/CustomNode.js.map +1 -1
- package/package.json +1 -1
- package/src/components/GraphRenderer.tsx +372 -38
- package/src/contexts/GraphEditContext.tsx +21 -0
- package/src/hooks/useUndoRedo.ts +138 -0
- package/src/nodes/CustomNode.tsx +35 -3
|
@@ -64,6 +64,14 @@ export interface GraphRendererHandle {
|
|
|
64
64
|
resetEditState: () => void;
|
|
65
65
|
/** Check if there are unsaved changes */
|
|
66
66
|
hasUnsavedChanges: () => boolean;
|
|
67
|
+
/** Whether there are changes that can be undone */
|
|
68
|
+
canUndo: () => boolean;
|
|
69
|
+
/** Whether there are changes that can be redone */
|
|
70
|
+
canRedo: () => boolean;
|
|
71
|
+
/** Undo the last change */
|
|
72
|
+
undo: () => void;
|
|
73
|
+
/** Redo the last undone change */
|
|
74
|
+
redo: () => void;
|
|
67
75
|
}
|
|
68
76
|
/** Base props shared by all render modes */
|
|
69
77
|
interface GraphRendererBaseProps {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GraphRenderer.d.ts","sourceRoot":"","sources":["../../src/components/GraphRenderer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAUN,MAAM,OAAO,CAAC;AAqBf,OAAO,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAIV,SAAS,EACT,UAAU,EAIV,cAAc,EACd,gBAAgB,EACjB,MAAM,mCAAmC,CAAC;
|
|
1
|
+
{"version":3,"file":"GraphRenderer.d.ts","sourceRoot":"","sources":["../../src/components/GraphRenderer.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useState, useEffect, useLayoutEffect, useCallback, useRef, useImperativeHandle, forwardRef, createContext, } from 'react';
|
|
3
3
|
import { ReactFlow, Background, BackgroundVariant, Controls, MiniMap, ReactFlowProvider, useReactFlow, useUpdateNodeInternals, useViewport, applyNodeChanges, applyEdgeChanges, } from '@xyflow/react';
|
|
4
4
|
import '@xyflow/react/dist/style.css';
|
|
@@ -7,6 +7,8 @@ import { useTheme } from '@principal-ade/industry-theme';
|
|
|
7
7
|
import { CustomNode } from '../nodes/CustomNode';
|
|
8
8
|
import { CustomEdge } from '../edges/CustomEdge';
|
|
9
9
|
import { convertToXYFlowNodes, convertToXYFlowEdges, } from '../utils/graphConverter';
|
|
10
|
+
import { GraphEditProvider } from '../contexts/GraphEditContext';
|
|
11
|
+
import { useUndoRedo } from '../hooks/useUndoRedo';
|
|
10
12
|
/**
|
|
11
13
|
* Context for providing a portal target for tooltips.
|
|
12
14
|
* This allows tooltips to be rendered within the graph container
|
|
@@ -84,7 +86,7 @@ const CenterIndicator = ({ color }) => {
|
|
|
84
86
|
/**
|
|
85
87
|
* Inner component that uses ReactFlow hooks
|
|
86
88
|
*/
|
|
87
|
-
const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges, violations = [], configName: _configName, showMinimap = false, showControls = true, showBackground = true, backgroundVariant = 'lines', backgroundGap, showCenterIndicator = false, showTooltips = true, fitViewDuration = 200, highlightedNodeId, activeNodeIds, events = [], onEventProcessed, editable = false, onPendingChangesChange, onEditStateChange, editStateRef, onNodeClick: onNodeClickProp, fitViewToNodeIds, fitViewPadding = 0.2, draggableNodeIds, onNodeDragStop: onNodeDragStopProp, onCopy, }) => {
|
|
89
|
+
const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges, violations = [], configName: _configName, showMinimap = false, showControls = true, showBackground = true, backgroundVariant = 'lines', backgroundGap, showCenterIndicator = false, showTooltips = true, fitViewDuration = 200, highlightedNodeId, activeNodeIds, events = [], onEventProcessed, editable = false, onPendingChangesChange, onEditStateChange, editStateRef, resetVisualStateRef, undoRedoFunctionsRef, pushHistory, clearHistory, undoFromStack, redoFromStack, onNodeClick: onNodeClickProp, fitViewToNodeIds, fitViewPadding = 0.2, draggableNodeIds, onNodeDragStop: onNodeDragStopProp, onCopy, }) => {
|
|
88
90
|
const { fitView, fitBounds, getNodes } = useReactFlow();
|
|
89
91
|
const updateNodeInternals = useUpdateNodeInternals();
|
|
90
92
|
const { theme } = useTheme();
|
|
@@ -200,6 +202,8 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
200
202
|
onPendingChangesChange?.(false);
|
|
201
203
|
// Clear animation state to prevent stale animations from affecting new edges
|
|
202
204
|
setAnimationState({ nodeAnimations: {}, edgeAnimations: {} });
|
|
205
|
+
// Clear undo/redo history when props change
|
|
206
|
+
clearHistory();
|
|
203
207
|
}
|
|
204
208
|
}, [propNodes, propEdges, editStateRef, onEditStateChange, onPendingChangesChange]);
|
|
205
209
|
// When draggableNodeIds is provided, positions are managed externally
|
|
@@ -218,6 +222,10 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
218
222
|
// and receives state_changed event updates. localEdges only used in edit mode.
|
|
219
223
|
const nodes = localNodes;
|
|
220
224
|
const edges = editable ? localEdges : propEdges;
|
|
225
|
+
// Ref to track current xyflow nodes for undo/redo (set later via useEffect)
|
|
226
|
+
const xyflowNodesRef = useRef([]);
|
|
227
|
+
// Ref to capture node positions at drag start (for accurate undo "before" values)
|
|
228
|
+
const dragStartPositionsRef = useRef(new Map());
|
|
221
229
|
// Helper to check if there are pending changes
|
|
222
230
|
const checkHasChanges = useCallback((state) => {
|
|
223
231
|
return (state.positionChanges.size > 0 ||
|
|
@@ -244,6 +252,34 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
244
252
|
onEditStateChange?.(newState);
|
|
245
253
|
onPendingChangesChange?.(hasChanges);
|
|
246
254
|
}, [editStateRef, onEditStateChange, onPendingChangesChange, checkHasChanges]);
|
|
255
|
+
// Handler for node resize end - called from CustomNode via context
|
|
256
|
+
const handleNodeResizeEnd = useCallback((nodeId, dimensions) => {
|
|
257
|
+
if (!editable)
|
|
258
|
+
return;
|
|
259
|
+
// Capture before dimensions for undo
|
|
260
|
+
const currentNode = xyflowNodesRef.current.find((n) => n.id === nodeId);
|
|
261
|
+
const beforeDimensions = currentNode
|
|
262
|
+
? { width: currentNode.width ?? 0, height: currentNode.height ?? 0 }
|
|
263
|
+
: { width: 0, height: 0 };
|
|
264
|
+
// Push to history
|
|
265
|
+
pushHistory([
|
|
266
|
+
{
|
|
267
|
+
type: 'dimension',
|
|
268
|
+
nodeId,
|
|
269
|
+
before: beforeDimensions,
|
|
270
|
+
after: dimensions,
|
|
271
|
+
},
|
|
272
|
+
]);
|
|
273
|
+
updateEditState((prev) => {
|
|
274
|
+
const newDimensions = new Map(prev.dimensionChanges);
|
|
275
|
+
newDimensions.set(nodeId, dimensions);
|
|
276
|
+
return { ...prev, dimensionChanges: newDimensions };
|
|
277
|
+
});
|
|
278
|
+
}, [editable, updateEditState, pushHistory]);
|
|
279
|
+
// Memoize the context value to prevent unnecessary re-renders
|
|
280
|
+
const graphEditContextValue = useMemo(() => ({
|
|
281
|
+
onNodeResizeEnd: handleNodeResizeEnd,
|
|
282
|
+
}), [handleNodeResizeEnd]);
|
|
247
283
|
// ============================================
|
|
248
284
|
// ALIGNMENT GUIDES
|
|
249
285
|
// ============================================
|
|
@@ -491,6 +527,7 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
491
527
|
// Create edge helper
|
|
492
528
|
const createEdge = useCallback((from, to, type, sourceHandle, targetHandle) => {
|
|
493
529
|
const edgeId = `${from}-${to}-${type}-${Date.now()}`;
|
|
530
|
+
const now = Date.now();
|
|
494
531
|
// Add to local state with handle information
|
|
495
532
|
const newEdge = {
|
|
496
533
|
id: edgeId,
|
|
@@ -498,12 +535,19 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
498
535
|
from,
|
|
499
536
|
to,
|
|
500
537
|
data: {},
|
|
501
|
-
createdAt:
|
|
502
|
-
updatedAt:
|
|
538
|
+
createdAt: now,
|
|
539
|
+
updatedAt: now,
|
|
503
540
|
sourceHandle,
|
|
504
541
|
targetHandle,
|
|
505
542
|
};
|
|
506
543
|
setLocalEdges((prev) => [...prev, newEdge]);
|
|
544
|
+
// Push to history for undo
|
|
545
|
+
pushHistory([
|
|
546
|
+
{
|
|
547
|
+
type: 'edgeCreate',
|
|
548
|
+
edge: newEdge,
|
|
549
|
+
},
|
|
550
|
+
]);
|
|
507
551
|
// Track the change
|
|
508
552
|
updateEditState((prev) => ({
|
|
509
553
|
...prev,
|
|
@@ -512,7 +556,7 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
512
556
|
{ id: edgeId, from, to, type, sourceHandle, targetHandle },
|
|
513
557
|
],
|
|
514
558
|
}));
|
|
515
|
-
}, [updateEditState]);
|
|
559
|
+
}, [updateEditState, pushHistory]);
|
|
516
560
|
// Handle new connection from drag
|
|
517
561
|
const handleConnect = useCallback((connection) => {
|
|
518
562
|
if (!editable || !connection.source || !connection.target)
|
|
@@ -837,6 +881,10 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
837
881
|
}, [edges]);
|
|
838
882
|
// Local xyflow nodes state for dragging
|
|
839
883
|
const [xyflowLocalNodes, setXyflowLocalNodes] = useState(xyflowNodesBase);
|
|
884
|
+
// Keep xyflowNodesRef in sync for undo/redo access to current node state
|
|
885
|
+
useEffect(() => {
|
|
886
|
+
xyflowNodesRef.current = xyflowLocalNodes;
|
|
887
|
+
}, [xyflowLocalNodes]);
|
|
840
888
|
// Sync when base node IDs change
|
|
841
889
|
const prevBaseNodesKeyRef = useRef(baseNodesKey);
|
|
842
890
|
useEffect(() => {
|
|
@@ -915,6 +963,165 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
915
963
|
onNodeDragStopProp(node.id, node.position);
|
|
916
964
|
}
|
|
917
965
|
}, [onNodeDragStopProp]);
|
|
966
|
+
// ============================================
|
|
967
|
+
// UNDO/REDO APPLY FUNCTIONS
|
|
968
|
+
// ============================================
|
|
969
|
+
// Apply undo - reverse the effects of history entries
|
|
970
|
+
const applyUndo = useCallback(() => {
|
|
971
|
+
const batch = undoFromStack();
|
|
972
|
+
if (!batch)
|
|
973
|
+
return;
|
|
974
|
+
// Process entries in reverse order for undo
|
|
975
|
+
for (const entry of batch.slice().reverse()) {
|
|
976
|
+
switch (entry.type) {
|
|
977
|
+
case 'position':
|
|
978
|
+
// Restore previous position
|
|
979
|
+
setXyflowLocalNodes((nodes) => nodes.map((n) => n.id === entry.nodeId
|
|
980
|
+
? { ...n, position: entry.before }
|
|
981
|
+
: n));
|
|
982
|
+
// Update edit state
|
|
983
|
+
updateEditState((prev) => {
|
|
984
|
+
const newPositions = new Map(prev.positionChanges);
|
|
985
|
+
newPositions.set(entry.nodeId, entry.before);
|
|
986
|
+
return { ...prev, positionChanges: newPositions };
|
|
987
|
+
});
|
|
988
|
+
break;
|
|
989
|
+
case 'dimension':
|
|
990
|
+
// Restore previous dimensions
|
|
991
|
+
setXyflowLocalNodes((nodes) => nodes.map((n) => n.id === entry.nodeId
|
|
992
|
+
? { ...n, width: entry.before.width, height: entry.before.height }
|
|
993
|
+
: n));
|
|
994
|
+
// Update edit state
|
|
995
|
+
updateEditState((prev) => {
|
|
996
|
+
const newDimensions = new Map(prev.dimensionChanges);
|
|
997
|
+
newDimensions.set(entry.nodeId, entry.before);
|
|
998
|
+
return { ...prev, dimensionChanges: newDimensions };
|
|
999
|
+
});
|
|
1000
|
+
break;
|
|
1001
|
+
case 'edgeCreate':
|
|
1002
|
+
// Remove the created edge
|
|
1003
|
+
setLocalEdges((edges) => edges.filter((e) => e.id !== entry.edge.id));
|
|
1004
|
+
// Update edit state
|
|
1005
|
+
updateEditState((prev) => ({
|
|
1006
|
+
...prev,
|
|
1007
|
+
createdEdges: prev.createdEdges.filter((e) => e.id !== entry.edge.id),
|
|
1008
|
+
}));
|
|
1009
|
+
break;
|
|
1010
|
+
case 'edgeDelete':
|
|
1011
|
+
// Restore the deleted edge
|
|
1012
|
+
setLocalEdges((edges) => [...edges, entry.edge]);
|
|
1013
|
+
// Update edit state
|
|
1014
|
+
updateEditState((prev) => ({
|
|
1015
|
+
...prev,
|
|
1016
|
+
deletedEdges: prev.deletedEdges.filter((e) => e.id !== entry.edge.id),
|
|
1017
|
+
}));
|
|
1018
|
+
break;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}, [undoFromStack, updateEditState]);
|
|
1022
|
+
// Apply redo - re-apply the effects of history entries
|
|
1023
|
+
const applyRedo = useCallback(() => {
|
|
1024
|
+
const batch = redoFromStack();
|
|
1025
|
+
if (!batch)
|
|
1026
|
+
return;
|
|
1027
|
+
// Process entries in original order for redo
|
|
1028
|
+
for (const entry of batch) {
|
|
1029
|
+
switch (entry.type) {
|
|
1030
|
+
case 'position':
|
|
1031
|
+
// Apply new position
|
|
1032
|
+
setXyflowLocalNodes((nodes) => nodes.map((n) => n.id === entry.nodeId
|
|
1033
|
+
? { ...n, position: entry.after }
|
|
1034
|
+
: n));
|
|
1035
|
+
// Update edit state
|
|
1036
|
+
updateEditState((prev) => {
|
|
1037
|
+
const newPositions = new Map(prev.positionChanges);
|
|
1038
|
+
newPositions.set(entry.nodeId, entry.after);
|
|
1039
|
+
return { ...prev, positionChanges: newPositions };
|
|
1040
|
+
});
|
|
1041
|
+
break;
|
|
1042
|
+
case 'dimension':
|
|
1043
|
+
// Apply new dimensions
|
|
1044
|
+
setXyflowLocalNodes((nodes) => nodes.map((n) => n.id === entry.nodeId
|
|
1045
|
+
? { ...n, width: entry.after.width, height: entry.after.height }
|
|
1046
|
+
: n));
|
|
1047
|
+
// Update edit state
|
|
1048
|
+
updateEditState((prev) => {
|
|
1049
|
+
const newDimensions = new Map(prev.dimensionChanges);
|
|
1050
|
+
newDimensions.set(entry.nodeId, entry.after);
|
|
1051
|
+
return { ...prev, dimensionChanges: newDimensions };
|
|
1052
|
+
});
|
|
1053
|
+
break;
|
|
1054
|
+
case 'edgeCreate':
|
|
1055
|
+
// Re-create the edge
|
|
1056
|
+
setLocalEdges((edges) => [...edges, entry.edge]);
|
|
1057
|
+
// Update edit state
|
|
1058
|
+
updateEditState((prev) => ({
|
|
1059
|
+
...prev,
|
|
1060
|
+
createdEdges: [
|
|
1061
|
+
...prev.createdEdges,
|
|
1062
|
+
{
|
|
1063
|
+
id: entry.edge.id,
|
|
1064
|
+
from: entry.edge.from,
|
|
1065
|
+
to: entry.edge.to,
|
|
1066
|
+
type: entry.edge.type,
|
|
1067
|
+
sourceHandle: entry.edge.sourceHandle,
|
|
1068
|
+
targetHandle: entry.edge.targetHandle,
|
|
1069
|
+
},
|
|
1070
|
+
],
|
|
1071
|
+
}));
|
|
1072
|
+
break;
|
|
1073
|
+
case 'edgeDelete':
|
|
1074
|
+
// Re-delete the edge
|
|
1075
|
+
setLocalEdges((edges) => edges.filter((e) => e.id !== entry.edge.id));
|
|
1076
|
+
// Update edit state
|
|
1077
|
+
updateEditState((prev) => ({
|
|
1078
|
+
...prev,
|
|
1079
|
+
deletedEdges: [
|
|
1080
|
+
...prev.deletedEdges,
|
|
1081
|
+
{
|
|
1082
|
+
id: entry.edge.id,
|
|
1083
|
+
from: entry.edge.from,
|
|
1084
|
+
to: entry.edge.to,
|
|
1085
|
+
type: entry.edge.type,
|
|
1086
|
+
},
|
|
1087
|
+
],
|
|
1088
|
+
}));
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}, [redoFromStack, updateEditState]);
|
|
1093
|
+
// Keyboard shortcuts for undo/redo
|
|
1094
|
+
useEffect(() => {
|
|
1095
|
+
if (!editable)
|
|
1096
|
+
return;
|
|
1097
|
+
const handleKeyDown = (e) => {
|
|
1098
|
+
// Cmd+Z (Mac) or Ctrl+Z (Windows/Linux) for undo
|
|
1099
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'z') {
|
|
1100
|
+
e.preventDefault();
|
|
1101
|
+
if (e.shiftKey) {
|
|
1102
|
+
// Cmd+Shift+Z for redo
|
|
1103
|
+
applyRedo();
|
|
1104
|
+
}
|
|
1105
|
+
else {
|
|
1106
|
+
applyUndo();
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
// Ctrl+Y for redo (Windows)
|
|
1110
|
+
if (e.ctrlKey && e.key === 'y') {
|
|
1111
|
+
e.preventDefault();
|
|
1112
|
+
applyRedo();
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
1116
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
1117
|
+
}, [editable, applyUndo, applyRedo]);
|
|
1118
|
+
// Set undo/redo functions in ref for outer component access
|
|
1119
|
+
useEffect(() => {
|
|
1120
|
+
undoRedoFunctionsRef.current = {
|
|
1121
|
+
applyUndo,
|
|
1122
|
+
applyRedo,
|
|
1123
|
+
};
|
|
1124
|
+
}, [applyUndo, applyRedo, undoRedoFunctionsRef]);
|
|
918
1125
|
// Handle node changes (drag and resize events)
|
|
919
1126
|
const handleNodesChange = useCallback((changes) => {
|
|
920
1127
|
const hasDraggableNodes = draggableNodeIds && draggableNodeIds.size > 0;
|
|
@@ -979,33 +1186,50 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
979
1186
|
}));
|
|
980
1187
|
});
|
|
981
1188
|
}
|
|
982
|
-
//
|
|
1189
|
+
// Capture positions at drag START for accurate undo "before" values
|
|
1190
|
+
const dragStartChanges = changes.filter((change) => change.type === 'position' &&
|
|
1191
|
+
'id' in change &&
|
|
1192
|
+
'dragging' in change &&
|
|
1193
|
+
change.dragging === true);
|
|
1194
|
+
// For each node that just started dragging, capture its current position
|
|
1195
|
+
for (const change of dragStartChanges) {
|
|
1196
|
+
if (!dragStartPositionsRef.current.has(change.id)) {
|
|
1197
|
+
const currentNode = xyflowNodesRef.current.find((n) => n.id === change.id);
|
|
1198
|
+
if (currentNode) {
|
|
1199
|
+
dragStartPositionsRef.current.set(change.id, {
|
|
1200
|
+
x: currentNode.position.x,
|
|
1201
|
+
y: currentNode.position.y,
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
// Track position changes on drag END
|
|
983
1207
|
const positionChanges = changes.filter((change) => change.type === 'position' &&
|
|
1208
|
+
'id' in change &&
|
|
984
1209
|
'position' in change &&
|
|
985
1210
|
change.position !== undefined &&
|
|
986
1211
|
'dragging' in change &&
|
|
987
1212
|
change.dragging === false);
|
|
988
|
-
//
|
|
989
|
-
|
|
990
|
-
'dimensions' in change &&
|
|
991
|
-
change.dimensions !== undefined &&
|
|
992
|
-
'resizing' in change &&
|
|
993
|
-
change.resizing === false);
|
|
994
|
-
if (dimensionChanges.length > 0) {
|
|
995
|
-
updateEditState((prev) => {
|
|
996
|
-
const newDimensions = new Map(prev.dimensionChanges);
|
|
997
|
-
for (const change of dimensionChanges) {
|
|
998
|
-
if (change.dimensions) {
|
|
999
|
-
newDimensions.set(change.id, {
|
|
1000
|
-
width: Math.round(change.dimensions.width),
|
|
1001
|
-
height: Math.round(change.dimensions.height),
|
|
1002
|
-
});
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
return { ...prev, dimensionChanges: newDimensions };
|
|
1006
|
-
});
|
|
1007
|
-
}
|
|
1213
|
+
// Note: Dimension changes are tracked via onResizeEnd callback in GraphEditContext,
|
|
1214
|
+
// not through onNodesChange (which only fires with resizing=true during drag).
|
|
1008
1215
|
if (positionChanges.length > 0) {
|
|
1216
|
+
// Use positions captured at drag start for accurate undo
|
|
1217
|
+
const historyEntries = [];
|
|
1218
|
+
for (const change of positionChanges) {
|
|
1219
|
+
const beforePos = dragStartPositionsRef.current.get(change.id) ?? { x: 0, y: 0 };
|
|
1220
|
+
historyEntries.push({
|
|
1221
|
+
type: 'position',
|
|
1222
|
+
nodeId: change.id,
|
|
1223
|
+
before: beforePos,
|
|
1224
|
+
after: {
|
|
1225
|
+
x: Math.round(change.position.x),
|
|
1226
|
+
y: Math.round(change.position.y),
|
|
1227
|
+
},
|
|
1228
|
+
});
|
|
1229
|
+
// Clear the drag start position for this node
|
|
1230
|
+
dragStartPositionsRef.current.delete(change.id);
|
|
1231
|
+
}
|
|
1232
|
+
pushHistory(historyEntries);
|
|
1009
1233
|
updateEditState((prev) => {
|
|
1010
1234
|
const newPositions = new Map(prev.positionChanges);
|
|
1011
1235
|
for (const change of positionChanges) {
|
|
@@ -1017,7 +1241,7 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
1017
1241
|
return { ...prev, positionChanges: newPositions };
|
|
1018
1242
|
});
|
|
1019
1243
|
}
|
|
1020
|
-
}, [editable, updateEditState, selectedNodeIds, draggableNodeIds]);
|
|
1244
|
+
}, [editable, updateEditState, selectedNodeIds, draggableNodeIds, pushHistory]);
|
|
1021
1245
|
const xyflowEdgesBase = useMemo(() => {
|
|
1022
1246
|
const converted = convertToXYFlowEdges(edges, configuration, violations);
|
|
1023
1247
|
// Filter out edges connected to inactive or hidden nodes
|
|
@@ -1078,6 +1302,16 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
1078
1302
|
setXyflowLocalEdges(xyflowEdgesBase);
|
|
1079
1303
|
}
|
|
1080
1304
|
}, [baseEdgesKey, xyflowEdgesBase]);
|
|
1305
|
+
// Set the reset visual state function for use by resetEditState
|
|
1306
|
+
// This resets both nodes and edges to their original state
|
|
1307
|
+
useEffect(() => {
|
|
1308
|
+
resetVisualStateRef.current = () => {
|
|
1309
|
+
setXyflowLocalNodes(xyflowNodesBase);
|
|
1310
|
+
setXyflowLocalEdges(xyflowEdgesBase);
|
|
1311
|
+
// Notify parent that changes have been cleared
|
|
1312
|
+
onPendingChangesChange?.(false);
|
|
1313
|
+
};
|
|
1314
|
+
}, [xyflowNodesBase, xyflowEdgesBase, onPendingChangesChange]);
|
|
1081
1315
|
// Use local edges in edit mode, base edges otherwise
|
|
1082
1316
|
const xyflowEdges = editable ? xyflowLocalEdges : xyflowEdgesBase;
|
|
1083
1317
|
// Handle edge changes (selection, reconnection, etc.)
|
|
@@ -1144,7 +1378,7 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
1144
1378
|
// ============================================
|
|
1145
1379
|
// RENDER
|
|
1146
1380
|
// ============================================
|
|
1147
|
-
return (_jsxs(
|
|
1381
|
+
return (_jsxs(GraphEditProvider, { value: graphEditContextValue, children: [_jsxs(ReactFlow, { nodes: xyflowNodes, edges: xyflowEdges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, minZoom: 0.1, maxZoom: 4, defaultEdgeOptions: { type: 'custom' }, onEdgeClick: onEdgeClick, onNodeClick: onNodeClick, onNodeDoubleClick: onNodeDoubleClick, onNodeDrag: handleNodeDrag, onNodeDragStop: handleNodeDragStop, proOptions: { hideAttribution: true }, nodesDraggable: editable || (draggableNodeIds && draggableNodeIds.size > 0), elementsSelectable: editable, selectNodesOnDrag: false, nodesConnectable: editable, edgesReconnectable: editable, reconnectRadius: 25, elevateEdgesOnSelect: true, onNodesChange: handleNodesChange, onEdgesChange: handleEdgesChange, onConnect: handleConnect, onReconnectStart: handleReconnectStart, onReconnect: handleReconnect, onReconnectEnd: handleReconnectEnd, onPaneClick: onPaneClick, onSelectionChange: handleSelectionChange, panOnDrag: !editable, panOnScroll: true, zoomOnScroll: false, zoomOnPinch: true, zoomOnDoubleClick: false, selectionOnDrag: false, selectionKeyCode: null, multiSelectionKeyCode: "Shift", children: [showBackground && (_jsx(Background, { color: backgroundVariant === 'dots' ? theme.colors.border : theme.colors.textMuted, gap: backgroundGap ?? (backgroundVariant === 'dots' ? 16 : 50), size: backgroundVariant === 'dots' ? 1 : 0.5, variant: backgroundVariant === 'dots'
|
|
1148
1382
|
? BackgroundVariant.Dots
|
|
1149
1383
|
: backgroundVariant === 'lines'
|
|
1150
1384
|
? BackgroundVariant.Lines
|
|
@@ -1369,6 +1603,12 @@ export const GraphRenderer = forwardRef((props, ref) => {
|
|
|
1369
1603
|
}
|
|
1370
1604
|
// Internal edit state ref - must be before any conditional returns
|
|
1371
1605
|
const editStateRef = useRef(createEmptyEditState());
|
|
1606
|
+
// Ref to hold the reset visual state function - will be set after xyflowLocalNodes is defined
|
|
1607
|
+
const resetVisualStateRef = useRef(null);
|
|
1608
|
+
// Undo/redo management
|
|
1609
|
+
const { canUndo: canUndoState, canRedo: canRedoState, undo: undoFromStack, redo: redoFromStack, pushHistory, clearHistory, } = useUndoRedo();
|
|
1610
|
+
// Ref to hold undo/redo apply functions - will be set by inner component
|
|
1611
|
+
const undoRedoFunctionsRef = useRef(null);
|
|
1372
1612
|
// Expose imperative handle - must be before any conditional returns
|
|
1373
1613
|
useImperativeHandle(ref, () => ({
|
|
1374
1614
|
getPendingChanges: () => {
|
|
@@ -1405,6 +1645,10 @@ export const GraphRenderer = forwardRef((props, ref) => {
|
|
|
1405
1645
|
},
|
|
1406
1646
|
resetEditState: () => {
|
|
1407
1647
|
editStateRef.current = createEmptyEditState();
|
|
1648
|
+
// Also reset visual state (node positions/dimensions) if available
|
|
1649
|
+
resetVisualStateRef.current?.();
|
|
1650
|
+
// Clear undo/redo history
|
|
1651
|
+
clearHistory();
|
|
1408
1652
|
},
|
|
1409
1653
|
hasUnsavedChanges: () => {
|
|
1410
1654
|
const state = editStateRef.current;
|
|
@@ -1415,7 +1659,11 @@ export const GraphRenderer = forwardRef((props, ref) => {
|
|
|
1415
1659
|
state.createdEdges.length > 0 ||
|
|
1416
1660
|
state.deletedEdges.length > 0);
|
|
1417
1661
|
},
|
|
1418
|
-
|
|
1662
|
+
canUndo: () => canUndoState,
|
|
1663
|
+
canRedo: () => canRedoState,
|
|
1664
|
+
undo: () => undoRedoFunctionsRef.current?.applyUndo(),
|
|
1665
|
+
redo: () => undoRedoFunctionsRef.current?.applyRedo(),
|
|
1666
|
+
}), [canUndoState, canRedoState, clearHistory]);
|
|
1419
1667
|
// Validate we have required data
|
|
1420
1668
|
if (!canvasData) {
|
|
1421
1669
|
return (_jsx("div", { className: className, style: {
|
|
@@ -1431,7 +1679,7 @@ export const GraphRenderer = forwardRef((props, ref) => {
|
|
|
1431
1679
|
const { configuration, nodes, edges } = canvasData;
|
|
1432
1680
|
// Extract only the props that inner component needs
|
|
1433
1681
|
const { violations, configName, showMinimap, showControls, showBackground, backgroundVariant, backgroundGap, showCenterIndicator, showTooltips, fitViewDuration, highlightedNodeId, activeNodeIds, events, onEventProcessed, editable, onPendingChangesChange, onNodeClick, fitViewToNodeIds, fitViewPadding, draggableNodeIds, onNodeDragStop, onCopy, } = props;
|
|
1434
|
-
return (_jsx("div", { ref: containerRef, className: className, style: { width, height, position: 'relative' }, children: _jsx(TooltipPortalContext.Provider, { value: portalTarget, children: _jsx(ReactFlowProvider, { children: _jsx(GraphRendererInner, { configuration: configuration, nodes: nodes, edges: edges, violations: violations, configName: configName, showMinimap: showMinimap, showControls: showControls, showBackground: showBackground, backgroundVariant: backgroundVariant, backgroundGap: backgroundGap, showCenterIndicator: showCenterIndicator, showTooltips: showTooltips, fitViewDuration: fitViewDuration, highlightedNodeId: highlightedNodeId, activeNodeIds: activeNodeIds, events: events, onEventProcessed: onEventProcessed, editable: editable, onPendingChangesChange: onPendingChangesChange, editStateRef: editStateRef, onNodeClick: onNodeClick, fitViewToNodeIds: fitViewToNodeIds, fitViewPadding: fitViewPadding, draggableNodeIds: draggableNodeIds, onNodeDragStop: onNodeDragStop, onCopy: onCopy }) }) }) }));
|
|
1682
|
+
return (_jsx("div", { ref: containerRef, className: className, style: { width, height, position: 'relative' }, children: _jsx(TooltipPortalContext.Provider, { value: portalTarget, children: _jsx(ReactFlowProvider, { children: _jsx(GraphRendererInner, { configuration: configuration, nodes: nodes, edges: edges, violations: violations, configName: configName, showMinimap: showMinimap, showControls: showControls, showBackground: showBackground, backgroundVariant: backgroundVariant, backgroundGap: backgroundGap, showCenterIndicator: showCenterIndicator, showTooltips: showTooltips, fitViewDuration: fitViewDuration, highlightedNodeId: highlightedNodeId, activeNodeIds: activeNodeIds, events: events, onEventProcessed: onEventProcessed, editable: editable, onPendingChangesChange: onPendingChangesChange, editStateRef: editStateRef, resetVisualStateRef: resetVisualStateRef, undoRedoFunctionsRef: undoRedoFunctionsRef, pushHistory: pushHistory, clearHistory: clearHistory, undoFromStack: undoFromStack, redoFromStack: redoFromStack, onNodeClick: onNodeClick, fitViewToNodeIds: fitViewToNodeIds, fitViewPadding: fitViewPadding, draggableNodeIds: draggableNodeIds, onNodeDragStop: onNodeDragStop, onCopy: onCopy }) }) }) }));
|
|
1435
1683
|
});
|
|
1436
1684
|
GraphRenderer.displayName = 'GraphRenderer';
|
|
1437
1685
|
//# sourceMappingURL=GraphRenderer.js.map
|