@flowuent-org/diagramming-core 1.1.9 → 1.2.1

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.
@@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next';
10
10
  import { useDiagram } from '../../contexts/DiagramProvider';
11
11
  import { AISuggestion } from './AISuggestionsModal';
12
12
  import { AISuggestionsPanel } from './AISuggestionsPanel';
13
+ import { NodeActionButtons } from './NodeActionButtons';
13
14
 
14
15
  interface AutomationEndNodeProps {
15
16
  data: {
@@ -46,6 +47,9 @@ export const AutomationEndNode: React.FC<AutomationEndNodeProps> = ({ data, sele
46
47
  const nodeId = useNodeId();
47
48
  const setSelectedNode = useDiagram((state) => state.setSelectedNode);
48
49
  const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
50
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
51
+ const nodes = useDiagram((state) => state.nodes);
52
+ const setNodes = useDiagram((state) => state.setNodes);
49
53
 
50
54
  // Get the icon component based on the iconName
51
55
  const IconComponent = getIconByName(data.iconName);
@@ -337,22 +341,169 @@ export const AutomationEndNode: React.FC<AutomationEndNodeProps> = ({ data, sele
337
341
  </Box>
338
342
  </Box>
339
343
 
340
- {/* Input Handle - Hidden but functional */}
344
+ {/* Connection Handles - Bidirectional (source + target at each position) */}
345
+ {/* Top - Source */}
346
+ <Handle
347
+ type="source"
348
+ position={Position.Top}
349
+ id="top-source"
350
+ className="connection-handle"
351
+ style={{
352
+ background: selected ? '#10B981' : '#1a1a2e',
353
+ width: '14px',
354
+ height: '14px',
355
+ border: '3px solid #10B981',
356
+ top: '-8px',
357
+ opacity: selected ? 1 : 0,
358
+ transition: 'all 0.2s ease-in-out',
359
+ cursor: 'crosshair',
360
+ zIndex: 10,
361
+ }}
362
+ />
363
+ {/* Top - Target (hidden but functional) */}
364
+ <Handle
365
+ type="target"
366
+ position={Position.Top}
367
+ id="top-target"
368
+ style={{
369
+ background: 'transparent',
370
+ width: '14px',
371
+ height: '14px',
372
+ border: 'none',
373
+ top: '-8px',
374
+ opacity: 0,
375
+ pointerEvents: selected ? 'all' : 'none',
376
+ }}
377
+ />
378
+ {/* Bottom - Source */}
379
+ <Handle
380
+ type="source"
381
+ position={Position.Bottom}
382
+ id="bottom-source"
383
+ className="connection-handle"
384
+ style={{
385
+ background: selected ? '#10B981' : '#1a1a2e',
386
+ width: '14px',
387
+ height: '14px',
388
+ border: '3px solid #10B981',
389
+ bottom: '-8px',
390
+ opacity: selected ? 1 : 0,
391
+ transition: 'all 0.2s ease-in-out',
392
+ cursor: 'crosshair',
393
+ zIndex: 10,
394
+ }}
395
+ />
396
+ {/* Bottom - Target (hidden but functional) */}
341
397
  <Handle
342
398
  type="target"
399
+ position={Position.Bottom}
400
+ id="bottom-target"
401
+ style={{
402
+ background: 'transparent',
403
+ width: '14px',
404
+ height: '14px',
405
+ border: 'none',
406
+ bottom: '-8px',
407
+ opacity: 0,
408
+ pointerEvents: selected ? 'all' : 'none',
409
+ }}
410
+ />
411
+ {/* Left - Source */}
412
+ <Handle
413
+ type="source"
343
414
  position={Position.Left}
344
- id="left"
415
+ id="left-source"
416
+ className="connection-handle"
345
417
  style={{
346
- background: '#3b82f6',
347
- width: '12px',
348
- height: '12px',
349
- border: '2px solid white',
418
+ background: selected ? '#10B981' : '#1a1a2e',
419
+ width: '14px',
420
+ height: '14px',
421
+ border: '3px solid #10B981',
350
422
  left: '-8px',
351
- opacity: 0, // Hidden but functional
423
+ opacity: selected ? 1 : 0,
424
+ transition: 'all 0.2s ease-in-out',
425
+ cursor: 'crosshair',
426
+ zIndex: 10,
352
427
  }}
353
428
  />
429
+ {/* Left - Target (hidden but functional) */}
430
+ <Handle
431
+ type="target"
432
+ position={Position.Left}
433
+ id="left-target"
434
+ style={{
435
+ background: 'transparent',
436
+ width: '14px',
437
+ height: '14px',
438
+ border: 'none',
439
+ left: '-8px',
440
+ opacity: 0,
441
+ pointerEvents: selected ? 'all' : 'none',
442
+ }}
443
+ />
444
+ {/* Right - Source */}
445
+ <Handle
446
+ type="source"
447
+ position={Position.Right}
448
+ id="right-source"
449
+ className="connection-handle"
450
+ style={{
451
+ background: selected ? '#10B981' : '#1a1a2e',
452
+ width: '14px',
453
+ height: '14px',
454
+ border: '3px solid #10B981',
455
+ right: '-8px',
456
+ opacity: selected ? 1 : 0,
457
+ transition: 'all 0.2s ease-in-out',
458
+ cursor: 'crosshair',
459
+ zIndex: 10,
460
+ }}
461
+ />
462
+ {/* Right - Target (hidden but functional) */}
463
+ <Handle
464
+ type="target"
465
+ position={Position.Right}
466
+ id="right-target"
467
+ style={{
468
+ background: 'transparent',
469
+ width: '14px',
470
+ height: '14px',
471
+ border: 'none',
472
+ right: '-8px',
473
+ opacity: 0,
474
+ pointerEvents: selected ? 'all' : 'none',
475
+ }}
476
+ />
477
+
354
478
  </Box>
355
479
 
480
+ {/* Node Action Buttons - Shows when selected */}
481
+ <NodeActionButtons
482
+ selected={selected}
483
+ onDelete={() => {
484
+ if (nodeId && onNodesChange) {
485
+ onNodesChange([{ id: nodeId, type: 'remove' }]);
486
+ }
487
+ }}
488
+ onDuplicate={() => {
489
+ if (nodeId) {
490
+ const currentNode = nodes.find(n => n.id === nodeId);
491
+ if (currentNode) {
492
+ const newNode = {
493
+ ...currentNode,
494
+ id: `${currentNode.id}-copy-${Date.now()}`,
495
+ position: {
496
+ x: currentNode.position.x + 50,
497
+ y: currentNode.position.y + 50,
498
+ },
499
+ selected: false,
500
+ };
501
+ setNodes([...nodes, newNode]);
502
+ }
503
+ }
504
+ }}
505
+ />
506
+
356
507
  {/* AI Suggestions Button - Positioned below the node box */}
357
508
  {data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && (
358
509
  <Box
@@ -19,6 +19,7 @@ import { useTranslation } from 'react-i18next';
19
19
  import { useDiagram } from '../../contexts/DiagramProvider';
20
20
  import { AISuggestion } from './AISuggestionsModal';
21
21
  import { AISuggestionsPanel } from './AISuggestionsPanel';
22
+ import { NodeActionButtons } from './NodeActionButtons';
22
23
 
23
24
  interface AutomationFormattingNodeProps {
24
25
  data: {
@@ -71,6 +72,9 @@ export const AutomationFormattingNode: React.FC<AutomationFormattingNodeProps> =
71
72
  const nodeId = useNodeId();
72
73
  const setSelectedNode = useDiagram((state) => state.setSelectedNode);
73
74
  const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
75
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
76
+ const nodes = useDiagram((state) => state.nodes);
77
+ const setNodes = useDiagram((state) => state.setNodes);
74
78
 
75
79
  // Get the icon component based on the iconName
76
80
  const IconComponent = getIconByName(data.iconName);
@@ -562,35 +566,169 @@ export const AutomationFormattingNode: React.FC<AutomationFormattingNodeProps> =
562
566
  </Box>
563
567
  </Box>
564
568
 
565
- {/* Handles - Hidden but functional */}
569
+ {/* Connection Handles - Bidirectional (source + target at each position) */}
570
+ {/* Top - Source */}
571
+ <Handle
572
+ type="source"
573
+ position={Position.Top}
574
+ id="top-source"
575
+ className="connection-handle"
576
+ style={{
577
+ background: selected ? '#10B981' : '#1a1a2e',
578
+ width: '14px',
579
+ height: '14px',
580
+ border: '3px solid #10B981',
581
+ top: '-8px',
582
+ opacity: selected ? 1 : 0,
583
+ transition: 'all 0.2s ease-in-out',
584
+ cursor: 'crosshair',
585
+ zIndex: 10,
586
+ }}
587
+ />
588
+ {/* Top - Target (hidden but functional) */}
589
+ <Handle
590
+ type="target"
591
+ position={Position.Top}
592
+ id="top-target"
593
+ style={{
594
+ background: 'transparent',
595
+ width: '14px',
596
+ height: '14px',
597
+ border: 'none',
598
+ top: '-8px',
599
+ opacity: 0,
600
+ pointerEvents: selected ? 'all' : 'none',
601
+ }}
602
+ />
603
+ {/* Bottom - Source */}
604
+ <Handle
605
+ type="source"
606
+ position={Position.Bottom}
607
+ id="bottom-source"
608
+ className="connection-handle"
609
+ style={{
610
+ background: selected ? '#10B981' : '#1a1a2e',
611
+ width: '14px',
612
+ height: '14px',
613
+ border: '3px solid #10B981',
614
+ bottom: '-8px',
615
+ opacity: selected ? 1 : 0,
616
+ transition: 'all 0.2s ease-in-out',
617
+ cursor: 'crosshair',
618
+ zIndex: 10,
619
+ }}
620
+ />
621
+ {/* Bottom - Target (hidden but functional) */}
622
+ <Handle
623
+ type="target"
624
+ position={Position.Bottom}
625
+ id="bottom-target"
626
+ style={{
627
+ background: 'transparent',
628
+ width: '14px',
629
+ height: '14px',
630
+ border: 'none',
631
+ bottom: '-8px',
632
+ opacity: 0,
633
+ pointerEvents: selected ? 'all' : 'none',
634
+ }}
635
+ />
636
+ {/* Left - Source */}
637
+ <Handle
638
+ type="source"
639
+ position={Position.Left}
640
+ id="left-source"
641
+ className="connection-handle"
642
+ style={{
643
+ background: selected ? '#10B981' : '#1a1a2e',
644
+ width: '14px',
645
+ height: '14px',
646
+ border: '3px solid #10B981',
647
+ left: '-8px',
648
+ opacity: selected ? 1 : 0,
649
+ transition: 'all 0.2s ease-in-out',
650
+ cursor: 'crosshair',
651
+ zIndex: 10,
652
+ }}
653
+ />
654
+ {/* Left - Target (hidden but functional) */}
566
655
  <Handle
567
656
  type="target"
568
657
  position={Position.Left}
569
- id="left"
658
+ id="left-target"
570
659
  style={{
571
- background: '#3b82f6',
572
- width: '12px',
573
- height: '12px',
574
- border: '2px solid white',
660
+ background: 'transparent',
661
+ width: '14px',
662
+ height: '14px',
663
+ border: 'none',
575
664
  left: '-8px',
576
- opacity: 0, // Hidden but functional
665
+ opacity: 0,
666
+ pointerEvents: selected ? 'all' : 'none',
577
667
  }}
578
668
  />
669
+ {/* Right - Source */}
579
670
  <Handle
580
671
  type="source"
581
672
  position={Position.Right}
582
- id="right"
673
+ id="right-source"
674
+ className="connection-handle"
675
+ style={{
676
+ background: selected ? '#10B981' : '#1a1a2e',
677
+ width: '14px',
678
+ height: '14px',
679
+ border: '3px solid #10B981',
680
+ right: '-8px',
681
+ opacity: selected ? 1 : 0,
682
+ transition: 'all 0.2s ease-in-out',
683
+ cursor: 'crosshair',
684
+ zIndex: 10,
685
+ }}
686
+ />
687
+ {/* Right - Target (hidden but functional) */}
688
+ <Handle
689
+ type="target"
690
+ position={Position.Right}
691
+ id="right-target"
583
692
  style={{
584
- background: '#3b82f6',
585
- width: '12px',
586
- height: '12px',
587
- border: '2px solid white',
693
+ background: 'transparent',
694
+ width: '14px',
695
+ height: '14px',
696
+ border: 'none',
588
697
  right: '-8px',
589
- opacity: 0, // Hidden but functional
698
+ opacity: 0,
699
+ pointerEvents: selected ? 'all' : 'none',
590
700
  }}
591
701
  />
702
+
592
703
  </Box>
593
704
 
705
+ {/* Node Action Buttons - Shows when selected */}
706
+ <NodeActionButtons
707
+ selected={selected}
708
+ onDelete={() => {
709
+ if (nodeId && onNodesChange) {
710
+ onNodesChange([{ id: nodeId, type: 'remove' }]);
711
+ }
712
+ }}
713
+ onDuplicate={() => {
714
+ if (nodeId) {
715
+ const currentNode = nodes.find(n => n.id === nodeId);
716
+ if (currentNode) {
717
+ const newNode = {
718
+ ...currentNode,
719
+ id: `${currentNode.id}-copy-${Date.now()}`,
720
+ position: {
721
+ x: currentNode.position.x + 50,
722
+ y: currentNode.position.y + 50,
723
+ },
724
+ selected: false,
725
+ };
726
+ setNodes([...nodes, newNode]);
727
+ }
728
+ }
729
+ }}
730
+ />
731
+
594
732
  {/* AI Suggestions Button - Positioned below the node box */}
595
733
  {data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && (
596
734
  <Box
@@ -5,6 +5,8 @@ import { Description as DescriptionIcon, Lightbulb as LightbulbIcon } from '@mui
5
5
  import { getIconByName } from '../../utils/iconMapper';
6
6
  import { AISuggestion } from './AISuggestionsModal';
7
7
  import { AISuggestionsPanel } from './AISuggestionsPanel';
8
+ import { NodeActionButtons } from './NodeActionButtons';
9
+ import { useDiagram } from '../../contexts/DiagramProvider';
8
10
 
9
11
  interface AutomationNoteNodeProps {
10
12
  data: {
@@ -27,6 +29,9 @@ export const AutomationNoteNode: React.FC<AutomationNoteNodeProps> = ({ data, se
27
29
  const nodeRef = useRef<HTMLDivElement | null>(null);
28
30
  const [showSuggestions, setShowSuggestions] = useState(false);
29
31
  const nodeId = useNodeId();
32
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
33
+ const nodes = useDiagram((state) => state.nodes);
34
+ const setNodes = useDiagram((state) => state.setNodes);
30
35
 
31
36
  // Get the icon component based on the iconName, default to DescriptionIcon
32
37
  const IconComponent = getIconByName(data.iconName || 'Description');
@@ -150,35 +155,169 @@ export const AutomationNoteNode: React.FC<AutomationNoteNodeProps> = ({ data, se
150
155
  {data.noteType || 'note'}
151
156
  </Box>
152
157
 
153
- {/* Handles - Hidden but functional */}
158
+ {/* Connection Handles - Bidirectional (source + target at each position) */}
159
+ {/* Top - Source */}
160
+ <Handle
161
+ type="source"
162
+ position={Position.Top}
163
+ id="top-source"
164
+ className="connection-handle"
165
+ style={{
166
+ background: selected ? '#10B981' : '#1a1a2e',
167
+ width: '14px',
168
+ height: '14px',
169
+ border: '3px solid #10B981',
170
+ top: '-8px',
171
+ opacity: selected ? 1 : 0,
172
+ transition: 'all 0.2s ease-in-out',
173
+ cursor: 'crosshair',
174
+ zIndex: 10,
175
+ }}
176
+ />
177
+ {/* Top - Target (hidden but functional) */}
178
+ <Handle
179
+ type="target"
180
+ position={Position.Top}
181
+ id="top-target"
182
+ style={{
183
+ background: 'transparent',
184
+ width: '14px',
185
+ height: '14px',
186
+ border: 'none',
187
+ top: '-8px',
188
+ opacity: 0,
189
+ pointerEvents: selected ? 'all' : 'none',
190
+ }}
191
+ />
192
+ {/* Bottom - Source */}
193
+ <Handle
194
+ type="source"
195
+ position={Position.Bottom}
196
+ id="bottom-source"
197
+ className="connection-handle"
198
+ style={{
199
+ background: selected ? '#10B981' : '#1a1a2e',
200
+ width: '14px',
201
+ height: '14px',
202
+ border: '3px solid #10B981',
203
+ bottom: '-8px',
204
+ opacity: selected ? 1 : 0,
205
+ transition: 'all 0.2s ease-in-out',
206
+ cursor: 'crosshair',
207
+ zIndex: 10,
208
+ }}
209
+ />
210
+ {/* Bottom - Target (hidden but functional) */}
154
211
  <Handle
155
212
  type="target"
213
+ position={Position.Bottom}
214
+ id="bottom-target"
215
+ style={{
216
+ background: 'transparent',
217
+ width: '14px',
218
+ height: '14px',
219
+ border: 'none',
220
+ bottom: '-8px',
221
+ opacity: 0,
222
+ pointerEvents: selected ? 'all' : 'none',
223
+ }}
224
+ />
225
+ {/* Left - Source */}
226
+ <Handle
227
+ type="source"
156
228
  position={Position.Left}
157
- id="left"
229
+ id="left-source"
230
+ className="connection-handle"
158
231
  style={{
159
- background: colors.borderColor,
160
- width: '12px',
161
- height: '12px',
162
- border: '2px solid white',
232
+ background: selected ? '#10B981' : '#1a1a2e',
233
+ width: '14px',
234
+ height: '14px',
235
+ border: '3px solid #10B981',
163
236
  left: '-8px',
164
- opacity: 0, // Hidden but functional
237
+ opacity: selected ? 1 : 0,
238
+ transition: 'all 0.2s ease-in-out',
239
+ cursor: 'crosshair',
240
+ zIndex: 10,
165
241
  }}
166
242
  />
243
+ {/* Left - Target (hidden but functional) */}
244
+ <Handle
245
+ type="target"
246
+ position={Position.Left}
247
+ id="left-target"
248
+ style={{
249
+ background: 'transparent',
250
+ width: '14px',
251
+ height: '14px',
252
+ border: 'none',
253
+ left: '-8px',
254
+ opacity: 0,
255
+ pointerEvents: selected ? 'all' : 'none',
256
+ }}
257
+ />
258
+ {/* Right - Source */}
167
259
  <Handle
168
260
  type="source"
169
261
  position={Position.Right}
170
- id="right"
262
+ id="right-source"
263
+ className="connection-handle"
171
264
  style={{
172
- background: colors.borderColor,
173
- width: '12px',
174
- height: '12px',
175
- border: '2px solid white',
265
+ background: selected ? '#10B981' : '#1a1a2e',
266
+ width: '14px',
267
+ height: '14px',
268
+ border: '3px solid #10B981',
176
269
  right: '-8px',
177
- opacity: 0, // Hidden but functional
270
+ opacity: selected ? 1 : 0,
271
+ transition: 'all 0.2s ease-in-out',
272
+ cursor: 'crosshair',
273
+ zIndex: 10,
178
274
  }}
179
275
  />
276
+ {/* Right - Target (hidden but functional) */}
277
+ <Handle
278
+ type="target"
279
+ position={Position.Right}
280
+ id="right-target"
281
+ style={{
282
+ background: 'transparent',
283
+ width: '14px',
284
+ height: '14px',
285
+ border: 'none',
286
+ right: '-8px',
287
+ opacity: 0,
288
+ pointerEvents: selected ? 'all' : 'none',
289
+ }}
290
+ />
291
+
180
292
  </Box>
181
293
 
294
+ {/* Node Action Buttons - Shows when selected */}
295
+ <NodeActionButtons
296
+ selected={selected}
297
+ onDelete={() => {
298
+ if (nodeId && onNodesChange) {
299
+ onNodesChange([{ id: nodeId, type: 'remove' }]);
300
+ }
301
+ }}
302
+ onDuplicate={() => {
303
+ if (nodeId) {
304
+ const currentNode = nodes.find(n => n.id === nodeId);
305
+ if (currentNode) {
306
+ const newNode = {
307
+ ...currentNode,
308
+ id: `${currentNode.id}-copy-${Date.now()}`,
309
+ position: {
310
+ x: currentNode.position.x + 50,
311
+ y: currentNode.position.y + 50,
312
+ },
313
+ selected: false,
314
+ };
315
+ setNodes([...nodes, newNode]);
316
+ }
317
+ }
318
+ }}
319
+ />
320
+
182
321
  {/* AI Suggestions Button - Positioned below the node box */}
183
322
  {data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && (
184
323
  <Box