@flowuent-org/diagramming-core 1.1.2 → 1.1.4

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.2",
3
+ "version": "1.1.4",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -18,16 +18,56 @@ export const AISuggestionsPanel: React.FC<AISuggestionsPanelProps> = ({
18
18
  onClose,
19
19
  }) => {
20
20
  const { getNodes, setNodes, setEdges, getNode } = useReactFlow();
21
+ const isAddingRef = React.useRef(false); // Prevent multiple simultaneous additions
22
+
23
+ const handleSuggestionClick = (suggestion: AISuggestion, event?: React.MouseEvent) => {
24
+ // Prevent event propagation
25
+ if (event) {
26
+ event.stopPropagation();
27
+ event.preventDefault();
28
+ }
29
+
30
+ // Prevent multiple rapid clicks
31
+ if (isAddingRef.current) {
32
+ return;
33
+ }
34
+ isAddingRef.current = true;
35
+
36
+ // Close the panel immediately to prevent multiple clicks
37
+ if (onClose) {
38
+ onClose();
39
+ }
21
40
 
22
- const handleSuggestionClick = (suggestion: AISuggestion) => {
23
41
  const nodes = getNodes();
24
42
  const parentNode = getNode(parentNodeId);
25
43
  if (!parentNode) return;
26
44
 
27
- // Calculate position for new node - to the right of the parent node
28
- // Node width + spacing
29
- const nodeWidth = parentNode.width || 336;
30
- const newNodeX = parentNode.position.x + nodeWidth + 50;
45
+ // Find the rightmost node in the workflow by X position
46
+ // This ensures suggestion nodes are added horizontally after the last node
47
+ // We'll find the node with the highest X position + width
48
+ const findRightmostNode = (): Node => {
49
+ // Get all nodes and find the one with the rightmost position
50
+ const allNodes = getNodes();
51
+ let rightmostNode = parentNode;
52
+ let maxRightEdge = parentNode.position.x + (parentNode.width || 336);
53
+
54
+ allNodes.forEach((node) => {
55
+ const nodeRightEdge = node.position.x + (node.width || 336);
56
+ if (nodeRightEdge > maxRightEdge) {
57
+ maxRightEdge = nodeRightEdge;
58
+ rightmostNode = node;
59
+ }
60
+ });
61
+
62
+ return rightmostNode;
63
+ };
64
+
65
+ const rightmostNode = findRightmostNode();
66
+
67
+ // Calculate position for new node - to the right of the rightmost node
68
+ const nodeWidth = rightmostNode.width || 336;
69
+ const spacing = 50; // Spacing between nodes
70
+ const newNodeX = rightmostNode.position.x + nodeWidth + spacing;
31
71
 
32
72
  // Create new AI Suggestion node
33
73
  const newNodeId = `ai-suggestion-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
@@ -71,10 +111,27 @@ export const AISuggestionsPanel: React.FC<AISuggestionsPanelProps> = ({
71
111
  measured: { width: 336, height: 150 },
72
112
  };
73
113
 
114
+ // Check if a suggestion node with the same title already exists to prevent duplicates
115
+ const existingSuggestionNode = nodes.find(
116
+ (n) => n.type === 'AutomationAISuggestionNode' &&
117
+ n.data?.label === suggestion.title &&
118
+ !(n.data?.formData as any)?.isConfirmed
119
+ );
120
+
121
+ if (existingSuggestionNode) {
122
+ // If a similar unconfirmed suggestion already exists, don't add another
123
+ isAddingRef.current = false;
124
+ if (onClose) {
125
+ onClose();
126
+ }
127
+ return;
128
+ }
129
+
74
130
  // Add the new node
75
- setNodes([...nodes, newNode]);
131
+ setNodes((prevNodes) => [...prevNodes, newNode]);
76
132
 
77
- // Create edge from parent to new node with dotted line (unconfirmed)
133
+ // Create edge from parent node to new node with dotted line (unconfirmed)
134
+ // The suggestion is for the parent node, but the new node is positioned after the rightmost node
78
135
  const edgeId = `edge-${parentNodeId}-${newNodeId}`;
79
136
  const newEdge: Edge = {
80
137
  id: edgeId,
@@ -108,10 +165,10 @@ export const AISuggestionsPanel: React.FC<AISuggestionsPanelProps> = ({
108
165
  onSuggestionClick(suggestion);
109
166
  }
110
167
 
111
- // Close the panel
112
- if (onClose) {
113
- onClose();
114
- }
168
+ // Reset the flag after a delay to allow the layout to settle
169
+ setTimeout(() => {
170
+ isAddingRef.current = false;
171
+ }, 1000);
115
172
  };
116
173
 
117
174
  return (
@@ -154,7 +211,7 @@ export const AISuggestionsPanel: React.FC<AISuggestionsPanelProps> = ({
154
211
  {/* Suggestions List */}
155
212
  <Box sx={{ }}>
156
213
  {suggestions.map((suggestion) => (
157
- <Card key={suggestion.id} onClick={() => handleSuggestionClick(suggestion)} sx={{ mb: 1, bgcolor: '#2563EB1A', border: '1px solid #2563EB', borderRadius: '8px', cursor: 'pointer', transition: 'all 0.2s ease', '&:last-child': { mb: 0, } }} >
214
+ <Card key={suggestion.id} onClick={(e) => handleSuggestionClick(suggestion, e)} sx={{ mb: 1, bgcolor: '#2563EB1A', border: '1px solid #2563EB', borderRadius: '8px', cursor: 'pointer', transition: 'all 0.2s ease', '&:last-child': { mb: 0, } }} >
158
215
  <CardContent>
159
216
  <Box sx={{ display: 'flex' }}>
160
217
 
@@ -295,12 +295,12 @@ export const AutomationApiNode: React.FC<AutomationApiNodeProps> = ({ data, sele
295
295
  animation: 'pulse-glow 2s ease-in-out infinite',
296
296
  '@keyframes pulse-glow': {
297
297
  '0%, 100%': {
298
- boxShadow: '0 0 20px rgba(251, 191, 36, 0.4), 0 0 40px rgba(251, 191, 36, 0.2)',
299
- borderColor: 'rgba(251, 191, 36, 0.6)',
298
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
299
+ borderColor: 'rgba(59, 130, 246, 0.6)',
300
300
  },
301
301
  '50%': {
302
- boxShadow: '0 0 30px rgba(251, 191, 36, 0.6), 0 0 60px rgba(251, 191, 36, 0.3)',
303
- borderColor: 'rgba(251, 191, 36, 0.9)',
302
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
303
+ borderColor: 'rgba(59, 130, 246, 0.9)',
304
304
  },
305
305
  },
306
306
  }),
@@ -345,19 +345,23 @@ export const AutomationApiNode: React.FC<AutomationApiNodeProps> = ({ data, sele
345
345
  </Typography>
346
346
  </Box>
347
347
  <Chip
348
- label={data.status}
348
+ label={data.status || 'Ready'}
349
349
  size="small"
350
350
  sx={{
351
351
  backgroundColor: data.status === 'Completed'
352
352
  ? 'rgba(37, 99, 235, 0.1)'
353
353
  : data.status === 'Running'
354
354
  ? 'rgba(251, 191, 36, 0.1)'
355
- : 'rgba(16, 185, 129, 0.1)',
355
+ : data.status === 'Error'
356
+ ? 'rgba(239, 68, 68, 0.1)'
357
+ : 'rgba(16, 185, 129, 0.1)',
356
358
  color: data.status === 'Completed'
357
359
  ? '#93C5FD'
358
360
  : data.status === 'Running'
359
361
  ? '#FCD34D'
360
- : '#86EFAC',
362
+ : data.status === 'Error'
363
+ ? '#FCA5A5'
364
+ : '#86EFAC',
361
365
  fontWeight: 500,
362
366
  fontSize: '12px',
363
367
  height: '24px',
@@ -224,12 +224,12 @@ export const AutomationEndNode: React.FC<AutomationEndNodeProps> = ({ data, sele
224
224
  animation: 'pulse-glow 2s ease-in-out infinite',
225
225
  '@keyframes pulse-glow': {
226
226
  '0%, 100%': {
227
- boxShadow: '0 0 20px rgba(251, 191, 36, 0.4), 0 0 40px rgba(251, 191, 36, 0.2)',
228
- borderColor: 'rgba(251, 191, 36, 0.6)',
227
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
228
+ borderColor: 'rgba(59, 130, 246, 0.6)',
229
229
  },
230
230
  '50%': {
231
- boxShadow: '0 0 30px rgba(251, 191, 36, 0.6), 0 0 60px rgba(251, 191, 36, 0.3)',
232
- borderColor: 'rgba(251, 191, 36, 0.9)',
231
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
232
+ borderColor: 'rgba(59, 130, 246, 0.9)',
233
233
  },
234
234
  },
235
235
  }),
@@ -274,19 +274,23 @@ export const AutomationEndNode: React.FC<AutomationEndNodeProps> = ({ data, sele
274
274
  </Typography>
275
275
  </Box>
276
276
  <Chip
277
- label={data.status}
277
+ label={data.status || 'Ready'}
278
278
  size="small"
279
279
  sx={{
280
280
  backgroundColor: data.status === 'Completed'
281
281
  ? 'rgba(37, 99, 235, 0.1)'
282
282
  : data.status === 'Running'
283
283
  ? 'rgba(251, 191, 36, 0.1)'
284
- : 'rgba(16, 185, 129, 0.1)',
284
+ : data.status === 'Error'
285
+ ? 'rgba(239, 68, 68, 0.1)'
286
+ : 'rgba(16, 185, 129, 0.1)',
285
287
  color: data.status === 'Completed'
286
288
  ? '#93C5FD'
287
289
  : data.status === 'Running'
288
290
  ? '#FCD34D'
289
- : '#86EFAC',
291
+ : data.status === 'Error'
292
+ ? '#FCA5A5'
293
+ : '#86EFAC',
290
294
  fontWeight: 500,
291
295
  fontSize: '12px',
292
296
  height: '24px',
@@ -298,12 +298,12 @@ export const AutomationFormattingNode: React.FC<AutomationFormattingNodeProps> =
298
298
  animation: 'pulse-glow 2s ease-in-out infinite',
299
299
  '@keyframes pulse-glow': {
300
300
  '0%, 100%': {
301
- boxShadow: '0 0 20px rgba(251, 191, 36, 0.4), 0 0 40px rgba(251, 191, 36, 0.2)',
302
- borderColor: 'rgba(251, 191, 36, 0.6)',
301
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
302
+ borderColor: 'rgba(59, 130, 246, 0.6)',
303
303
  },
304
304
  '50%': {
305
- boxShadow: '0 0 30px rgba(251, 191, 36, 0.6), 0 0 60px rgba(251, 191, 36, 0.3)',
306
- borderColor: 'rgba(251, 191, 36, 0.9)',
305
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
306
+ borderColor: 'rgba(59, 130, 246, 0.9)',
307
307
  },
308
308
  },
309
309
  }),
@@ -348,19 +348,23 @@ export const AutomationFormattingNode: React.FC<AutomationFormattingNodeProps> =
348
348
  </Typography>
349
349
  </Box>
350
350
  <Chip
351
- label={data.status}
351
+ label={data.status || 'Ready'}
352
352
  size="small"
353
353
  sx={{
354
354
  backgroundColor: data.status === 'Completed'
355
355
  ? 'rgba(37, 99, 235, 0.1)'
356
356
  : data.status === 'Running'
357
357
  ? 'rgba(251, 191, 36, 0.1)'
358
- : 'rgba(16, 185, 129, 0.1)',
358
+ : data.status === 'Error'
359
+ ? 'rgba(239, 68, 68, 0.1)'
360
+ : 'rgba(16, 185, 129, 0.1)',
359
361
  color: data.status === 'Completed'
360
362
  ? '#93C5FD'
361
363
  : data.status === 'Running'
362
364
  ? '#FCD34D'
363
- : '#86EFAC',
365
+ : data.status === 'Error'
366
+ ? '#FCA5A5'
367
+ : '#86EFAC',
364
368
  fontWeight: 500,
365
369
  fontSize: '12px',
366
370
  height: '24px',
@@ -722,6 +722,19 @@ Data: ${JSON.stringify(data, null, 2)}
722
722
  transition: 'all 0.2s ease',
723
723
  cursor: 'pointer',
724
724
  overflow: 'hidden',
725
+ ...(data.status === 'Running' && {
726
+ animation: 'pulse-glow 2s ease-in-out infinite',
727
+ '@keyframes pulse-glow': {
728
+ '0%, 100%': {
729
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
730
+ borderColor: 'rgba(59, 130, 246, 0.6)',
731
+ },
732
+ '50%': {
733
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
734
+ borderColor: 'rgba(59, 130, 246, 0.9)',
735
+ },
736
+ },
737
+ }),
725
738
  }}
726
739
  onClick={handleJsonClick}
727
740
  >
@@ -752,21 +765,44 @@ Data: ${JSON.stringify(data, null, 2)}
752
765
  </Typography>
753
766
  </Box>
754
767
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
755
- <Typography variant="body2" sx={{
756
- color: getNodeStatus() === 'Failed' ? '#ef4444' : getNodeStatus() === 'Running' ? '#f59e0b' : getNodeStatus() === 'Success' ? '#10b981' : '#f59e0b',
757
- fontSize: '12px',
758
- fontWeight: 500
759
- }}>
760
- {(() => {
761
- const status = getNodeStatus();
762
- if (status === 'Failed') return t('automation.sheetsNode.status.failed');
763
- if (status === 'Running') return t('automation.sheetsNode.status.running');
764
- if (status === 'Success') return t('automation.sheetsNode.status.success');
765
- if (status === 'Ready') return t('automation.sheetsNode.status.ready');
766
- if (status === 'Mixed Status') return t('automation.sheetsNode.status.mixed');
767
- return status;
768
+ {/* Status Chip */}
769
+ <Chip
770
+ label={(() => {
771
+ // Map node status to standard status values
772
+ const nodeStatus = getNodeStatus();
773
+ if (nodeStatus === 'Failed' || data.status === 'Error') return 'Error';
774
+ if (nodeStatus === 'Running' || data.status === 'Running') return 'Running';
775
+ if (nodeStatus === 'Success' || data.status === 'Completed') return 'Completed';
776
+ return data.status || 'Ready';
768
777
  })()}
769
- </Typography>
778
+ size="small"
779
+ sx={{
780
+ backgroundColor: (() => {
781
+ const status = data.status || 'Ready';
782
+ return status === 'Completed'
783
+ ? 'rgba(37, 99, 235, 0.1)'
784
+ : status === 'Running'
785
+ ? 'rgba(251, 191, 36, 0.1)'
786
+ : status === 'Error'
787
+ ? 'rgba(239, 68, 68, 0.1)'
788
+ : 'rgba(16, 185, 129, 0.1)';
789
+ })(),
790
+ color: (() => {
791
+ const status = data.status || 'Ready';
792
+ return status === 'Completed'
793
+ ? '#93C5FD'
794
+ : status === 'Running'
795
+ ? '#FCD34D'
796
+ : status === 'Error'
797
+ ? '#FCA5A5'
798
+ : '#86EFAC';
799
+ })(),
800
+ fontWeight: 500,
801
+ fontSize: '12px',
802
+ height: '24px',
803
+ borderRadius: '12px',
804
+ }}
805
+ />
770
806
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
771
807
  <AccessTimeIcon sx={{ fontSize: '12px', color: '#94a3b8' }} />
772
808
  <Typography variant="body2" sx={{ color: '#94a3b8', fontSize: '11px' }}>
@@ -212,12 +212,12 @@ export const AutomationStartNode: React.FC<AutomationStartNodeProps> = ({ data,
212
212
  animation: 'pulse-glow 2s ease-in-out infinite',
213
213
  '@keyframes pulse-glow': {
214
214
  '0%, 100%': {
215
- boxShadow: '0 0 20px rgba(251, 191, 36, 0.4), 0 0 40px rgba(251, 191, 36, 0.2)',
216
- borderColor: 'rgba(251, 191, 36, 0.6)',
215
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
216
+ borderColor: 'rgba(59, 130, 246, 0.6)',
217
217
  },
218
218
  '50%': {
219
- boxShadow: '0 0 30px rgba(251, 191, 36, 0.6), 0 0 60px rgba(251, 191, 36, 0.3)',
220
- borderColor: 'rgba(251, 191, 36, 0.9)',
219
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
220
+ borderColor: 'rgba(59, 130, 246, 0.9)',
221
221
  },
222
222
  },
223
223
  }),
@@ -262,19 +262,23 @@ export const AutomationStartNode: React.FC<AutomationStartNodeProps> = ({ data,
262
262
  </Typography>
263
263
  </Box>
264
264
  <Chip
265
- label={data.status}
265
+ label={data.status || 'Ready'}
266
266
  size="small"
267
267
  sx={{
268
268
  backgroundColor: data.status === 'Completed'
269
269
  ? 'rgba(37, 99, 235, 0.1)'
270
270
  : data.status === 'Running'
271
271
  ? 'rgba(251, 191, 36, 0.1)'
272
- : 'rgba(16, 185, 129, 0.1)',
272
+ : data.status === 'Error'
273
+ ? 'rgba(239, 68, 68, 0.1)'
274
+ : 'rgba(16, 185, 129, 0.1)',
273
275
  color: data.status === 'Completed'
274
276
  ? '#93C5FD'
275
277
  : data.status === 'Running'
276
278
  ? '#FCD34D'
277
- : '#86EFAC',
279
+ : data.status === 'Error'
280
+ ? '#FCA5A5'
281
+ : '#86EFAC',
278
282
  fontWeight: 500,
279
283
  fontSize: '12px',
280
284
  height: '24px',
@@ -115,6 +115,7 @@ export const DiagramContent: React.FC<DiagramContentProps> = ({
115
115
  const { getLayoutedElements } = useElk();
116
116
  const layoutDirection = useLayoutDirection();
117
117
  const setLayoutDirection = useSetLayoutDirection();
118
+ const layoutTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
118
119
 
119
120
  // Set layout direction to RIGHT for automation diagrams
120
121
  useEffect(() => {
@@ -123,10 +124,23 @@ export const DiagramContent: React.FC<DiagramContentProps> = ({
123
124
  }
124
125
  }, [diagramType, layoutDirection, setLayoutDirection]);
125
126
 
127
+ // Debounce layout recalculation to prevent continuous propagation
126
128
  useEffect(() => {
127
- getLayoutedElements();
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
128
138
 
129
- return () => { };
139
+ return () => {
140
+ if (layoutTimeoutRef.current) {
141
+ clearTimeout(layoutTimeoutRef.current);
142
+ }
143
+ };
130
144
  }, [nodes.length, edges.length, contentHeights, diagramType, layoutDirection, getLayoutedElements]);
131
145
 
132
146
  // Listen for layout direction changes