@principal-ai/principal-view-react 0.13.6 → 0.13.8

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.
Files changed (37) hide show
  1. package/dist/components/GraphRenderer.d.ts +1 -15
  2. package/dist/components/GraphRenderer.d.ts.map +1 -1
  3. package/dist/components/GraphRenderer.js +94 -269
  4. package/dist/components/GraphRenderer.js.map +1 -1
  5. package/dist/components/NodeInfoPanel.d.ts.map +1 -1
  6. package/dist/components/NodeInfoPanel.js.map +1 -1
  7. package/dist/edges/CustomEdge.d.ts +2 -2
  8. package/dist/edges/CustomEdge.d.ts.map +1 -1
  9. package/dist/edges/CustomEdge.js +5 -4
  10. package/dist/edges/CustomEdge.js.map +1 -1
  11. package/dist/hooks/usePathBasedEvents.d.ts.map +1 -1
  12. package/dist/hooks/usePathBasedEvents.js +2 -1
  13. package/dist/hooks/usePathBasedEvents.js.map +1 -1
  14. package/dist/index.d.ts +0 -4
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +0 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/nodes/CustomNode.d.ts +2 -2
  19. package/dist/nodes/CustomNode.d.ts.map +1 -1
  20. package/dist/nodes/CustomNode.js +41 -10
  21. package/dist/nodes/CustomNode.js.map +1 -1
  22. package/package.json +1 -1
  23. package/src/components/GraphRenderer.tsx +106 -354
  24. package/src/edges/CustomEdge.tsx +8 -7
  25. package/src/hooks/usePathBasedEvents.ts +2 -1
  26. package/src/index.ts +0 -6
  27. package/src/nodes/CustomNode.tsx +50 -13
  28. package/src/stories/ColorPriority.stories.tsx +256 -1
  29. package/src/stories/EventDrivenAnimations.stories.tsx +332 -326
  30. package/src/stories/GraphRenderer.stories.tsx +2 -2
  31. package/src/stories/NodeDefinitionComparison.stories.tsx +1 -1
  32. package/src/stories/NodeDimensionsTesting.stories.tsx +2 -2
  33. package/src/stories/OtelComponents.stories.tsx +0 -47
  34. package/src/stories/data/graph-converter-test-execution.json +244 -26
  35. package/src/stories/data/graph-converter-validated-execution.json +6 -6
  36. package/src/components/EdgeInfoPanel.tsx +0 -247
  37. package/src/components/NodeInfoPanel.tsx +0 -724
