@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.
@@ -28,6 +28,7 @@ import { useTranslation } from 'react-i18next';
28
28
  import { useDiagram } from '../../contexts/DiagramProvider';
29
29
  import { AISuggestion } from './AISuggestionsModal';
30
30
  import { AISuggestionsPanel } from './AISuggestionsPanel';
31
+ import { NodeActionButtons } from './NodeActionButtons';
31
32
 
32
33
  interface AutomationSheetsNodeProps {
33
34
  data: {
@@ -110,6 +111,9 @@ export const AutomationSheetsNode: React.FC<AutomationSheetsNodeProps> = ({ data
110
111
  const nodeId = useNodeId();
111
112
  const setSelectedNode = useDiagram((state) => state.setSelectedNode);
112
113
  const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
114
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
115
+ const nodes = useDiagram((state) => state.nodes);
116
+ const setNodes = useDiagram((state) => state.setNodes);
113
117
 
114
118
  // Get the icon component based on the iconName
115
119
  const IconComponent = getIconByName(data.iconName || 'TableChart');
@@ -848,35 +852,169 @@ Data: ${JSON.stringify(data, null, 2)}
848
852
  </Button>
849
853
  </Box>
850
854
 
851
- {/* Handles - Hidden but functional */}
855
+ {/* Connection Handles - Bidirectional (source + target at each position) */}
856
+ {/* Top - Source */}
857
+ <Handle
858
+ type="source"
859
+ position={Position.Top}
860
+ id="top-source"
861
+ className="connection-handle"
862
+ style={{
863
+ background: selected ? '#10B981' : '#1a1a2e',
864
+ width: '14px',
865
+ height: '14px',
866
+ border: '3px solid #10B981',
867
+ top: '-8px',
868
+ opacity: selected ? 1 : 0,
869
+ transition: 'all 0.2s ease-in-out',
870
+ cursor: 'crosshair',
871
+ zIndex: 10,
872
+ }}
873
+ />
874
+ {/* Top - Target (hidden but functional) */}
875
+ <Handle
876
+ type="target"
877
+ position={Position.Top}
878
+ id="top-target"
879
+ style={{
880
+ background: 'transparent',
881
+ width: '14px',
882
+ height: '14px',
883
+ border: 'none',
884
+ top: '-8px',
885
+ opacity: 0,
886
+ pointerEvents: selected ? 'all' : 'none',
887
+ }}
888
+ />
889
+ {/* Bottom - Source */}
890
+ <Handle
891
+ type="source"
892
+ position={Position.Bottom}
893
+ id="bottom-source"
894
+ className="connection-handle"
895
+ style={{
896
+ background: selected ? '#10B981' : '#1a1a2e',
897
+ width: '14px',
898
+ height: '14px',
899
+ border: '3px solid #10B981',
900
+ bottom: '-8px',
901
+ opacity: selected ? 1 : 0,
902
+ transition: 'all 0.2s ease-in-out',
903
+ cursor: 'crosshair',
904
+ zIndex: 10,
905
+ }}
906
+ />
907
+ {/* Bottom - Target (hidden but functional) */}
908
+ <Handle
909
+ type="target"
910
+ position={Position.Bottom}
911
+ id="bottom-target"
912
+ style={{
913
+ background: 'transparent',
914
+ width: '14px',
915
+ height: '14px',
916
+ border: 'none',
917
+ bottom: '-8px',
918
+ opacity: 0,
919
+ pointerEvents: selected ? 'all' : 'none',
920
+ }}
921
+ />
922
+ {/* Left - Source */}
923
+ <Handle
924
+ type="source"
925
+ position={Position.Left}
926
+ id="left-source"
927
+ className="connection-handle"
928
+ style={{
929
+ background: selected ? '#10B981' : '#1a1a2e',
930
+ width: '14px',
931
+ height: '14px',
932
+ border: '3px solid #10B981',
933
+ left: '-8px',
934
+ opacity: selected ? 1 : 0,
935
+ transition: 'all 0.2s ease-in-out',
936
+ cursor: 'crosshair',
937
+ zIndex: 10,
938
+ }}
939
+ />
940
+ {/* Left - Target (hidden but functional) */}
852
941
  <Handle
853
942
  type="target"
854
943
  position={Position.Left}
855
- id="left"
944
+ id="left-target"
856
945
  style={{
857
- background: '#3b82f6',
858
- width: '12px',
859
- height: '12px',
860
- border: '2px solid white',
946
+ background: 'transparent',
947
+ width: '14px',
948
+ height: '14px',
949
+ border: 'none',
861
950
  left: '-8px',
862
- opacity: 0, // Hidden but functional
951
+ opacity: 0,
952
+ pointerEvents: selected ? 'all' : 'none',
863
953
  }}
864
954
  />
955
+ {/* Right - Source */}
865
956
  <Handle
866
957
  type="source"
867
958
  position={Position.Right}
868
- id="right"
959
+ id="right-source"
960
+ className="connection-handle"
869
961
  style={{
870
- background: '#3b82f6',
871
- width: '12px',
872
- height: '12px',
873
- border: '2px solid white',
962
+ background: selected ? '#10B981' : '#1a1a2e',
963
+ width: '14px',
964
+ height: '14px',
965
+ border: '3px solid #10B981',
874
966
  right: '-8px',
875
- opacity: 0, // Hidden but functional
967
+ opacity: selected ? 1 : 0,
968
+ transition: 'all 0.2s ease-in-out',
969
+ cursor: 'crosshair',
970
+ zIndex: 10,
876
971
  }}
877
972
  />
973
+ {/* Right - Target (hidden but functional) */}
974
+ <Handle
975
+ type="target"
976
+ position={Position.Right}
977
+ id="right-target"
978
+ style={{
979
+ background: 'transparent',
980
+ width: '14px',
981
+ height: '14px',
982
+ border: 'none',
983
+ right: '-8px',
984
+ opacity: 0,
985
+ pointerEvents: selected ? 'all' : 'none',
986
+ }}
987
+ />
988
+
878
989
  </Box>
879
990
 
991
+ {/* Node Action Buttons - Shows when selected */}
992
+ <NodeActionButtons
993
+ selected={selected}
994
+ onDelete={() => {
995
+ if (nodeId && onNodesChange) {
996
+ onNodesChange([{ id: nodeId, type: 'remove' }]);
997
+ }
998
+ }}
999
+ onDuplicate={() => {
1000
+ if (nodeId) {
1001
+ const currentNode = nodes.find(n => n.id === nodeId);
1002
+ if (currentNode) {
1003
+ const newNode = {
1004
+ ...currentNode,
1005
+ id: `${currentNode.id}-copy-${Date.now()}`,
1006
+ position: {
1007
+ x: currentNode.position.x + 50,
1008
+ y: currentNode.position.y + 50,
1009
+ },
1010
+ selected: false,
1011
+ };
1012
+ setNodes([...nodes, newNode]);
1013
+ }
1014
+ }
1015
+ }}
1016
+ />
1017
+
880
1018
  {/* AI Suggestions Button - Positioned below the node box */}
881
1019
  {data.formData?.aiSuggestionsCount !== undefined && data.formData.aiSuggestionsCount > 0 && (
882
1020
  <Box
@@ -8,6 +8,7 @@ import ReactJson from 'react-json-view';
8
8
  import { getIconByName } from '../../utils/iconMapper';
9
9
  import { useTranslation } from 'react-i18next';
10
10
  import { useDiagram } from '../../contexts/DiagramProvider';
11
+ import { NodeActionButtons } from './NodeActionButtons';
11
12
 
12
13
  interface AutomationStartNodeProps {
13
14
  data: {
@@ -39,6 +40,9 @@ export const AutomationStartNode: React.FC<AutomationStartNodeProps> = ({ data,
39
40
  const nodeId = useNodeId();
40
41
  const setSelectedNode = useDiagram((state) => state.setSelectedNode);
41
42
  const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
43
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
44
+ const nodes = useDiagram((state) => state.nodes);
45
+ const setNodes = useDiagram((state) => state.setNodes);
42
46
 
43
47
  // Get the icon component based on the iconName
44
48
  const IconComponent = getIconByName(data.iconName);
@@ -195,6 +199,13 @@ export const AutomationStartNode: React.FC<AutomationStartNodeProps> = ({ data,
195
199
 
196
200
  return (
197
201
  <Box
202
+ sx={{
203
+ position: 'relative',
204
+ width: '336px',
205
+ overflow: 'visible',
206
+ }}
207
+ >
208
+ <Box
198
209
  ref={nodeRef}
199
210
  sx={{
200
211
  width: '336px',
@@ -325,18 +336,166 @@ export const AutomationStartNode: React.FC<AutomationStartNodeProps> = ({ data,
325
336
  </Box>
326
337
  </Box>
327
338
 
328
- {/* Output Handle - Hidden but functional */}
339
+ {/* Connection Handles - Bidirectional (source + target at each position) */}
340
+ {/* Top - Source */}
341
+ <Handle
342
+ type="source"
343
+ position={Position.Top}
344
+ id="top-source"
345
+ className="connection-handle"
346
+ style={{
347
+ background: selected ? '#10B981' : '#1a1a2e',
348
+ width: '14px',
349
+ height: '14px',
350
+ border: '3px solid #10B981',
351
+ top: '-8px',
352
+ opacity: selected ? 1 : 0,
353
+ transition: 'all 0.2s ease-in-out',
354
+ cursor: 'crosshair',
355
+ zIndex: 10,
356
+ }}
357
+ />
358
+ {/* Top - Target (hidden but functional) */}
359
+ <Handle
360
+ type="target"
361
+ position={Position.Top}
362
+ id="top-target"
363
+ style={{
364
+ background: 'transparent',
365
+ width: '14px',
366
+ height: '14px',
367
+ border: 'none',
368
+ top: '-8px',
369
+ opacity: 0,
370
+ pointerEvents: selected ? 'all' : 'none',
371
+ }}
372
+ />
373
+ {/* Bottom - Source */}
374
+ <Handle
375
+ type="source"
376
+ position={Position.Bottom}
377
+ id="bottom-source"
378
+ className="connection-handle"
379
+ style={{
380
+ background: selected ? '#10B981' : '#1a1a2e',
381
+ width: '14px',
382
+ height: '14px',
383
+ border: '3px solid #10B981',
384
+ bottom: '-8px',
385
+ opacity: selected ? 1 : 0,
386
+ transition: 'all 0.2s ease-in-out',
387
+ cursor: 'crosshair',
388
+ zIndex: 10,
389
+ }}
390
+ />
391
+ {/* Bottom - Target (hidden but functional) */}
392
+ <Handle
393
+ type="target"
394
+ position={Position.Bottom}
395
+ id="bottom-target"
396
+ style={{
397
+ background: 'transparent',
398
+ width: '14px',
399
+ height: '14px',
400
+ border: 'none',
401
+ bottom: '-8px',
402
+ opacity: 0,
403
+ pointerEvents: selected ? 'all' : 'none',
404
+ }}
405
+ />
406
+ {/* Left - Source */}
329
407
  <Handle
330
408
  type="source"
409
+ position={Position.Left}
410
+ id="left-source"
411
+ className="connection-handle"
412
+ style={{
413
+ background: selected ? '#10B981' : '#1a1a2e',
414
+ width: '14px',
415
+ height: '14px',
416
+ border: '3px solid #10B981',
417
+ left: '-8px',
418
+ opacity: selected ? 1 : 0,
419
+ transition: 'all 0.2s ease-in-out',
420
+ cursor: 'crosshair',
421
+ zIndex: 10,
422
+ }}
423
+ />
424
+ {/* Left - Target (hidden but functional) */}
425
+ <Handle
426
+ type="target"
427
+ position={Position.Left}
428
+ id="left-target"
429
+ style={{
430
+ background: 'transparent',
431
+ width: '14px',
432
+ height: '14px',
433
+ border: 'none',
434
+ left: '-8px',
435
+ opacity: 0,
436
+ pointerEvents: selected ? 'all' : 'none',
437
+ }}
438
+ />
439
+ {/* Right - Source */}
440
+ <Handle
441
+ type="source"
442
+ position={Position.Right}
443
+ id="right-source"
444
+ className="connection-handle"
445
+ style={{
446
+ background: selected ? '#10B981' : '#1a1a2e',
447
+ width: '14px',
448
+ height: '14px',
449
+ border: '3px solid #10B981',
450
+ right: '-8px',
451
+ opacity: selected ? 1 : 0,
452
+ transition: 'all 0.2s ease-in-out',
453
+ cursor: 'crosshair',
454
+ zIndex: 10,
455
+ }}
456
+ />
457
+ {/* Right - Target (hidden but functional) */}
458
+ <Handle
459
+ type="target"
331
460
  position={Position.Right}
332
- id="right"
461
+ id="right-target"
333
462
  style={{
334
- background: '#3b82f6',
335
- width: '12px',
336
- height: '12px',
337
- border: '2px solid white',
463
+ background: 'transparent',
464
+ width: '14px',
465
+ height: '14px',
466
+ border: 'none',
338
467
  right: '-8px',
339
- opacity: 0, // Hidden but functional
468
+ opacity: 0,
469
+ pointerEvents: selected ? 'all' : 'none',
470
+ }}
471
+ />
472
+
473
+ </Box>
474
+
475
+ {/* Node Action Buttons - Shows when selected */}
476
+ <NodeActionButtons
477
+ selected={selected}
478
+ onDelete={() => {
479
+ if (nodeId && onNodesChange) {
480
+ onNodesChange([{ id: nodeId, type: 'remove' }]);
481
+ }
482
+ }}
483
+ onDuplicate={() => {
484
+ if (nodeId) {
485
+ const currentNode = nodes.find(n => n.id === nodeId);
486
+ if (currentNode) {
487
+ const newNode = {
488
+ ...currentNode,
489
+ id: `${currentNode.id}-copy-${Date.now()}`,
490
+ position: {
491
+ x: currentNode.position.x + 50,
492
+ y: currentNode.position.y + 50,
493
+ },
494
+ selected: false,
495
+ };
496
+ setNodes([...nodes, newNode]);
497
+ }
498
+ }
340
499
  }}
341
500
  />
342
501
  </Box>
@@ -0,0 +1,145 @@
1
+ import React from 'react';
2
+ import { Box, IconButton, Tooltip } from '@mui/material';
3
+ import {
4
+ IconLayoutGrid,
5
+ IconMessage,
6
+ IconPlus,
7
+ IconCopy,
8
+ IconTrash,
9
+ } from '@tabler/icons-react';
10
+
11
+ interface NodeActionButtonsProps {
12
+ selected?: boolean;
13
+ onLayout?: () => void;
14
+ onAddNote?: () => void;
15
+ onAddToGroup?: () => void;
16
+ onDuplicate?: () => void;
17
+ onDelete?: () => void;
18
+ }
19
+
20
+ export const NodeActionButtons: React.FC<NodeActionButtonsProps> = ({
21
+ selected,
22
+ onLayout,
23
+ onAddNote,
24
+ onAddToGroup,
25
+ onDuplicate,
26
+ onDelete,
27
+ }) => {
28
+ if (!selected) return null;
29
+
30
+ return (
31
+ <Box
32
+ sx={{
33
+ position: 'absolute',
34
+ left: '100%',
35
+ top: '50%',
36
+ transform: 'translateY(-50%)',
37
+ marginLeft: '12px',
38
+ display: 'flex',
39
+ flexDirection: 'column',
40
+ gap: 0.5,
41
+ backgroundColor: 'rgba(15, 15, 35, 0.95)',
42
+ borderRadius: '12px',
43
+ border: '1px solid rgba(255, 255, 255, 0.1)',
44
+ p: 0.5,
45
+ zIndex: 1000,
46
+ boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
47
+ }}
48
+ onClick={(e) => e.stopPropagation()}
49
+ >
50
+ <Tooltip title="Layout" placement="right">
51
+ <IconButton
52
+ size="small"
53
+ onClick={(e) => {
54
+ e.stopPropagation();
55
+ onLayout?.();
56
+ }}
57
+ sx={{
58
+ color: 'rgba(255, 255, 255, 0.6)',
59
+ '&:hover': {
60
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
61
+ color: '#fff',
62
+ },
63
+ }}
64
+ >
65
+ <IconLayoutGrid size={18} />
66
+ </IconButton>
67
+ </Tooltip>
68
+ <Tooltip title="Add Note" placement="right">
69
+ <IconButton
70
+ size="small"
71
+ onClick={(e) => {
72
+ e.stopPropagation();
73
+ onAddNote?.();
74
+ }}
75
+ sx={{
76
+ color: 'rgba(255, 255, 255, 0.6)',
77
+ '&:hover': {
78
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
79
+ color: '#fff',
80
+ },
81
+ }}
82
+ >
83
+ <IconMessage size={18} />
84
+ </IconButton>
85
+ </Tooltip>
86
+ <Tooltip title="Add to Group" placement="right">
87
+ <IconButton
88
+ size="small"
89
+ onClick={(e) => {
90
+ e.stopPropagation();
91
+ onAddToGroup?.();
92
+ }}
93
+ sx={{
94
+ color: 'rgba(255, 255, 255, 0.6)',
95
+ '&:hover': {
96
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
97
+ color: '#fff',
98
+ },
99
+ }}
100
+ >
101
+ <IconPlus size={18} />
102
+ </IconButton>
103
+ </Tooltip>
104
+ <Tooltip title="Duplicate" placement="right">
105
+ <IconButton
106
+ size="small"
107
+ onClick={(e) => {
108
+ e.stopPropagation();
109
+ onDuplicate?.();
110
+ }}
111
+ sx={{
112
+ color: 'rgba(255, 255, 255, 0.6)',
113
+ '&:hover': {
114
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
115
+ color: '#fff',
116
+ },
117
+ }}
118
+ >
119
+ <IconCopy size={18} />
120
+ </IconButton>
121
+ </Tooltip>
122
+ <Tooltip title="Delete" placement="right">
123
+ <IconButton
124
+ size="small"
125
+ onClick={(e) => {
126
+ e.stopPropagation();
127
+ onDelete?.();
128
+ }}
129
+ sx={{
130
+ color: 'rgba(255, 255, 255, 0.6)',
131
+ '&:hover': {
132
+ backgroundColor: 'rgba(239, 68, 68, 0.2)',
133
+ color: '#EF4444',
134
+ },
135
+ }}
136
+ >
137
+ <IconTrash size={18} />
138
+ </IconButton>
139
+ </Tooltip>
140
+ </Box>
141
+ );
142
+ };
143
+
144
+ export default NodeActionButtons;
145
+
@@ -8,4 +8,5 @@ export { AutomationExecutionPanel } from './AutomationExecutionPanel';
8
8
  export { AutomationAISuggestionNode } from './AutomationAISuggestionNode';
9
9
  export { AISuggestionsModal, showAISuggestionsModal } from './AISuggestionsModal';
10
10
  export { AISuggestionsPanel } from './AISuggestionsPanel';
11
+ export { NodeActionButtons } from './NodeActionButtons';
11
12
  export type { AISuggestion } from './AISuggestionsModal';