@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.
@@ -7,24 +7,93 @@ import { useSelectedNode } from '../contexts/DiagramProvider';
7
7
  // Define the props type for the component
8
8
  interface SideHandlesProps {
9
9
  className?: string;
10
+ /** Show all handles regardless of selection state */
11
+ alwaysShow?: boolean;
12
+ /** Which positions to show handles on */
13
+ positions?: ('top' | 'bottom' | 'left' | 'right')[];
10
14
  }
11
15
 
16
+ // Connection point handle styles
17
+ const handleBaseStyle: React.CSSProperties = {
18
+ width: '14px',
19
+ height: '14px',
20
+ borderRadius: '50%',
21
+ border: '3px solid #10B981',
22
+ backgroundColor: '#1a1a2e',
23
+ transition: 'all 0.2s ease-in-out',
24
+ cursor: 'crosshair',
25
+ zIndex: 10,
26
+ };
27
+
28
+ // Hidden handle style for the duplicate (allows bidirectional but only shows one visual handle)
29
+ const hiddenHandleStyle: React.CSSProperties = {
30
+ ...handleBaseStyle,
31
+ opacity: 0,
32
+ pointerEvents: 'all',
33
+ };
34
+
35
+ const handleHoverStyle = `
36
+ .connection-handle:hover {
37
+ transform: scale(1.3);
38
+ background-color: #10B981 !important;
39
+ box-shadow: 0 0 12px rgba(16, 185, 129, 0.6);
40
+ }
41
+ .connection-handle:active {
42
+ transform: scale(1.1);
43
+ background-color: #059669 !important;
44
+ }
45
+ `;
46
+
12
47
  export const SideHandles: React.FC<SideHandlesProps> = React.memo(
13
- ({ className }) => {
48
+ ({ className, alwaysShow = false, positions = ['top', 'bottom', 'left', 'right'] }) => {
14
49
  const sideHandles = useNodeSideHandles();
15
50
  const nodeId = useNodeId();
16
51
  const selectedNode = useSelectedNode();
52
+
53
+ const isSelected = selectedNode === nodeId;
54
+ const shouldShow = alwaysShow || isSelected;
55
+
56
+ // Inject hover styles
57
+ React.useEffect(() => {
58
+ const styleId = 'connection-handle-styles';
59
+ if (!document.getElementById(styleId)) {
60
+ const styleSheet = document.createElement('style');
61
+ styleSheet.id = styleId;
62
+ styleSheet.textContent = handleHoverStyle;
63
+ document.head.appendChild(styleSheet);
64
+ }
65
+ }, []);
17
66
 
18
67
  if (sideHandles) {
19
68
  return (
20
69
  <div>
70
+ {/* Left - Bidirectional */}
21
71
  <Handle
22
72
  type="source"
73
+ id={`${nodeId}-left-source`}
74
+ position={Position.Left}
75
+ className="connection-handle"
76
+ style={handleBaseStyle}
77
+ />
78
+ <Handle
79
+ type="target"
80
+ id={`${nodeId}-left-target`}
23
81
  position={Position.Left}
82
+ style={hiddenHandleStyle}
24
83
  />
84
+ {/* Right - Bidirectional */}
25
85
  <Handle
26
86
  type="source"
87
+ id={`${nodeId}-right-source`}
88
+ position={Position.Right}
89
+ className="connection-handle"
90
+ style={handleBaseStyle}
91
+ />
92
+ <Handle
93
+ type="target"
94
+ id={`${nodeId}-right-target`}
27
95
  position={Position.Right}
96
+ style={hiddenHandleStyle}
28
97
  />
29
98
  </div>
30
99
  );
@@ -32,18 +101,114 @@ export const SideHandles: React.FC<SideHandlesProps> = React.memo(
32
101
 
33
102
  return (
34
103
  <Box
35
- display={selectedNode ? undefined : 'none'}
104
+ sx={{
105
+ opacity: shouldShow ? 1 : 0,
106
+ transition: 'opacity 0.2s ease-in-out',
107
+ pointerEvents: shouldShow ? 'auto' : 'none',
108
+ '& .react-flow__handle': {
109
+ visibility: shouldShow ? 'visible' : 'hidden',
110
+ }
111
+ }}
36
112
  >
37
- <Handle
38
- type="source"
39
- id={`${nodeId}-bottom`}
40
- position={Position.Bottom}
41
- />
42
- <Handle
43
- type="target"
44
- id={`${nodeId}-top`}
45
- position={Position.Top}
46
- />
113
+ {/* Top Handle - Bidirectional (source + target) */}
114
+ {positions.includes('top') && (
115
+ <>
116
+ <Handle
117
+ type="source"
118
+ id={`${nodeId}-top-source`}
119
+ position={Position.Top}
120
+ className="connection-handle"
121
+ style={{
122
+ ...handleBaseStyle,
123
+ top: '-8px',
124
+ }}
125
+ />
126
+ <Handle
127
+ type="target"
128
+ id={`${nodeId}-top-target`}
129
+ position={Position.Top}
130
+ style={{
131
+ ...hiddenHandleStyle,
132
+ top: '-8px',
133
+ }}
134
+ />
135
+ </>
136
+ )}
137
+
138
+ {/* Bottom Handle - Bidirectional (source + target) */}
139
+ {positions.includes('bottom') && (
140
+ <>
141
+ <Handle
142
+ type="source"
143
+ id={`${nodeId}-bottom-source`}
144
+ position={Position.Bottom}
145
+ className="connection-handle"
146
+ style={{
147
+ ...handleBaseStyle,
148
+ bottom: '-8px',
149
+ }}
150
+ />
151
+ <Handle
152
+ type="target"
153
+ id={`${nodeId}-bottom-target`}
154
+ position={Position.Bottom}
155
+ style={{
156
+ ...hiddenHandleStyle,
157
+ bottom: '-8px',
158
+ }}
159
+ />
160
+ </>
161
+ )}
162
+
163
+ {/* Left Handle - Bidirectional (source + target) */}
164
+ {positions.includes('left') && (
165
+ <>
166
+ <Handle
167
+ type="source"
168
+ id={`${nodeId}-left-source`}
169
+ position={Position.Left}
170
+ className="connection-handle"
171
+ style={{
172
+ ...handleBaseStyle,
173
+ left: '-8px',
174
+ }}
175
+ />
176
+ <Handle
177
+ type="target"
178
+ id={`${nodeId}-left-target`}
179
+ position={Position.Left}
180
+ style={{
181
+ ...hiddenHandleStyle,
182
+ left: '-8px',
183
+ }}
184
+ />
185
+ </>
186
+ )}
187
+
188
+ {/* Right Handle - Bidirectional (source + target) */}
189
+ {positions.includes('right') && (
190
+ <>
191
+ <Handle
192
+ type="source"
193
+ id={`${nodeId}-right-source`}
194
+ position={Position.Right}
195
+ className="connection-handle"
196
+ style={{
197
+ ...handleBaseStyle,
198
+ right: '-8px',
199
+ }}
200
+ />
201
+ <Handle
202
+ type="target"
203
+ id={`${nodeId}-right-target`}
204
+ position={Position.Right}
205
+ style={{
206
+ ...hiddenHandleStyle,
207
+ right: '-8px',
208
+ }}
209
+ />
210
+ </>
211
+ )}
47
212
  </Box>
48
213
  );
49
214
  },
@@ -94,21 +94,26 @@ export const CustomEdgeGenerator = (type: EdgeTypes = EdgeTypes.Default) => {
94
94
  onDoubleClick={createBendPoint}
95
95
  style={{
96
96
  ...style,
97
- strokeWidth: (Number(style?.strokeWidth) || 1) * 4,
98
- strokeOpacity: selectedEdgeId === id ? 0.1 : 0,
97
+ strokeWidth: (Number(style?.strokeWidth) || 2) * 4,
98
+ strokeOpacity: selectedEdgeId === id ? 0.2 : 0.05,
99
+ stroke: style?.stroke || '#10B981',
99
100
  }}
100
101
  fill="none"
101
102
  />
102
103
 
103
- {/* Main path for the edge */}
104
+ {/* Main path for the edge - always visible */}
104
105
  <path
105
106
  id={id}
106
107
  d={edgePath}
107
108
  onClick={() => isErDiagram && toggleModal(setSelectedEdgeId)}
108
109
  onDoubleClick={createBendPoint}
109
- style={{ ...style }}
110
+ style={{
111
+ ...style,
112
+ stroke: style?.stroke || '#10B981',
113
+ strokeWidth: style?.strokeWidth || 2,
114
+ }}
110
115
  markerStart={isErDiagram ? `url(#${id}-marker-start)` : markerStart}
111
- markerEnd={isErDiagram ? `url(#${id}-marker-end)` : markerEnd}
116
+ markerEnd={isErDiagram ? `url(#${id}-marker-end)` : (markerEnd || 'url(#arrow)')}
112
117
  fill="none"
113
118
  />
114
119
  </g>
@@ -1,3 +1,56 @@
1
1
  .react-flow__edges {
2
2
  z-index: 20 !important;
3
3
  }
4
+
5
+ /* Connection Handle Styles */
6
+ .react-flow__handle {
7
+ transition: all 0.2s ease-in-out;
8
+ }
9
+
10
+ .react-flow__handle.connection-handle {
11
+ width: 14px;
12
+ height: 14px;
13
+ border-radius: 50%;
14
+ border: 3px solid #10B981;
15
+ background-color: #1a1a2e;
16
+ cursor: crosshair;
17
+ z-index: 10;
18
+ }
19
+
20
+ .react-flow__handle.connection-handle:hover {
21
+ transform: scale(1.3);
22
+ background-color: #10B981;
23
+ box-shadow: 0 0 12px rgba(16, 185, 129, 0.6);
24
+ }
25
+
26
+ .react-flow__handle.connection-handle:active,
27
+ .react-flow__handle.connection-handle.connecting {
28
+ transform: scale(1.1);
29
+ background-color: #059669;
30
+ }
31
+
32
+ /* Pulse animation for selected node handles */
33
+ @keyframes handle-pulse {
34
+ 0%, 100% {
35
+ box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4);
36
+ }
37
+ 50% {
38
+ box-shadow: 0 0 0 6px rgba(16, 185, 129, 0);
39
+ }
40
+ }
41
+
42
+ .react-flow__node.selected .connection-handle {
43
+ animation: handle-pulse 1.5s ease-in-out infinite;
44
+ }
45
+
46
+ /* Connection line styles when drawing */
47
+ .react-flow__connectionline {
48
+ stroke: #10B981;
49
+ stroke-width: 2;
50
+ }
51
+
52
+ /* Valid connection target highlight */
53
+ .react-flow__handle.connectionindicator {
54
+ background-color: #10B981;
55
+ border-color: #fff;
56
+ }
@@ -8,9 +8,68 @@ interface DiagramContainerProps
8
8
  showAutomationExecutionPanel?: boolean;
9
9
  }
10
10
 
11
+ // Default arrow marker definitions for edges
12
+ const EdgeMarkerDefinitions = () => (
13
+ <svg style={{ position: 'absolute', width: 0, height: 0 }}>
14
+ <defs>
15
+ {/* Arrow marker - Green */}
16
+ <marker
17
+ id="arrow"
18
+ viewBox="0 0 10 10"
19
+ refX="8"
20
+ refY="5"
21
+ markerWidth="6"
22
+ markerHeight="6"
23
+ orient="auto-start-reverse"
24
+ >
25
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#10B981" />
26
+ </marker>
27
+
28
+ {/* Arrow marker - Blue */}
29
+ <marker
30
+ id="arrow-blue"
31
+ viewBox="0 0 10 10"
32
+ refX="8"
33
+ refY="5"
34
+ markerWidth="6"
35
+ markerHeight="6"
36
+ orient="auto-start-reverse"
37
+ >
38
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#4858E9" />
39
+ </marker>
40
+
41
+ {/* Arrow marker - White */}
42
+ <marker
43
+ id="arrow-white"
44
+ viewBox="0 0 10 10"
45
+ refX="8"
46
+ refY="5"
47
+ markerWidth="6"
48
+ markerHeight="6"
49
+ orient="auto-start-reverse"
50
+ >
51
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#ffffff" />
52
+ </marker>
53
+
54
+ {/* Circle marker */}
55
+ <marker
56
+ id="circle"
57
+ viewBox="0 0 10 10"
58
+ refX="5"
59
+ refY="5"
60
+ markerWidth="5"
61
+ markerHeight="5"
62
+ >
63
+ <circle cx="5" cy="5" r="4" fill="#10B981" />
64
+ </marker>
65
+ </defs>
66
+ </svg>
67
+ );
68
+
11
69
  export const DiagramContainer: React.FC<DiagramContainerProps> = (props) => {
12
70
  return (
13
71
  <ReactFlowProvider>
72
+ <EdgeMarkerDefinitions />
14
73
  <DiagramContent {...props} />
15
74
  </ReactFlowProvider>
16
75
  );
@@ -45,6 +45,10 @@ export interface DiagrammingPageRef {
45
45
  getEdges: () => any[];
46
46
  // Get ordered nodes based on edge connections (arrow direction)
47
47
  getNodeOrder: () => NodeOrderResult;
48
+ // Get current state for saving
49
+ getCurrentState: () => { nodes: any[]; edges: any[] };
50
+ // Set nodes and edges (for restoring saved state)
51
+ setNodesAndEdges: (nodes: any[], edges: any[]) => void;
48
52
  }
49
53
 
50
54
  // Internal component that exposes undo/redo functionality
@@ -63,52 +67,42 @@ const DiagrammingPageInternal = forwardRef<DiagrammingPageRef, {
63
67
  const edges = useDiagram((state) => state.edges);
64
68
  const selectedNode = useDiagram((state) => state.selectedNode);
65
69
  const setNodes = useDiagram((state) => state.setNodes);
70
+ const setEdges = useDiagram((state) => state.setEdges);
66
71
 
67
72
  // Send node order to parent project whenever nodes/edges change
68
73
  useEffect(() => {
69
74
  if (edges.length > 0) {
70
- // Get only nodes that are connected by edges (exclude orphan nodes)
71
- const nodeOrder = orderNodesByEdges(nodes, edges, { includeOrphanNodes: false });
72
-
73
- // Get only connected nodes (nodes that appear in edges)
75
+ // Get only nodes that are connected by edges
74
76
  const connectedNodeIds = new Set<string>();
75
77
  edges.forEach((edge) => {
76
78
  connectedNodeIds.add(edge.source);
77
79
  connectedNodeIds.add(edge.target);
78
80
  });
79
81
 
80
- // Filter ordered nodes to only include connected ones
81
- const connectedOrderedNodes = nodeOrder.orderedNodes.filter(
82
- (node) => connectedNodeIds.has(node.id)
83
- );
82
+ // Filter nodes to only include connected ones
83
+ const connectedNodes = nodes.filter((node) => connectedNodeIds.has(node.id));
84
+
85
+ // Get node order for connected nodes only
86
+ const nodeOrder = orderNodesByEdges(connectedNodes, edges, { includeOrphanNodes: false });
84
87
 
85
- // Print node structure to console
86
- console.log('\n========== NODE ORDER BY EDGES ==========');
87
- console.log('\n📋 CONNECTED NODES ORDER (following arrows):');
88
- connectedOrderedNodes.forEach((node, index) => {
88
+ // Print only connected nodes to console
89
+ console.log('\n========== NODE ORDER (Connected Only) ==========');
90
+ console.log('\n📋 EXECUTION ORDER:');
91
+ nodeOrder.orderedNodes.forEach((node, index) => {
89
92
  console.log(` ${index + 1}. [${node.type || 'unknown'}] ${node.id}`);
90
93
  });
91
94
 
92
- console.log('\n🟢 START NODE:', nodeOrder.startNodes[0]?.id || '(none)');
93
- console.log('🔴 END NODE:', nodeOrder.endNodes[0]?.id || '(none)');
94
-
95
95
  console.log('\n🔗 EDGE ORDER:');
96
96
  edges.forEach((edge, index) => {
97
97
  console.log(` ${index + 1}. ${edge.source} ──→ ${edge.target}`);
98
98
  });
99
99
 
100
- console.log('\n📊 TOTAL CONNECTED NODES:', connectedOrderedNodes.length);
101
- console.log('==========================================\n');
102
-
103
- // Create updated result with only connected nodes
104
- const connectedNodeOrder: NodeOrderResult = {
105
- ...nodeOrder,
106
- orderedNodes: connectedOrderedNodes,
107
- };
100
+ console.log('\n📊 TOTAL:', nodeOrder.orderedNodes.length, 'nodes,', edges.length, 'edges');
101
+ console.log('=================================================\n');
108
102
 
109
103
  // Send to parent project if callback provided
110
104
  if (onNodeOrderChange) {
111
- onNodeOrderChange(connectedNodeOrder);
105
+ onNodeOrderChange(nodeOrder);
112
106
  }
113
107
  }
114
108
  }, [nodes, edges, onNodeOrderChange]);
@@ -143,6 +137,13 @@ const DiagrammingPageInternal = forwardRef<DiagrammingPageRef, {
143
137
  getEdges: () => edges,
144
138
  // Get ordered nodes based on edge connections
145
139
  getNodeOrder: () => orderNodesByEdges(nodes, edges),
140
+ // Get current state for saving
141
+ getCurrentState: () => ({ nodes: [...nodes], edges: [...edges] }),
142
+ // Set nodes and edges (for restoring saved state)
143
+ setNodesAndEdges: (newNodes: any[], newEdges: any[]) => {
144
+ setNodes(newNodes);
145
+ setEdges(newEdges);
146
+ },
146
147
  }));
147
148
 
148
149
  return (
@@ -1,3 +1,4 @@
1
+ import { MarkerType } from '@xyflow/react';
1
2
  import { Markers } from '../assets/markers/markers.type';
2
3
  import { CustomEdgeGenerator } from '../organisms/CustomEdge/custom-edge-generator';
3
4
  import { EdgeTypes } from '../organisms/CustomEdge/custom-edge.type';
@@ -13,6 +14,22 @@ export default {
13
14
  };
14
15
 
15
16
  export const defaultEdgeOptions = {
17
+ style: {
18
+ strokeWidth: 2,
19
+ stroke: '#10B981', // Green color for better visibility
20
+ },
21
+ // Arrow marker at the end of the edge
22
+ markerEnd: {
23
+ type: MarkerType.ArrowClosed,
24
+ width: 20,
25
+ height: 20,
26
+ color: '#10B981',
27
+ },
28
+ animated: false,
29
+ };
30
+
31
+ // ER Diagram edge options (for entity relationship diagrams)
32
+ export const erDiagramEdgeOptions = {
16
33
  style: {
17
34
  strokeWidth: 2,
18
35
  stroke: '#4858E9',
@@ -0,0 +1,113 @@
1
+ import { Edge, MarkerType } from '@xyflow/react';
2
+ import { ICardNode } from '../types/card-node';
3
+
4
+ /**
5
+ * Generates edges based on the order of nodes in the array.
6
+ * Creates a linear connection from first node to last node.
7
+ *
8
+ * @param nodes - Array of nodes in the desired order
9
+ * @param options - Configuration options for edge generation
10
+ * @returns Array of edges connecting nodes in sequence
11
+ */
12
+ export interface GenerateEdgesOptions {
13
+ /** Edge style stroke color */
14
+ strokeColor?: string;
15
+ /** Edge stroke width */
16
+ strokeWidth?: number;
17
+ /** Whether to add arrow marker at the end */
18
+ markerEnd?: boolean;
19
+ /** Whether edges should be animated */
20
+ animated?: boolean;
21
+ /** Source handle ID pattern (e.g., 'right-source', 'bottom-source') */
22
+ sourceHandleId?: string;
23
+ /** Target handle ID pattern (e.g., 'left-target', 'top-target') */
24
+ targetHandleId?: string;
25
+ }
26
+
27
+ const defaultOptions: GenerateEdgesOptions = {
28
+ strokeColor: '#10B981',
29
+ strokeWidth: 2,
30
+ markerEnd: true,
31
+ animated: false,
32
+ sourceHandleId: 'right-source',
33
+ targetHandleId: 'left-target',
34
+ };
35
+
36
+ export function generateEdgesFromNodeOrder(
37
+ nodes: ICardNode[],
38
+ options: GenerateEdgesOptions = {}
39
+ ): Edge[] {
40
+ const opts = { ...defaultOptions, ...options };
41
+ const edges: Edge[] = [];
42
+
43
+ // Need at least 2 nodes to create edges
44
+ if (nodes.length < 2) {
45
+ return edges;
46
+ }
47
+
48
+ // Create edges between consecutive nodes
49
+ for (let i = 0; i < nodes.length - 1; i++) {
50
+ const sourceNode = nodes[i];
51
+ const targetNode = nodes[i + 1];
52
+
53
+ const edge: Edge = {
54
+ id: `edge-${sourceNode.id}-${targetNode.id}`,
55
+ source: sourceNode.id,
56
+ target: targetNode.id,
57
+ sourceHandle: opts.sourceHandleId,
58
+ targetHandle: opts.targetHandleId,
59
+ type: 'default',
60
+ animated: opts.animated,
61
+ style: {
62
+ stroke: opts.strokeColor,
63
+ strokeWidth: opts.strokeWidth,
64
+ },
65
+ };
66
+
67
+ // Add arrow marker if enabled
68
+ if (opts.markerEnd) {
69
+ edge.markerEnd = {
70
+ type: MarkerType.ArrowClosed,
71
+ width: 20,
72
+ height: 20,
73
+ color: opts.strokeColor,
74
+ };
75
+ }
76
+
77
+ edges.push(edge);
78
+ }
79
+
80
+ return edges;
81
+ }
82
+
83
+ /**
84
+ * Generates edges based on node order for automation diagrams (horizontal layout)
85
+ */
86
+ export function generateAutomationEdges(nodes: ICardNode[]): Edge[] {
87
+ return generateEdgesFromNodeOrder(nodes, {
88
+ strokeColor: '#10B981',
89
+ strokeWidth: 2,
90
+ markerEnd: true,
91
+ animated: false,
92
+ sourceHandleId: 'right-source',
93
+ targetHandleId: 'left-target',
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Generates edges based on node order for vertical layout diagrams
99
+ */
100
+ export function generateVerticalEdges(nodes: ICardNode[]): Edge[] {
101
+ return generateEdgesFromNodeOrder(nodes, {
102
+ strokeColor: '#10B981',
103
+ strokeWidth: 2,
104
+ markerEnd: true,
105
+ animated: false,
106
+ sourceHandleId: 'bottom-source',
107
+ targetHandleId: 'top-target',
108
+ });
109
+ }
110
+
111
+ export default generateEdgesFromNodeOrder;
112
+
113
+