@@ -2,6 +2,7 @@ import React, {
2
2
  useMemo,
3
3
  useState,
4
4
  useEffect,
5
+ useLayoutEffect,
5
6
  useCallback,
6
7
  useRef,
7
8
  useImperativeHandle,
@@ -21,8 +22,10 @@ import {
21
22
  applyEdgeChanges,
22
23
  type Edge,
23
24
  type EdgeChange,
25
+ type EdgeTypes,
24
26
  type NodeChange,
25
27
  type Node,
28
+ type NodeTypes,
26
29
  type Connection,
27
30
  } from '@xyflow/react';
28
31
  import '@xyflow/react/dist/style.css';
@@ -37,8 +40,6 @@ import type {
37
40
  StateEvent,
38
41
  ExtendedCanvas,
39
42
  ComponentLibrary,
40
- JsonValue,
41
- PVEventSchema,
42
43
  } from '@principal-ai/principal-view-core';
43
44
  import { CanvasConverter } from '@principal-ai/principal-view-core';
44
45
  import { useTheme } from '@principal-ade/industry-theme';
@@ -50,8 +51,6 @@ import {
50
51
  convertToXYFlowNodes,
51
52
  convertToXYFlowEdges,
52
53
  } from '../utils/graphConverter';
53
- import { EdgeInfoPanel } from './EdgeInfoPanel';
54
- import { NodeInfoPanel } from './NodeInfoPanel';
55
54
 
56
55
  /** Position change event for tracking node movements */
57
56
  export interface NodePositionChange {
@@ -201,22 +200,6 @@ interface GraphRendererBaseProps {
201
200
  */
202
201
  onNodeClick?: (nodeId: string, event: React.MouseEvent) => void;
203
202
 
204
- /**
205
- * Whether to show the node detail panel when nodes are clicked.
206
- * Defaults to undefined (auto), which shows panel only when onNodeClick is not provided.
207
- * Set to true to force showing panel even with custom onNodeClick handler.
208
- * Set to false to hide panel completely.
209
- */
210
- showNodeDetailPanel?: boolean;
211
-
212
- /**
213
- * Optional callback to resolve event references to full event schemas.
214
- * When a node has an eventRef (string reference like "order.completed"),
215
- * this function is called to retrieve the full event definition.
216
- * Return undefined if the event reference cannot be resolved.
217
- */
218
- resolveEventRef?: (eventRef: string) => PVEventSchema | undefined;
219
-
220
203
  /**
221
204
  * When set, fits the viewport to show these specific nodes.
222
205
  * Useful for focusing on a subset of the graph (e.g., scenario nodes).
@@ -246,14 +229,14 @@ export interface GraphRendererProps extends GraphRendererBaseProps {
246
229
  }
247
230
 
248
231
  // Define custom node types
249
- const nodeTypes = {
250
- custom: CustomNode as any,
251
- };
232
+ // Type assertion needed because ReactFlow's NodeTypes expects generic components
233
+ // but CustomNode is properly typed with CustomNodeData for type safety internally
234
+ const nodeTypes = { custom: CustomNode } as NodeTypes;
252
235
 
253
236
  // Define custom edge types
254
- const edgeTypes = {
255
- custom: CustomEdge as any,
256
- };
237
+ // Type assertion needed because ReactFlow's EdgeTypes expects generic components
238
+ // but CustomEdge is properly typed with CustomEdgeData for type safety internally
239
+ const edgeTypes = { custom: CustomEdge } as EdgeTypes;
257
240
 
258
241
  // Animation state for nodes and edges
259
242
  interface AnimationState {
@@ -455,8 +438,6 @@ interface GraphRendererInnerProps {
455
438
  onEditStateChange?: (editState: EditState) => void;
456
439
  editStateRef: React.MutableRefObject<EditState>;
457
440
  onNodeClick?: (nodeId: string, event: React.MouseEvent) => void;
458
- showNodeDetailPanel?: boolean;
459
- resolveEventRef?: (eventRef: string) => PVEventSchema | undefined;
460
441
  fitViewToNodeIds?: string[] | null;
461
442
  fitViewPadding?: number;
462
443
  }
@@ -487,8 +468,6 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
487
468
  onEditStateChange,
488
469
  editStateRef,
489
470
  onNodeClick: onNodeClickProp,
490
- showNodeDetailPanel,
491
- resolveEventRef,
492
471
  fitViewToNodeIds,
493
472
  fitViewPadding = 0.2,
494
473
  }) => {
@@ -539,9 +518,6 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
539
518
 
540
519
  // Track selected nodes for info panel (supports multi-select)
541
520
  const [selectedNodeIds, setSelectedNodeIds] = useState<Set<string>>(new Set());
542
- // Track whether panel should be shown (only on explicit clicks, not after dragging)
543
- const [showNodePanel, setShowNodePanel] = useState(false);
544
- const [showEdgePanel, setShowEdgePanel] = useState(false);
545
521
 
546
522
  // Track hidden nodes (shift-click to toggle)
547
523
  const [hiddenNodeIds, setHiddenNodeIds] = useState<Set<string>>(new Set());
@@ -555,14 +531,12 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
555
531
  validTypes: string[];
556
532
  } | null>(null);
557
533
 
558
- // Sync highlightedNodeId to selection state so info panel shows
534
+ // Sync highlightedNodeId to selection state
559
535
  useEffect(() => {
560
536
  if (highlightedNodeId) {
561
537
  setSelectedNodeIds(new Set([highlightedNodeId]));
562
- setShowNodePanel(true);
563
538
  // Clear edge selection when highlighting a node
564
539
  setSelectedEdgeIds(new Set());
565
- setShowEdgePanel(false);
566
540
  }
567
541
  }, [highlightedNodeId]);
568
542
 
@@ -750,19 +724,15 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
750
724
  }
751
725
  return next;
752
726
  });
753
- setShowEdgePanel(true);
754
727
  } else {
755
728
  // Regular click: single select (replace selection)
756
729
  const shouldDeselect = selectedEdgeIds.size === 1 && selectedEdgeIds.has(edge.id);
757
730
  if (shouldDeselect) {
758
731
  setSelectedEdgeIds(new Set());
759
- setShowEdgePanel(false);
760
732
  } else {
761
733
  setSelectedEdgeIds(new Set([edge.id]));
762
- setShowEdgePanel(true);
763
734
  }
764
735
  setSelectedNodeIds(new Set());
765
- setShowNodePanel(false);
766
736
  }
767
737
  },
768
738
  [editable, selectedEdgeIds]
@@ -810,12 +780,12 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
810
780
  next.add(node.id);
811
781
  }
812
782
 
813
- // Also update local nodes selection state immediately
783
+ // Update local nodes selection state immediately for edit mode
814
784
  if (editable) {
815
785
  setXyflowLocalNodes((nodes) =>
816
786
  nodes.map((n) => ({
817
787
  ...n,
818
- selected: n.id === node.id ? !prev.has(node.id) : next.has(n.id),
788
+ selected: next.has(n.id),
819
789
  }))
820
790
  );
821
791
  }
@@ -825,25 +795,15 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
825
795
  return;
826
796
  }
827
797
 
828
- // Determine if we should show the panel based on showNodeDetailPanel prop
829
- const shouldShowPanel = showNodeDetailPanel !== false && (showNodeDetailPanel === true || !onNodeClickProp);
830
-
831
- // If custom node click handler is provided, call it
832
- if (onNodeClickProp) {
833
- onNodeClickProp(node.id, event);
834
- // If showNodeDetailPanel is not explicitly true, return early (old behavior)
835
- if (showNodeDetailPanel !== true) {
836
- return;
837
- }
838
- }
839
-
840
798
  // Regular click: single select (replace selection)
799
+ event.preventDefault();
800
+ event.stopPropagation();
801
+
841
802
  const shouldDeselect = selectedNodeIds.size === 1 && selectedNodeIds.has(node.id);
842
803
  if (shouldDeselect) {
843
804
  setSelectedNodeIds(new Set());
844
- setShowNodePanel(false);
845
805
 
846
- // Also update local nodes selection state immediately
806
+ // Update local nodes selection state immediately for edit mode
847
807
  if (editable) {
848
808
  setXyflowLocalNodes((nodes) =>
849
809
  nodes.map((n) => ({
@@ -854,11 +814,8 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
854
814
  }
855
815
  } else {
856
816
  setSelectedNodeIds(new Set([node.id]));
857
- if (shouldShowPanel) {
858
- setShowNodePanel(true);
859
- }
860
817
 
861
- // Also update local nodes selection state immediately
818
+ // Update local nodes selection state immediately for edit mode
862
819
  if (editable) {
863
820
  setXyflowLocalNodes((nodes) =>
864
821
  nodes.map((n) => ({
@@ -869,48 +826,31 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
869
826
  }
870
827
  }
871
828
  setSelectedEdgeIds(new Set());
872
- setShowEdgePanel(false);
829
+
830
+ // If custom node click handler is provided, call it after selection is updated
831
+ if (onNodeClickProp) {
832
+ onNodeClickProp(node.id, event);
833
+ }
873
834
  },
874
- [selectedNodeIds, onNodeClickProp, showNodeDetailPanel, editable]
835
+ [selectedNodeIds, onNodeClickProp, editable]
875
836
  );
876
837
 
877
- // Handle close edge info panel
878
- const onCloseEdgeInfoPanel = useCallback(() => {
879
- setSelectedEdgeIds(new Set());
880
- setShowEdgePanel(false);
881
- }, []);
882
-
883
- // Handle edge side updates from EdgeInfoPanel
884
- const handleUpdateEdgeSides = useCallback((edgeId: string, fromSide: string, toSide: string) => {
885
- setLocalEdges((currentEdges) =>
886
- currentEdges.map((edge) =>
887
- edge.id === edgeId
888
- ? {
889
- ...edge,
890
- data: {
891
- ...edge.data,
892
- fromSide,
893
- toSide,
894
- },
895
- }
896
- : edge
897
- )
898
- );
899
- }, []);
900
-
901
- // Handle close node info panel
902
- const onCloseNodeInfoPanel = useCallback(() => {
903
- setSelectedNodeIds(new Set());
904
- setShowNodePanel(false);
905
- }, []);
906
838
 
907
839
  // Handle pane click (clear selection when clicking empty space)
908
840
  const onPaneClick = useCallback(() => {
909
841
  setSelectedNodeIds(new Set());
910
842
  setSelectedEdgeIds(new Set());
911
- setShowNodePanel(false);
912
- setShowEdgePanel(false);
913
- }, []);
843
+
844
+ // In edit mode, also update local nodes selection state
845
+ if (editable) {
846
+ setXyflowLocalNodes((nodes) =>
847
+ nodes.map((n) => ({
848
+ ...n,
849
+ selected: false,
850
+ }))
851
+ );
852
+ }
853
+ }, [editable]);
914
854
 
915
855
  // Handle selection change from ReactFlow (box selection and clicks)
916
856
  const handleSelectionChange = useCallback(
@@ -920,138 +860,51 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
920
860
  return;
921
861
  }
922
862
 
923
- // Always update selection state, even in read-only mode (for visual feedback)
924
- setSelectedNodeIds(new Set(selectedNodes.map((n) => n.id)));
925
- setSelectedEdgeIds(new Set(selectedEdges.map((e) => e.id)));
926
-
927
- // Only show panels in edit mode or when explicitly enabled
863
+ // In edit mode, we manage selection ourselves via onNodeClick
864
+ // Skip handleSelectionChange to avoid ReactFlow overwriting our selection state
928
865
  if (editable) {
929
- // Box selection is an explicit action, so show panels
930
- if (selectedNodes.length > 0) {
931
- setShowNodePanel(true);
932
- }
933
- if (selectedEdges.length > 0) {
934
- setShowEdgePanel(true);
935
- }
866
+ return;
936
867
  }
937
- },
938
- [editable]
939
- );
940
-
941
- // Handle node update (internal - updates local state only)
942
- const handleNodeUpdate = useCallback(
943
- (nodeId: string, updates: { type?: string; data?: Record<string, JsonValue> }) => {
944
- if (!editable) return;
945
-
946
- // Update local nodes
947
- setLocalNodes((prev) =>
948
- prev.map((node) => {
949
- if (node.id === nodeId) {
950
- return {
951
- ...node,
952
- type: updates.type ?? node.type,
953
- data: updates.data ? { ...node.data, ...updates.data } : node.data,
954
- };
955
- }
956
- return node;
957
- })
958
- );
959
868
 
960
- // Track the change
961
- updateEditState((prev) => {
962
- const newUpdates = new Map(prev.nodeUpdates);
963
- const existing = newUpdates.get(nodeId) || {};
964
- newUpdates.set(nodeId, {
965
- type: updates.type ?? existing.type,
966
- data: updates.data ? { ...existing.data, ...updates.data } : existing.data,
967
- });
968
- return { ...prev, nodeUpdates: newUpdates };
969
- });
970
- },
971
- [editable, updateEditState]
972
- );
973
-
974
- // Handle node delete (internal)
975
- const handleNodeDelete = useCallback(
976
- (nodeId: string) => {
977
- if (!editable) return;
869
+ const newSelectedNodeIds = new Set(selectedNodes.map((n) => n.id));
978
870
 
979
- // Remove from local state
980
- setLocalNodes((prev) => prev.filter((n) => n.id !== nodeId));
981
- setLocalEdges((prev) => prev.filter((e) => e.from !== nodeId && e.to !== nodeId));
982
-
983
- // Track the change
984
- updateEditState((prev) => {
985
- const newDeletedNodes = new Set(prev.deletedNodeIds);
986
- newDeletedNodes.add(nodeId);
987
- // Remove any pending updates for this node
988
- const newUpdates = new Map(prev.nodeUpdates);
989
- newUpdates.delete(nodeId);
990
- // Remove any position changes for this node
991
- const newPositions = new Map(prev.positionChanges);
992
- newPositions.delete(nodeId);
993
- // Remove any dimension changes for this node
994
- const newDimensions = new Map(prev.dimensionChanges);
995
- newDimensions.delete(nodeId);
996
- // Remove created edges that involve this node
997
- const newCreatedEdges = prev.createdEdges.filter(
998
- (e) => e.from !== nodeId && e.to !== nodeId
999
- );
1000
- return {
1001
- ...prev,
1002
- deletedNodeIds: newDeletedNodes,
1003
- nodeUpdates: newUpdates,
1004
- positionChanges: newPositions,
1005
- dimensionChanges: newDimensions,
1006
- createdEdges: newCreatedEdges,
1007
- };
1008
- });
1009
-
1010
- setSelectedNodeIds(new Set());
871
+ // Update selection state for read-only mode (for visual feedback)
872
+ setSelectedNodeIds(newSelectedNodeIds);
873
+ setSelectedEdgeIds(new Set(selectedEdges.map((e) => e.id)));
1011
874
  },
1012
- [editable, updateEditState]
875
+ [editable]
1013
876
  );
1014
877
 
1015
- // Handle edge delete (internal)
1016
- const handleEdgeDelete = useCallback(
1017
- (edgeId: string) => {
1018
- if (!editable) return;
1019
878
 
1020
- // Find the edge before removing it so we can track its full info
1021
- const edgeToDelete = localEdges.find((e) => e.id === edgeId);
879
+ // Create edge helper
880
+ const createEdge = useCallback(
881
+ (from: string, to: string, type: string, sourceHandle?: string, targetHandle?: string) => {
882
+ const edgeId = `${from}-${to}-${type}-${Date.now()}`;
1022
883
 
1023
- // Remove from local state
1024
- setLocalEdges((prev) => prev.filter((e) => e.id !== edgeId));
884
+ // Add to local state with handle information
885
+ const newEdge: EdgeState & { sourceHandle?: string; targetHandle?: string } = {
886
+ id: edgeId,
887
+ type,
888
+ from,
889
+ to,
890
+ data: {},
891
+ createdAt: Date.now(),
892
+ updatedAt: Date.now(),
893
+ sourceHandle,
894
+ targetHandle,
895
+ };
896
+ setLocalEdges((prev) => [...prev, newEdge]);
1025
897
 
1026
898
  // Track the change
1027
- updateEditState((prev) => {
1028
- // Check if this was a newly created edge
1029
- const createdEdgeIndex = prev.createdEdges.findIndex((e) => e.id === edgeId);
1030
- if (createdEdgeIndex >= 0) {
1031
- // Just remove it from created edges
1032
- const newCreatedEdges = [...prev.createdEdges];
1033
- newCreatedEdges.splice(createdEdgeIndex, 1);
1034
- return { ...prev, createdEdges: newCreatedEdges };
1035
- }
1036
- // Otherwise mark as deleted with full edge info
1037
- if (edgeToDelete) {
1038
- const newDeletedEdges = [
1039
- ...prev.deletedEdges,
1040
- {
1041
- id: edgeId,
1042
- from: edgeToDelete.from,
1043
- to: edgeToDelete.to,
1044
- type: edgeToDelete.type,
1045
- },
1046
- ];
1047
- return { ...prev, deletedEdges: newDeletedEdges };
1048
- }
1049
- return prev;
1050
- });
1051
-
1052
- setSelectedEdgeIds(new Set());
899
+ updateEditState((prev) => ({
900
+ ...prev,
901
+ createdEdges: [
902
+ ...prev.createdEdges,
903
+ { id: edgeId, from, to, type, sourceHandle, targetHandle },
904
+ ],
905
+ }));
1053
906
  },
1054
- [editable, updateEditState, localEdges]
907
+ [updateEditState]
1055
908
  );
1056
909
 
1057
910
  // Handle new connection from drag
@@ -1098,38 +951,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1098
951
  });
1099
952
  }
1100
953
  },
1101
- [editable, nodes, configuration.allowedConnections]
1102
- );
1103
-
1104
- // Create edge helper
1105
- const createEdge = useCallback(
1106
- (from: string, to: string, type: string, sourceHandle?: string, targetHandle?: string) => {
1107
- const edgeId = `${from}-${to}-${type}-${Date.now()}`;
1108
-
1109
- // Add to local state with handle information
1110
- const newEdge: EdgeState & { sourceHandle?: string; targetHandle?: string } = {
1111
- id: edgeId,
1112
- type,
1113
- from,
1114
- to,
1115
- data: {},
1116
- createdAt: Date.now(),
1117
- updatedAt: Date.now(),
1118
- sourceHandle,
1119
- targetHandle,
1120
- };
1121
- setLocalEdges((prev) => [...prev, newEdge]);
1122
-
1123
- // Track the change
1124
- updateEditState((prev) => ({
1125
- ...prev,
1126
- createdEdges: [
1127
- ...prev.createdEdges,
1128
- { id: edgeId, from, to, type, sourceHandle, targetHandle },
1129
- ],
1130
- }));
1131
- },
1132
- [updateEditState]
954
+ [editable, nodes, configuration.allowedConnections, createEdge]
1133
955
  );
1134
956
 
1135
957
  // Handle edge type selection from picker
@@ -1305,36 +1127,6 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1305
1127
  // ============================================
1306
1128
 
1307
1129
  // Get first selected edge (for single-selection info panel)
1308
- const selectedEdgeId = useMemo(() => {
1309
- if (selectedEdgeIds.size === 0) return null;
1310
- return selectedEdgeIds.values().next().value;
1311
- }, [selectedEdgeIds]);
1312
-
1313
- const selectedEdge = useMemo(() => {
1314
- if (!selectedEdgeId) return null;
1315
- return edges.find((e) => e.id === selectedEdgeId);
1316
- }, [selectedEdgeId, edges]);
1317
-
1318
- const selectedEdgeTypeDefinition = useMemo(() => {
1319
- if (!selectedEdge) return null;
1320
- return configuration.edgeTypes[selectedEdge.type];
1321
- }, [selectedEdge, configuration.edgeTypes]);
1322
-
1323
- // Get first selected node (for single-selection info panel)
1324
- const selectedNodeId = useMemo(() => {
1325
- if (selectedNodeIds.size === 0) return null;
1326
- return selectedNodeIds.values().next().value;
1327
- }, [selectedNodeIds]);
1328
-
1329
- const selectedNode = useMemo(() => {
1330
- if (!selectedNodeId) return null;
1331
- return nodes.find((n) => n.id === selectedNodeId);
1332
- }, [selectedNodeId, nodes]);
1333
-
1334
- const selectedNodeTypeDefinition = useMemo(() => {
1335
- if (!selectedNode) return null;
1336
- return configuration.nodeTypes[selectedNode.type];
1337
- }, [selectedNode, configuration.nodeTypes]);
1338
1130
 
1339
1131
  // ============================================
1340
1132
  // ANIMATIONS
@@ -1504,7 +1296,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1504
1296
  // Local xyflow nodes state for dragging
1505
1297
  const [xyflowLocalNodes, setXyflowLocalNodes] = useState<Node<CustomNodeData>[]>(xyflowNodesBase);
1506
1298
 
1507
- // Sync when base changes
1299
+ // Sync when base node IDs change
1508
1300
  const prevBaseNodesKeyRef = useRef(baseNodesKey);
1509
1301
  useEffect(() => {
1510
1302
  if (prevBaseNodesKeyRef.current !== baseNodesKey) {
@@ -1513,6 +1305,22 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1513
1305
  }
1514
1306
  }, [baseNodesKey, xyflowNodesBase]);
1515
1307
 
1308
+ // Sync selection state to local nodes when selectedNodeIds changes (edit mode only)
1309
+ // Use useLayoutEffect to ensure sync happens before paint (prevents flash)
1310
+ const prevSelectedNodeIdsRef = useRef(selectedNodeIds);
1311
+ useLayoutEffect(() => {
1312
+ if (!editable) return;
1313
+ if (prevSelectedNodeIdsRef.current === selectedNodeIds) return;
1314
+ prevSelectedNodeIdsRef.current = selectedNodeIds;
1315
+
1316
+ setXyflowLocalNodes((localNodes) =>
1317
+ localNodes.map((localNode) => ({
1318
+ ...localNode,
1319
+ selected: selectedNodeIds.has(localNode.id),
1320
+ }))
1321
+ );
1322
+ }, [editable, selectedNodeIds]);
1323
+
1516
1324
  // Also sync when entering edit mode or when base nodes change content
1517
1325
  const prevEditableRef = useRef(editable);
1518
1326
  useEffect(() => {
@@ -1554,19 +1362,20 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1554
1362
  (changes: NodeChange[]) => {
1555
1363
  if (!editable) return;
1556
1364
 
1557
- setXyflowLocalNodes((nds) => applyNodeChanges(changes, nds) as Node<CustomNodeData>[]);
1558
-
1559
- // Check if dragging started - hide panel when dragging starts
1560
- const hasDragging = changes.some(
1561
- (change) =>
1562
- change.type === 'position' &&
1563
- 'dragging' in change &&
1564
- change.dragging === true
1565
- );
1566
-
1567
- if (hasDragging) {
1568
- // Hide panel when dragging starts - it won't show again until an explicit click
1569
- setShowNodePanel(false);
1365
+ // Filter out selection changes - we manage selection ourselves via selectedNodeIds
1366
+ const nonSelectionChanges = changes.filter((change) => change.type !== 'select');
1367
+
1368
+ // Only apply changes if there are non-selection changes to apply
1369
+ if (nonSelectionChanges.length > 0) {
1370
+ setXyflowLocalNodes((nds) => {
1371
+ // Apply changes but preserve our selection state
1372
+ const updated = applyNodeChanges(nonSelectionChanges, nds) as Node<CustomNodeData>[];
1373
+ // Restore selection state from our managed selectedNodeIds
1374
+ return updated.map((node) => ({
1375
+ ...node,
1376
+ selected: selectedNodeIds.has(node.id),
1377
+ }));
1378
+ });
1570
1379
  }
1571
1380
 
1572
1381
  // Track position changes on drag end
@@ -1601,23 +1410,6 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1601
1410
  change.resizing === false
1602
1411
  );
1603
1412
 
1604
- // Debug logging for dimension changes
1605
- if (process.env.NODE_ENV === 'development') {
1606
- const allDimensionChanges = changes.filter(c => c.type === 'dimensions');
1607
- if (allDimensionChanges.length > 0) {
1608
- console.log('[GraphRenderer] Dimension changes detected:', allDimensionChanges.map(c => ({
1609
- // @ts-expect-error - accessing properties for debug
1610
- id: c.id,
1611
- // @ts-expect-error - accessing properties for debug
1612
- dimensions: c.dimensions,
1613
- // @ts-expect-error - accessing properties for debug
1614
- resizing: c.resizing,
1615
- // @ts-expect-error - accessing properties for debug
1616
- isGroup: nodes.find(n => n.id === c.id)?.data?.canvasType === 'group'
1617
- })));
1618
- }
1619
- }
1620
-
1621
1413
  if (dimensionChanges.length > 0) {
1622
1414
  updateEditState((prev) => {
1623
1415
  const newDimensions = new Map(prev.dimensionChanges);
@@ -1646,7 +1438,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1646
1438
  });
1647
1439
  }
1648
1440
  },
1649
- [editable, updateEditState]
1441
+ [editable, updateEditState, selectedNodeIds]
1650
1442
  );
1651
1443
 
1652
1444
  const xyflowEdgesBase = useMemo(() => {
@@ -1671,18 +1463,6 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1671
1463
  return true;
1672
1464
  });
1673
1465
 
1674
- // Debug: Log edge counts to help diagnose disappearing edges
1675
- if (process.env.NODE_ENV === 'development') {
1676
- console.log('[GraphRenderer] xyflowEdges computed:', {
1677
- inputEdges: edges.length,
1678
- convertedEdges: converted.length,
1679
- filteredEdges: filtered.length,
1680
- editable,
1681
- propEdgesCount: propEdges.length,
1682
- localEdgesCount: localEdges.length,
1683
- });
1684
- }
1685
-
1686
1466
  const mappedEdges = filtered.map((edge) => {
1687
1467
  const animation = animationState.edgeAnimations[edge.id];
1688
1468
  const isSelected = selectedEdgeIds.has(edge.id);
@@ -1808,7 +1588,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1808
1588
  }, 150);
1809
1589
 
1810
1590
  return () => clearTimeout(timeoutId);
1811
- }, [fitViewToNodeIdsKey, fitViewToNodeIds, fitViewPadding, fitView, fitViewDuration, getNodes]);
1591
+ }, [fitViewToNodeIdsKey, fitViewToNodeIds, fitViewPadding, fitView, fitViewDuration, getNodes, fitBounds]);
1812
1592
 
1813
1593
  // ============================================
1814
1594
  // RENDER
@@ -1831,7 +1611,8 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1831
1611
  onNodeDragStop={handleNodeDragStop}
1832
1612
  proOptions={{ hideAttribution: true }}
1833
1613
  nodesDraggable={editable}
1834
- elementsSelectable={true}
1614
+ elementsSelectable={editable}
1615
+ selectNodesOnDrag={false}
1835
1616
  nodesConnectable={editable}
1836
1617
  edgesReconnectable={editable}
1837
1618
  reconnectRadius={25}
@@ -1844,13 +1625,13 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1844
1625
  onReconnectEnd={handleReconnectEnd}
1845
1626
  onPaneClick={onPaneClick}
1846
1627
  onSelectionChange={handleSelectionChange}
1847
- panOnDrag={false}
1628
+ panOnDrag={!editable}
1848
1629
  panOnScroll={true}
1849
1630
  zoomOnScroll={false}
1850
1631
  zoomOnPinch={true}
1851
- selectionOnDrag={true}
1632
+ selectionOnDrag={false}
1852
1633
  selectionKeyCode={null}
1853
- multiSelectionKeyCode={null}
1634
+ multiSelectionKeyCode="Shift"
1854
1635
  >
1855
1636
  {showBackground && (
1856
1637
  <Background
@@ -1884,31 +1665,6 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1884
1665
  )}
1885
1666
  </ReactFlow>
1886
1667
 
1887
- {/* Single edge info panel - shown only on explicit click */}
1888
- {selectedEdgeIds.size === 1 && selectedEdge && selectedEdgeTypeDefinition && showEdgePanel && (
1889
- <EdgeInfoPanel
1890
- edge={selectedEdge}
1891
- typeDefinition={selectedEdgeTypeDefinition}
1892
- sourceNodeId={selectedEdge.from}
1893
- targetNodeId={selectedEdge.to}
1894
- onClose={onCloseEdgeInfoPanel}
1895
- onDelete={editable ? handleEdgeDelete : undefined}
1896
- onUpdateSides={editable ? handleUpdateEdgeSides : undefined}
1897
- />
1898
- )}
1899
-
1900
- {/* Single node info panel - shown only on explicit click */}
1901
- {selectedNodeIds.size === 1 && selectedNode && selectedNodeTypeDefinition && showNodePanel && showNodeDetailPanel !== false && (
1902
- <NodeInfoPanel
1903
- node={selectedNode}
1904
- typeDefinition={selectedNodeTypeDefinition}
1905
- availableNodeTypes={configuration.nodeTypes}
1906
- onClose={onCloseNodeInfoPanel}
1907
- onDelete={editable ? handleNodeDelete : undefined}
1908
- onUpdate={editable ? handleNodeUpdate : undefined}
1909
- resolveEventRef={resolveEventRef}
1910
- />
1911
- )}
1912
1668
 
1913
1669
 
1914
1670
  {pendingConnection && (
@@ -2285,8 +2041,6 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
2285
2041
  editable,
2286
2042
  onPendingChangesChange,
2287
2043
  onNodeClick,
2288
- showNodeDetailPanel,
2289
- resolveEventRef,
2290
2044
  fitViewToNodeIds,
2291
2045
  fitViewPadding,
2292
2046
  } = props;
@@ -2316,8 +2070,6 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
2316
2070
  onPendingChangesChange={onPendingChangesChange}
2317
2071
  editStateRef={editStateRef}
2318
2072
  onNodeClick={onNodeClick}
2319
- showNodeDetailPanel={showNodeDetailPanel}
2320
- resolveEventRef={resolveEventRef}
2321
2073
  fitViewToNodeIds={fitViewToNodeIds}
2322
2074
  fitViewPadding={fitViewPadding}
2323
2075
  />