@flowuent-org/diagramming-core 1.1.4 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowuent-org/diagramming-core",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -8,6 +8,7 @@ import { ICardNode } from '../types/card-node';
8
8
  import { setPannable } from './setPannable';
9
9
  import { setSelectedNode } from './setSelectedNode';
10
10
  import { onConnect } from './onConnect';
11
+ import { onReconnect } from './onReconnect';
11
12
  import { setNodes } from './setNodes';
12
13
  import { setEdges } from './setEdges';
13
14
  import { setContentHeight } from './setContentHeight';
@@ -99,6 +100,9 @@ const createDiagramStore = ({
99
100
  goToHistoryIndex(set, get as () => DiagramStore, index),
100
101
  onConnect: (connection: Connection) =>
101
102
  onConnect(set, onChange)(connection),
103
+ onReconnect: (oldEdge: Edge, newConnection: Connection) => {
104
+ onReconnect(set, onChange)(oldEdge, newConnection);
105
+ },
102
106
  setNodes: (nodes: ICardNode[]) => {
103
107
  setNodes(set)(nodes);
104
108
  },
@@ -373,3 +377,16 @@ export const useEditFunction = (): ((func: FunctionSignature) => void) =>
373
377
 
374
378
  export const useDeleteFunction = (): ((id: string) => void) =>
375
379
  useDiagram((state) => state.deleteFunction);
380
+
381
+ // Undo/Redo hooks
382
+ export const useUndo = (): (() => void) =>
383
+ useDiagram((state) => state.undo);
384
+
385
+ export const useRedo = (): (() => void) =>
386
+ useDiagram((state) => state.redo);
387
+
388
+ export const useCanUndo = (): boolean =>
389
+ useDiagram((state) => state.historyIndex >= 0);
390
+
391
+ export const useCanRedo = (): boolean =>
392
+ useDiagram((state) => state.historyIndex < state.history.length - 1);
@@ -90,6 +90,7 @@ export interface DiagramActions {
90
90
  onNodesChange: (changes: NodeChange[]) => void;
91
91
  onEdgesChange: (changes: EdgeChange[]) => void;
92
92
  onConnect: (connection: Connection) => void;
93
+ onReconnect: (oldEdge: Edge, newConnection: Connection) => void;
93
94
  setNodes: (nodes: ICardNode[]) => void;
94
95
  setEdges: (edges: Edge[]) => void;
95
96
  setContentHeight: (id: string, height: number) => void;
@@ -0,0 +1,59 @@
1
+ // onReconnect.ts
2
+ import { Edge, Connection } from '@xyflow/react';
3
+ import { DiagramStore, OnChangeEventHandler } from './diagramStoreTypes';
4
+
5
+ export const onReconnect =
6
+ (
7
+ set: (fn: (state: DiagramStore) => DiagramStore) => void,
8
+ onChange?: OnChangeEventHandler,
9
+ ) =>
10
+ (oldEdge: Edge, newConnection: Connection) => {
11
+ if (!newConnection.source || !newConnection.target) {
12
+ // If connection is invalid, remove the edge
13
+ set((state) => {
14
+ const updatedEdges = state.edges.filter((edge) => edge.id !== oldEdge.id);
15
+ return { ...state, edges: updatedEdges };
16
+ });
17
+
18
+ if (onChange) {
19
+ onChange({
20
+ t: 'edge-change',
21
+ type: 'remove',
22
+ id: oldEdge.id,
23
+ message: 'Edge disconnected',
24
+ });
25
+ }
26
+ return;
27
+ }
28
+
29
+ set((state) => {
30
+ // Manually update the edge with new connection
31
+ const updatedEdges = state.edges.map((edge) => {
32
+ if (edge.id === oldEdge.id) {
33
+ return {
34
+ ...edge,
35
+ source: newConnection.source,
36
+ target: newConnection.target,
37
+ sourceHandle: newConnection.sourceHandle || edge.sourceHandle,
38
+ targetHandle: newConnection.targetHandle || edge.targetHandle,
39
+ };
40
+ }
41
+ return edge;
42
+ });
43
+
44
+ const reconnectedEdge = updatedEdges.find((edge) => edge.id === oldEdge.id);
45
+
46
+ if (onChange && reconnectedEdge) {
47
+ onChange({
48
+ t: 'edge-change',
49
+ type: 'replace',
50
+ id: reconnectedEdge.id,
51
+ item: reconnectedEdge,
52
+ message: 'Edge reconnected',
53
+ });
54
+ }
55
+
56
+ return { ...state, edges: updatedEdges };
57
+ });
58
+ };
59
+
@@ -1,338 +1,548 @@
1
- import React, { useEffect, useState, useMemo } from 'react';
2
- import { ConnectionMode, ReactFlow, ReactFlowProps } from '@xyflow/react';
3
- import { useTriggerEvent } from '../utils/event-hooks';
4
- import { IBlock, ICardNodeData } from '../types/card-node';
5
- import {
6
- DndContext,
7
- DragOverlay,
8
- PointerSensor,
9
- useDroppable,
10
- useSensor,
11
- useSensors,
12
- } from '@dnd-kit/core';
13
-
14
- import { Box, Fab } from '@mui/material';
15
- import { CardDataProvider } from '../contexts/CardDataProvider';
16
- import BlockWrapper from '../molecules/BlockWrapper';
17
- import useDragCallbacks from '../hooks/useDragCallbacks';
18
- import { useElk } from '../utils/elkLayout';
19
- import { useModalControls } from '../hooks/useModalControls';
20
- import { useWorkflowNodeAction } from '../hooks/useWorkflowNodeActiont';
21
- import {
22
- useContentHeights,
23
- useDiagram,
24
- useDiagramEdges,
25
- useDiagramNodes,
26
- useDiagramType,
27
- useLayoutDirection,
28
- useSetLayoutDirection,
29
- usePannable,
30
- } from '../contexts/DiagramProvider';
31
- import { useAutoRegisterFunctions } from '../hooks/useAutoRegisterFunctions';
32
- import SequenceDiagram from './sequence/SequenceDiagram';
33
- import ArchDiagram from './arch/ArchDiagram';
34
- import StateMachineDiagram from './stateMachine/StateMachineDiagram';
35
- import SystemFlowDiagram from './systemFlow/SystemFlowDiagram';
36
- import CloudArchitectureDiagram from './cloud-arch/components/CloudArchitectureDiagram';
37
- import UseCaseDiagram from './useCaseDiagram/UseCaseDiagram';
38
- import CollaborationDiagram from './collaborationDiagram/CollaborationDiagram';
39
- import Header from '../components/Header';
40
- import { PaneMouseHandler } from '../utils/event-store';
41
- import { AutomationExecutionPanel } from '../components/automation/AutomationExecutionPanel';
42
-
43
- interface DiagramContentProps
44
- extends Omit<ReactFlowProps, 'nodes' | 'edges' | 'onChange'> {
45
- className?: string;
46
- showAutomationExecutionPanel?: boolean;
47
- }
48
-
49
- export const DiagramContent: React.FC<DiagramContentProps> = ({
50
- className = '',
51
- showAutomationExecutionPanel = true,
52
- ...props
53
- }) => {
54
- // Auto-register function nodes when they are created
55
- useAutoRegisterFunctions();
56
-
57
- const handleOnPaneClick = useTriggerEvent('onPaneClick');
58
-
59
- // Use specific hooks where available
60
- const nodes = useDiagramNodes();
61
- const edges = useDiagramEdges();
62
- const onConnect = useDiagram((state) => state.onConnect);
63
- const onEdgesChange = useDiagram((state) => state.onEdgesChange);
64
- const onNodeDragEnd = useDiagram((state) => state.onNodeDragEnd);
65
- const onNodeDragStart = useDiagram((state) => state.onNodeDragStart);
66
- const onNodesChange = useDiagram((state) => state.onNodesChange);
67
- const setNodes = useDiagram((state) => state.setNodes);
68
- const diagramType = useDiagramType();
69
- const contentHeights = useContentHeights();
70
- const setShowEditEdge = useDiagram((state) => state.setSelectedEdge);
71
- const isPannable = usePannable();
72
-
73
- // Filter out hidden nodes for ReactFlow rendering
74
- const visibleNodes = useMemo(() => {
75
- return nodes.filter(node => !node.data?.isHidden);
76
- }, [nodes]);
77
-
78
- // Filter out edges connected to hidden nodes
79
- const visibleEdges = useMemo(() => {
80
- const visibleNodeIds = new Set(visibleNodes.map(node => node.id));
81
- return edges.filter(edge =>
82
- visibleNodeIds.has(edge.source) && visibleNodeIds.has(edge.target)
83
- );
84
- }, [edges, visibleNodes]);
85
-
86
- const { addNode } = useWorkflowNodeAction({
87
- isReplacing: false,
88
- variant: 'addParallelCol',
89
- addInMiddle: false,
90
- closeModal: () => setOpen(false),
91
- direction: 'left',
92
- });
93
- const [activeItem, setActiveItem] = useState<IBlock | null>(null);
94
- const [activeCard, setActiveCard] = useState<ICardNodeData | null>(null);
95
- const { setNodeRef } = useDroppable({ id: 'main' });
96
- const { open, onClick, setOpen } = useModalControls();
97
-
98
- const closeModalsOnClickOutside: PaneMouseHandler = () => {
99
- handleOnPaneClick();
100
- setShowEditEdge(null);
101
- };
102
-
103
- const sensors = useSensors(
104
- useSensor(PointerSensor, {
105
- activationConstraint: {
106
- delay: 250,
107
- distance: 5,
108
- },
109
- onActivation({ event }) {
110
- event.stopPropagation();
111
- },
112
- }),
113
- );
114
-
115
- const { getLayoutedElements } = useElk();
116
- const layoutDirection = useLayoutDirection();
117
- const setLayoutDirection = useSetLayoutDirection();
118
- const layoutTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
119
-
120
- // Set layout direction to RIGHT for automation diagrams
121
- useEffect(() => {
122
- if (diagramType === 'automation' && layoutDirection !== 'RIGHT') {
123
- setLayoutDirection('RIGHT');
124
- }
125
- }, [diagramType, layoutDirection, setLayoutDirection]);
126
-
127
- // Debounce layout recalculation to prevent continuous propagation
128
- useEffect(() => {
129
- // Clear any pending layout recalculation
130
- if (layoutTimeoutRef.current) {
131
- clearTimeout(layoutTimeoutRef.current);
132
- }
133
-
134
- // Debounce the layout recalculation
135
- layoutTimeoutRef.current = setTimeout(() => {
136
- getLayoutedElements();
137
- }, 300); // 300ms debounce delay
138
-
139
- return () => {
140
- if (layoutTimeoutRef.current) {
141
- clearTimeout(layoutTimeoutRef.current);
142
- }
143
- };
144
- }, [nodes.length, edges.length, contentHeights, diagramType, layoutDirection, getLayoutedElements]);
145
-
146
- // Listen for layout direction changes
147
- useEffect(() => {
148
- const handleLayoutDirectionChange = () => {
149
- getLayoutedElements();
150
- };
151
-
152
- window.addEventListener('layoutDirectionChanged', handleLayoutDirectionChange);
153
-
154
- return () => {
155
- window.removeEventListener('layoutDirectionChanged', handleLayoutDirectionChange);
156
- };
157
- }, [getLayoutedElements]);
158
-
159
- const { onDragEnd, onDragStart, onDragCancel } = useDragCallbacks({
160
- setActiveItem,
161
- setActiveCard,
162
- });
163
-
164
- const renderDynamicDiagram = () => {
165
- if (diagramType === 'sequence') {
166
- return (
167
- <>
168
- <SequenceDiagram sequenceNodes={visibleNodes} sequenceEdges={visibleEdges} />
169
- </>
170
- );
171
- }
172
-
173
- if (diagramType === 'arch') {
174
- return <ArchDiagram archNodes={visibleNodes as any} archEdges={visibleEdges as any} />;
175
- }
176
-
177
- if (diagramType === 'stateMachine') {
178
- return (
179
- <>
180
- <StateMachineDiagram stateNodes={visibleNodes} stateEdges={visibleEdges} />
181
- </>
182
- );
183
- }
184
- if (diagramType === 'systemFlow') {
185
- return (
186
- <>
187
- <SystemFlowDiagram systemFlowNodes={visibleNodes} systemFlowEdges={visibleEdges} />
188
- </>
189
- );
190
- }
191
- if (diagramType === 'case') {
192
- return (
193
- <>
194
- <UseCaseDiagram caseNodes={visibleNodes} caseEdges={visibleEdges} />
195
- </>
196
- );
197
- }
198
- if (diagramType === 'cloud-architecture') {
199
- return (
200
- <>
201
- <CloudArchitectureDiagram initialNodes={visibleNodes as any} initialEdges={visibleEdges as any} />
202
- </>
203
- );
204
- }
205
- if (diagramType === 'collaboration') {
206
- return (
207
- <>
208
- <CollaborationDiagram collabNodes={visibleNodes} collabEdges={visibleEdges} />
209
- </>
210
- );
211
- }
212
- if (diagramType === 'automation') {
213
- return (
214
- <>
215
- <Box
216
- sx={{ display: 'flex', justifyContent: 'space-between' }}
217
- className={className}
218
- >
219
- <Box
220
- ref={setNodeRef}
221
- sx={{ height: '100svh', width: '100svw', position: 'relative' }}
222
- >
223
- <ReactFlow
224
- panOnDrag={isPannable}
225
- nodesDraggable={isPannable}
226
- nodes={visibleNodes}
227
- onNodesChange={onNodesChange}
228
- edges={visibleEdges}
229
- onEdgesChange={onEdgesChange}
230
- fitView
231
- fitViewOptions={{
232
- padding: 0.2,
233
- minZoom: 0.3,
234
- maxZoom: 2,
235
- includeHiddenNodes: false,
236
- }}
237
- minZoom={0.1}
238
- maxZoom={2}
239
- onNodeDragStart={onNodeDragStart}
240
- onNodeDragStop={onNodeDragEnd}
241
- onConnect={onConnect}
242
- connectionMode={ConnectionMode.Strict}
243
- suppressContentEditableWarning
244
- suppressHydrationWarning
245
- onPaneClick={closeModalsOnClickOutside}
246
- {...props}
247
- />
248
- {props.children}
249
- </Box>
250
- </Box>
251
-
252
- <DragOverlay>
253
- {activeItem ? (
254
- <CardDataProvider nodeId={activeItem?.id}>
255
- <BlockWrapper data={activeItem} />
256
- </CardDataProvider>
257
- ) : null}
258
- </DragOverlay>
259
-
260
- {showAutomationExecutionPanel && (
261
- <AutomationExecutionPanel
262
- nodes={visibleNodes as any}
263
- edges={visibleEdges as any}
264
- workflowId="automation-workflow"
265
- onNodeUpdate={(nodeId, updatedData) => {
266
- const updatedNodes = nodes.map(node =>
267
- node.id === nodeId
268
- ? { ...node, data: updatedData }
269
- : node
270
- );
271
- setNodes(updatedNodes);
272
- }}
273
- />
274
- )}
275
- </>
276
- );
277
- }
278
- return (
279
- <>
280
- <Box
281
- sx={{ display: 'flex', justifyContent: 'space-between' }}
282
- className={className}
283
- >
284
- <Box
285
- ref={setNodeRef}
286
- sx={{ height: '100svh', width: '100svw', position: 'relative' }}
287
- >
288
- <Fab
289
- onClick={() => addNode('function')}
290
- sx={{ position: 'fixed', top: 450, left: 0 }}
291
- >
292
- Add Function
293
- </Fab>
294
- <ReactFlow
295
- panOnDrag={isPannable}
296
- nodesDraggable={isPannable}
297
- nodes={visibleNodes}
298
- onNodesChange={onNodesChange}
299
- edges={visibleEdges}
300
- onEdgesChange={onEdgesChange}
301
- fitView
302
- onNodeDragStart={onNodeDragStart}
303
- onNodeDragStop={onNodeDragEnd}
304
- onConnect={onConnect}
305
- connectionMode={ConnectionMode.Strict}
306
- suppressContentEditableWarning
307
- suppressHydrationWarning
308
- onPaneClick={closeModalsOnClickOutside}
309
- {...props}
310
- />
311
- {props.children}
312
- </Box>
313
- </Box>
314
-
315
- <DragOverlay>
316
- {activeItem ? (
317
- <CardDataProvider nodeId={activeItem?.id}>
318
- <BlockWrapper data={activeItem} />
319
- </CardDataProvider>
320
- ) : null}
321
- </DragOverlay>
322
- </>
323
- );
324
- };
325
-
326
- return (
327
- <DndContext
328
- onDragEnd={onDragEnd}
329
- onDragStart={onDragStart}
330
- onDragCancel={onDragCancel}
331
- sensors={sensors}
332
- >
333
- {renderDynamicDiagram()}
334
- </DndContext>
335
- );
336
- };
337
-
338
- export default DiagramContent;
1
+ import React, { useEffect, useState, useMemo, useCallback } from 'react';
2
+ import { ConnectionMode, ReactFlow, ReactFlowProps, NodeChange, applyNodeChanges } from '@xyflow/react';
3
+ import { useTriggerEvent } from '../utils/event-hooks';
4
+ import { IBlock, ICardNodeData } from '../types/card-node';
5
+ import {
6
+ DndContext,
7
+ DragOverlay,
8
+ PointerSensor,
9
+ useDroppable,
10
+ useSensor,
11
+ useSensors,
12
+ } from '@dnd-kit/core';
13
+
14
+ import { Box, Fab, IconButton, Tooltip } from '@mui/material';
15
+ import UndoIcon from '@mui/icons-material/Undo';
16
+ import RedoIcon from '@mui/icons-material/Redo';
17
+ import { CardDataProvider } from '../contexts/CardDataProvider';
18
+ import BlockWrapper from '../molecules/BlockWrapper';
19
+ import useDragCallbacks from '../hooks/useDragCallbacks';
20
+ import { useElk } from '../utils/elkLayout';
21
+ import { useModalControls } from '../hooks/useModalControls';
22
+ import { useWorkflowNodeAction } from '../hooks/useWorkflowNodeActiont';
23
+ import {
24
+ useContentHeights,
25
+ useDiagram,
26
+ useDiagramEdges,
27
+ useDiagramNodes,
28
+ useDiagramType,
29
+ useLayoutDirection,
30
+ useSetLayoutDirection,
31
+ usePannable,
32
+ useUndo,
33
+ useRedo,
34
+ useCanUndo,
35
+ useCanRedo,
36
+ } from '../contexts/DiagramProvider';
37
+ import { useAutoRegisterFunctions } from '../hooks/useAutoRegisterFunctions';
38
+ import SequenceDiagram from './sequence/SequenceDiagram';
39
+ import ArchDiagram from './arch/ArchDiagram';
40
+ import StateMachineDiagram from './stateMachine/StateMachineDiagram';
41
+ import SystemFlowDiagram from './systemFlow/SystemFlowDiagram';
42
+ import CloudArchitectureDiagram from './cloud-arch/components/CloudArchitectureDiagram';
43
+ import UseCaseDiagram from './useCaseDiagram/UseCaseDiagram';
44
+ import CollaborationDiagram from './collaborationDiagram/CollaborationDiagram';
45
+ import Header from '../components/Header';
46
+ import { PaneMouseHandler } from '../utils/event-store';
47
+ import { AutomationExecutionPanel } from '../components/automation/AutomationExecutionPanel';
48
+
49
+ interface DiagramContentProps
50
+ extends Omit<ReactFlowProps, 'nodes' | 'edges' | 'onChange'> {
51
+ className?: string;
52
+ showAutomationExecutionPanel?: boolean;
53
+ }
54
+
55
+ export const DiagramContent: React.FC<DiagramContentProps> = ({
56
+ className = '',
57
+ showAutomationExecutionPanel = true,
58
+ ...props
59
+ }) => {
60
+ // Auto-register function nodes when they are created
61
+ useAutoRegisterFunctions();
62
+
63
+ const handleOnPaneClick = useTriggerEvent('onPaneClick');
64
+
65
+ // Use specific hooks where available
66
+ const nodes = useDiagramNodes();
67
+ const edges = useDiagramEdges();
68
+ const onConnect = useDiagram((state) => state.onConnect);
69
+ const onReconnect = useDiagram((state) => state.onReconnect);
70
+ const onEdgesChange = useDiagram((state) => state.onEdgesChange);
71
+ const onNodeDragEnd = useDiagram((state) => state.onNodeDragEnd);
72
+ const onNodeDragStart = useDiagram((state) => state.onNodeDragStart);
73
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
74
+ const setNodes = useDiagram((state) => state.setNodes);
75
+ const diagramType = useDiagramType();
76
+ const contentHeights = useContentHeights();
77
+ const setShowEditEdge = useDiagram((state) => state.setSelectedEdge);
78
+ const selectedNode = useDiagram((state) => state.selectedNode);
79
+ const setSelectedNode = useDiagram((state) => state.setSelectedNode);
80
+ const isPannable = usePannable();
81
+
82
+ // Undo/Redo hooks
83
+ const undo = useUndo();
84
+ const redo = useRedo();
85
+ const canUndo = useCanUndo();
86
+ const canRedo = useCanRedo();
87
+
88
+ // Handle keyboard shortcuts for undo/redo
89
+ useEffect(() => {
90
+ const handleKeyDown = (event: KeyboardEvent) => {
91
+ // Don't trigger if user is typing in an input field
92
+ if (
93
+ event.target instanceof HTMLInputElement ||
94
+ event.target instanceof HTMLTextAreaElement ||
95
+ (event.target as any)?.isContentEditable
96
+ ) {
97
+ return;
98
+ }
99
+
100
+ // Ctrl+Z or Cmd+Z for Undo
101
+ if ((event.ctrlKey || event.metaKey) && event.key === 'z' && !event.shiftKey) {
102
+ event.preventDefault();
103
+ if (canUndo) {
104
+ undo();
105
+ }
106
+ }
107
+
108
+ // Ctrl+Y or Cmd+Shift+Z for Redo
109
+ if (
110
+ ((event.ctrlKey || event.metaKey) && event.key === 'y') ||
111
+ ((event.ctrlKey || event.metaKey) && event.key === 'z' && event.shiftKey)
112
+ ) {
113
+ event.preventDefault();
114
+ if (canRedo) {
115
+ redo();
116
+ }
117
+ }
118
+ };
119
+
120
+ document.addEventListener('keydown', handleKeyDown);
121
+ return () => {
122
+ document.removeEventListener('keydown', handleKeyDown);
123
+ };
124
+ }, [undo, redo, canUndo, canRedo]);
125
+
126
+ // Filter out hidden nodes for ReactFlow rendering
127
+ const visibleNodes = useMemo(() => {
128
+ return nodes.filter(node => !node.data?.isHidden);
129
+ }, [nodes]);
130
+
131
+ // Filter out edges connected to hidden nodes
132
+ const visibleEdges = useMemo(() => {
133
+ const visibleNodeIds = new Set(visibleNodes.map(node => node.id));
134
+ return edges.filter(edge =>
135
+ visibleNodeIds.has(edge.source) && visibleNodeIds.has(edge.target)
136
+ );
137
+ }, [edges, visibleNodes]);
138
+
139
+ const { addNode } = useWorkflowNodeAction({
140
+ isReplacing: false,
141
+ variant: 'addParallelCol',
142
+ addInMiddle: false,
143
+ closeModal: () => setOpen(false),
144
+ direction: 'left',
145
+ });
146
+ const [activeItem, setActiveItem] = useState<IBlock | null>(null);
147
+ const [activeCard, setActiveCard] = useState<ICardNodeData | null>(null);
148
+ const { setNodeRef } = useDroppable({ id: 'main' });
149
+ const { open, onClick, setOpen } = useModalControls();
150
+
151
+ const closeModalsOnClickOutside: PaneMouseHandler = () => {
152
+ handleOnPaneClick();
153
+ setShowEditEdge(null);
154
+ };
155
+
156
+ const sensors = useSensors(
157
+ useSensor(PointerSensor, {
158
+ activationConstraint: {
159
+ delay: 250,
160
+ distance: 5,
161
+ },
162
+ onActivation({ event }) {
163
+ event.stopPropagation();
164
+ },
165
+ }),
166
+ );
167
+
168
+ const { getLayoutedElements } = useElk();
169
+ const layoutDirection = useLayoutDirection();
170
+ const setLayoutDirection = useSetLayoutDirection();
171
+ const layoutTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
172
+
173
+ // Set layout direction to RIGHT for automation diagrams
174
+ useEffect(() => {
175
+ if (diagramType === 'automation' && layoutDirection !== 'RIGHT') {
176
+ setLayoutDirection('RIGHT');
177
+ }
178
+ }, [diagramType, layoutDirection, setLayoutDirection]);
179
+
180
+ // Debounce layout recalculation to prevent continuous propagation
181
+ useEffect(() => {
182
+ // Clear any pending layout recalculation
183
+ if (layoutTimeoutRef.current) {
184
+ clearTimeout(layoutTimeoutRef.current);
185
+ }
186
+
187
+ // Debounce the layout recalculation
188
+ layoutTimeoutRef.current = setTimeout(() => {
189
+ getLayoutedElements();
190
+ }, 300); // 300ms debounce delay
191
+
192
+ return () => {
193
+ if (layoutTimeoutRef.current) {
194
+ clearTimeout(layoutTimeoutRef.current);
195
+ }
196
+ };
197
+ }, [nodes.length, edges.length, contentHeights, diagramType, layoutDirection, getLayoutedElements]);
198
+
199
+ // Listen for layout direction changes
200
+ useEffect(() => {
201
+ const handleLayoutDirectionChange = () => {
202
+ getLayoutedElements();
203
+ };
204
+
205
+ window.addEventListener('layoutDirectionChanged', handleLayoutDirectionChange);
206
+
207
+ return () => {
208
+ window.removeEventListener('layoutDirectionChanged', handleLayoutDirectionChange);
209
+ };
210
+ }, [getLayoutedElements]);
211
+
212
+ const { onDragEnd, onDragStart, onDragCancel } = useDragCallbacks({
213
+ setActiveItem,
214
+ setActiveCard,
215
+ });
216
+
217
+ // Handle keyboard deletion of selected nodes (only for automation diagram)
218
+ useEffect(() => {
219
+ if (diagramType !== 'automation') return;
220
+
221
+ const handleKeyDown = (event: KeyboardEvent) => {
222
+ // Check if Delete or Backspace key is pressed
223
+ if (event.key === 'Delete' || event.key === 'Backspace') {
224
+ // Don't trigger deletion if user is typing in an input field
225
+ if (
226
+ event.target instanceof HTMLInputElement ||
227
+ event.target instanceof HTMLTextAreaElement ||
228
+ (event.target as any)?.isContentEditable
229
+ ) {
230
+ return;
231
+ }
232
+
233
+ // Check if a node is selected (either from store or ReactFlow's selection)
234
+ const selectedNodeId = selectedNode || visibleNodes.find(node => node.selected)?.id;
235
+
236
+ if (selectedNodeId) {
237
+ event.preventDefault();
238
+ event.stopPropagation();
239
+
240
+ // Find the node to delete
241
+ const nodeToDelete = nodes.find((node) => node.id === selectedNodeId);
242
+ if (!nodeToDelete) return;
243
+
244
+ // Create a remove change for the node
245
+ const nodeChanges: NodeChange[] = [
246
+ {
247
+ id: selectedNodeId,
248
+ type: 'remove',
249
+ },
250
+ ];
251
+
252
+ // Apply the changes through onNodesChange (which handles edge cleanup)
253
+ onNodesChange(nodeChanges);
254
+
255
+ // Clear the selected node
256
+ setSelectedNode(null);
257
+ }
258
+ }
259
+ };
260
+
261
+ // Add event listener
262
+ document.addEventListener('keydown', handleKeyDown);
263
+
264
+ // Cleanup on unmount
265
+ return () => {
266
+ document.removeEventListener('keydown', handleKeyDown);
267
+ };
268
+ }, [diagramType, selectedNode, nodes, visibleNodes, onNodesChange, setSelectedNode]);
269
+
270
+ const renderDynamicDiagram = () => {
271
+ if (diagramType === 'sequence') {
272
+ return (
273
+ <>
274
+ <SequenceDiagram sequenceNodes={visibleNodes} sequenceEdges={visibleEdges} />
275
+ </>
276
+ );
277
+ }
278
+
279
+ if (diagramType === 'arch') {
280
+ return <ArchDiagram archNodes={visibleNodes as any} archEdges={visibleEdges as any} />;
281
+ }
282
+
283
+ if (diagramType === 'stateMachine') {
284
+ return (
285
+ <>
286
+ <StateMachineDiagram stateNodes={visibleNodes} stateEdges={visibleEdges} />
287
+ </>
288
+ );
289
+ }
290
+ if (diagramType === 'systemFlow') {
291
+ return (
292
+ <>
293
+ <SystemFlowDiagram systemFlowNodes={visibleNodes} systemFlowEdges={visibleEdges} />
294
+ </>
295
+ );
296
+ }
297
+ if (diagramType === 'case') {
298
+ return (
299
+ <>
300
+ <UseCaseDiagram caseNodes={visibleNodes} caseEdges={visibleEdges} />
301
+ </>
302
+ );
303
+ }
304
+ if (diagramType === 'cloud-architecture') {
305
+ return (
306
+ <>
307
+ <CloudArchitectureDiagram initialNodes={visibleNodes as any} initialEdges={visibleEdges as any} />
308
+ </>
309
+ );
310
+ }
311
+ if (diagramType === 'collaboration') {
312
+ return (
313
+ <>
314
+ <CollaborationDiagram collabNodes={visibleNodes} collabEdges={visibleEdges} />
315
+ </>
316
+ );
317
+ }
318
+ if (diagramType === 'automation') {
319
+ return (
320
+ <>
321
+ <Box
322
+ sx={{ display: 'flex', justifyContent: 'space-between' }}
323
+ className={className}
324
+ >
325
+ <Box
326
+ ref={setNodeRef}
327
+ sx={{ height: '100svh', width: '100svw', position: 'relative' }}
328
+ >
329
+ {/* Undo/Redo Toolbar */}
330
+ <Box
331
+ sx={{
332
+ position: 'absolute',
333
+ top: 16,
334
+ left: 16,
335
+ zIndex: 10,
336
+ display: 'flex',
337
+ gap: 0.5,
338
+ backgroundColor: 'background.paper',
339
+ borderRadius: 2,
340
+ boxShadow: 2,
341
+ padding: 0.5,
342
+ }}
343
+ >
344
+ <Tooltip title="Undo (Ctrl+Z)" placement="bottom">
345
+ <span>
346
+ <IconButton
347
+ onClick={undo}
348
+ disabled={!canUndo}
349
+ size="small"
350
+ sx={{
351
+ color: canUndo ? 'primary.main' : 'action.disabled',
352
+ }}
353
+ >
354
+ <UndoIcon fontSize="small" />
355
+ </IconButton>
356
+ </span>
357
+ </Tooltip>
358
+ <Tooltip title="Redo (Ctrl+Y)" placement="bottom">
359
+ <span>
360
+ <IconButton
361
+ onClick={redo}
362
+ disabled={!canRedo}
363
+ size="small"
364
+ sx={{
365
+ color: canRedo ? 'primary.main' : 'action.disabled',
366
+ }}
367
+ >
368
+ <RedoIcon fontSize="small" />
369
+ </IconButton>
370
+ </span>
371
+ </Tooltip>
372
+ </Box>
373
+ <ReactFlow
374
+ panOnDrag={isPannable}
375
+ nodesDraggable={isPannable}
376
+ nodes={visibleNodes}
377
+ onNodesChange={onNodesChange}
378
+ edges={visibleEdges}
379
+ onEdgesChange={onEdgesChange}
380
+ onReconnect={onReconnect}
381
+ onNodeClick={(event, node) => {
382
+ event.stopPropagation();
383
+ setSelectedNode(node.id);
384
+ }}
385
+ onPaneClick={(event) => {
386
+ setSelectedNode(null);
387
+ closeModalsOnClickOutside(event);
388
+ }}
389
+ nodesConnectable={true}
390
+ nodesFocusable={true}
391
+ edgesFocusable={true}
392
+ elementsSelectable={true}
393
+ selectNodesOnDrag={false}
394
+ defaultEdgeOptions={{
395
+ reconnectable: true,
396
+ }}
397
+ fitView
398
+ fitViewOptions={{
399
+ padding: 0.2,
400
+ minZoom: 0.3,
401
+ maxZoom: 2,
402
+ includeHiddenNodes: false,
403
+ }}
404
+ minZoom={0.1}
405
+ maxZoom={2}
406
+ onNodeDragStart={onNodeDragStart}
407
+ onNodeDragStop={onNodeDragEnd}
408
+ onConnect={onConnect}
409
+ connectionMode={ConnectionMode.Strict}
410
+ suppressContentEditableWarning
411
+ suppressHydrationWarning
412
+ {...props}
413
+ />
414
+ {props.children}
415
+ </Box>
416
+ </Box>
417
+
418
+ <DragOverlay>
419
+ {activeItem ? (
420
+ <CardDataProvider nodeId={activeItem?.id}>
421
+ <BlockWrapper data={activeItem} />
422
+ </CardDataProvider>
423
+ ) : null}
424
+ </DragOverlay>
425
+
426
+ {showAutomationExecutionPanel && (
427
+ <AutomationExecutionPanel
428
+ nodes={visibleNodes as any}
429
+ edges={visibleEdges as any}
430
+ workflowId="automation-workflow"
431
+ onNodeUpdate={(nodeId, updatedData) => {
432
+ const updatedNodes = nodes.map(node =>
433
+ node.id === nodeId
434
+ ? { ...node, data: updatedData }
435
+ : node
436
+ );
437
+ setNodes(updatedNodes);
438
+ }}
439
+ />
440
+ )}
441
+ </>
442
+ );
443
+ }
444
+ return (
445
+ <>
446
+ <Box
447
+ sx={{ display: 'flex', justifyContent: 'space-between' }}
448
+ className={className}
449
+ >
450
+ <Box
451
+ ref={setNodeRef}
452
+ sx={{ height: '100svh', width: '100svw', position: 'relative' }}
453
+ >
454
+ {/* Undo/Redo Toolbar */}
455
+ <Box
456
+ sx={{
457
+ position: 'absolute',
458
+ top: 16,
459
+ left: 16,
460
+ zIndex: 10,
461
+ display: 'flex',
462
+ gap: 0.5,
463
+ backgroundColor: 'background.paper',
464
+ borderRadius: 2,
465
+ boxShadow: 2,
466
+ padding: 0.5,
467
+ }}
468
+ >
469
+ <Tooltip title="Undo (Ctrl+Z)" placement="bottom">
470
+ <span>
471
+ <IconButton
472
+ onClick={undo}
473
+ disabled={!canUndo}
474
+ size="small"
475
+ sx={{
476
+ color: canUndo ? 'primary.main' : 'action.disabled',
477
+ }}
478
+ >
479
+ <UndoIcon fontSize="small" />
480
+ </IconButton>
481
+ </span>
482
+ </Tooltip>
483
+ <Tooltip title="Redo (Ctrl+Y)" placement="bottom">
484
+ <span>
485
+ <IconButton
486
+ onClick={redo}
487
+ disabled={!canRedo}
488
+ size="small"
489
+ sx={{
490
+ color: canRedo ? 'primary.main' : 'action.disabled',
491
+ }}
492
+ >
493
+ <RedoIcon fontSize="small" />
494
+ </IconButton>
495
+ </span>
496
+ </Tooltip>
497
+ </Box>
498
+ <Fab
499
+ onClick={() => addNode('function')}
500
+ sx={{ position: 'fixed', top: 450, left: 0 }}
501
+ >
502
+ Add Function
503
+ </Fab>
504
+ <ReactFlow
505
+ panOnDrag={isPannable}
506
+ nodesDraggable={isPannable}
507
+ nodes={visibleNodes}
508
+ onNodesChange={onNodesChange}
509
+ edges={visibleEdges}
510
+ onEdgesChange={onEdgesChange}
511
+ fitView
512
+ onNodeDragStart={onNodeDragStart}
513
+ onNodeDragStop={onNodeDragEnd}
514
+ onConnect={onConnect}
515
+ connectionMode={ConnectionMode.Strict}
516
+ suppressContentEditableWarning
517
+ suppressHydrationWarning
518
+ onPaneClick={closeModalsOnClickOutside}
519
+ {...props}
520
+ />
521
+ {props.children}
522
+ </Box>
523
+ </Box>
524
+
525
+ <DragOverlay>
526
+ {activeItem ? (
527
+ <CardDataProvider nodeId={activeItem?.id}>
528
+ <BlockWrapper data={activeItem} />
529
+ </CardDataProvider>
530
+ ) : null}
531
+ </DragOverlay>
532
+ </>
533
+ );
534
+ };
535
+
536
+ return (
537
+ <DndContext
538
+ onDragEnd={onDragEnd}
539
+ onDragStart={onDragStart}
540
+ onDragCancel={onDragCancel}
541
+ sensors={sensors}
542
+ >
543
+ {renderDynamicDiagram()}
544
+ </DndContext>
545
+ );
546
+ };
547
+
548
+ export default DiagramContent;