@flowuent-org/diagramming-core 1.1.1 → 1.1.3

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.
@@ -5,7 +5,7 @@ export const automationDefaultNodes = [
5
5
  {
6
6
  id: 'start-node',
7
7
  type: 'AutomationStartNode',
8
- position: { x: 50, y: 200 },
8
+ position: { x: 100, y: 200 },
9
9
  data: {
10
10
  // Display data for the node UI
11
11
  label: 'Workflow Trigger',
@@ -61,7 +61,7 @@ export const automationDefaultNodes = [
61
61
  {
62
62
  id: 'api-call-node',
63
63
  type: 'AutomationApiNode',
64
- position: { x: 400, y: 200 },
64
+ position: { x: 500, y: 200 },
65
65
  data: {
66
66
  // Display data for the node UI
67
67
  label: 'Discover Headlines',
@@ -152,7 +152,7 @@ export const automationDefaultNodes = [
152
152
  {
153
153
  id: 'ai-suggestion-node',
154
154
  type: 'AutomationAISuggestionNode',
155
- position: { x: 400, y: 360 },
155
+ position: { x: 900, y: 200 },
156
156
  data: {
157
157
  label: 'Citation Extractor',
158
158
  description: 'Extract and format citation from article text.',
@@ -174,7 +174,7 @@ export const automationDefaultNodes = [
174
174
  {
175
175
  id: 'data-formatting-node',
176
176
  type: 'AutomationFormattingNode',
177
- position: { x: 750, y: 200 },
177
+ position: { x: 700, y: 200 },
178
178
  data: {
179
179
  // Display data for the node UI
180
180
  label: 'Article Analyzer',
@@ -249,7 +249,7 @@ export const automationDefaultNodes = [
249
249
  {
250
250
  id: 'google-sheets-node',
251
251
  type: 'AutomationSheetsNode',
252
- position: { x: 925, y: 200 },
252
+ position: { x: 1100, y: 200 },
253
253
  data: {
254
254
  // Display data for the node UI
255
255
  label: 'Data Sync Node',
@@ -357,7 +357,7 @@ export const automationDefaultNodes = [
357
357
  {
358
358
  id: 'note-node-1',
359
359
  type: 'AutomationNoteNode',
360
- position: { x: 400, y: 50 },
360
+ position: { x: 700, y: 50 },
361
361
  data: {
362
362
  label: 'Article Analyzer',
363
363
  description:
@@ -388,7 +388,7 @@ export const automationDefaultNodes = [
388
388
  {
389
389
  id: 'note-node-2',
390
390
  type: 'AutomationNoteNode',
391
- position: { x: 50, y: 400 },
391
+ position: { x: 500, y: 50 },
392
392
  data: {
393
393
  label: 'Discover Headlines',
394
394
  description:
@@ -419,7 +419,7 @@ export const automationDefaultNodes = [
419
419
  {
420
420
  id: 'end-node',
421
421
  type: 'AutomationEndNode',
422
- position: { x: 1275, y: 200 },
422
+ position: { x: 1500, y: 200 },
423
423
  data: {
424
424
  // Display data for the node UI
425
425
  label: 'Results Display',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowuent-org/diagramming-core",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -18,17 +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 - below the parent node
28
- // Node height + button height (~40px) + panel height (~400px) + spacing
29
- const nodeHeight = parentNode.height || 150;
30
- const panelHeight = suggestions.length * 180 + 100; // Approximate height per suggestion + padding
31
- const newNodeY = parentNode.position.y + nodeHeight + 40 + panelHeight + 40;
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;
32
71
 
33
72
  // Create new AI Suggestion node
34
73
  const newNodeId = `ai-suggestion-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
@@ -55,8 +94,8 @@ export const AISuggestionsPanel: React.FC<AISuggestionsPanelProps> = ({
55
94
  id: newNodeId,
56
95
  type: 'AutomationAISuggestionNode',
57
96
  position: {
58
- x: parentNode.position.x,
59
- y: newNodeY,
97
+ x: newNodeX,
98
+ y: parentNode.position.y,
60
99
  },
61
100
  data: {
62
101
  label: suggestion.title,
@@ -72,10 +111,27 @@ export const AISuggestionsPanel: React.FC<AISuggestionsPanelProps> = ({
72
111
  measured: { width: 336, height: 150 },
73
112
  };
74
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
+
75
130
  // Add the new node
76
- setNodes([...nodes, newNode]);
131
+ setNodes((prevNodes) => [...prevNodes, newNode]);
77
132
 
78
- // 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
79
135
  const edgeId = `edge-${parentNodeId}-${newNodeId}`;
80
136
  const newEdge: Edge = {
81
137
  id: edgeId,
@@ -109,10 +165,10 @@ export const AISuggestionsPanel: React.FC<AISuggestionsPanelProps> = ({
109
165
  onSuggestionClick(suggestion);
110
166
  }
111
167
 
112
- // Close the panel
113
- if (onClose) {
114
- onClose();
115
- }
168
+ // Reset the flag after a delay to allow the layout to settle
169
+ setTimeout(() => {
170
+ isAddingRef.current = false;
171
+ }, 1000);
116
172
  };
117
173
 
118
174
  return (
@@ -155,7 +211,7 @@ export const AISuggestionsPanel: React.FC<AISuggestionsPanelProps> = ({
155
211
  {/* Suggestions List */}
156
212
  <Box sx={{ }}>
157
213
  {suggestions.map((suggestion) => (
158
- <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, } }} >
159
215
  <CardContent>
160
216
  <Box sx={{ display: 'flex' }}>
161
217
 
@@ -67,7 +67,7 @@ const createDiagramStore = ({
67
67
  historyIndex: -1,
68
68
  diagramType,
69
69
  edgeShapeType: 'curly',
70
- layoutDirection: 'DOWN',
70
+ layoutDirection: diagramType === 'automation' ? 'RIGHT' : 'DOWN',
71
71
  pannable: true,
72
72
  selectedNode: null,
73
73
  selectedEdge: null,
@@ -10,7 +10,7 @@ import {
10
10
  createUpdatedEdges,
11
11
  createUpdatedNodes,
12
12
  } from '../utils/create-updated';
13
- import { getNodeHeight } from '../utils/utilities';
13
+ import { getNodeHeight, getNodeWidth } from '../utils/utilities';
14
14
  import {
15
15
  useDiagram,
16
16
  useGetDefaultNodeData,
@@ -154,7 +154,7 @@ export const useGenerateNodesAndEdges = () => {
154
154
  position:
155
155
  index === 0
156
156
  ? position
157
- : { x: position.x, y: position.y + 200 * index },
157
+ : { x: position.x + (constantLengths.SUB_NODE_WIDTH + constantLengths.CONNECTION_LENGTH) * index, y: position.y },
158
158
  replacingNode: isReplacement && index === 0 ? currentNode : {},
159
159
  });
160
160
  node.data.branchIndex = nodeInfo.branchIndex;
@@ -165,7 +165,7 @@ export const useGenerateNodesAndEdges = () => {
165
165
  const edge = createEdgeConnection({
166
166
  sourceId: newNodes[edgeInfo.source].id,
167
167
  targetId: newNodes[edgeInfo.target].id,
168
- sourceHandle: `${newNodes[edgeInfo.source].id}-bottom`,
168
+ sourceHandle: `${newNodes[edgeInfo.source].id}-right`,
169
169
  edgeLabel: edgeInfo.label,
170
170
  });
171
171
  if (edge) newEdges.push(edge);
@@ -203,11 +203,11 @@ export const useCalculateNewNodesAndEdges = () => {
203
203
  : isParallelRoot || addBeforeCurrentNode
204
204
  ? { x: 0, y: constantLengths.NODE_TOP_HEIGHT }
205
205
  : {
206
- x: currentNode.position.x,
207
- y:
208
- currentNode.position.y +
209
- getNodeHeight(currentNode) +
206
+ x:
207
+ currentNode.position.x +
208
+ getNodeWidth(currentNode) +
210
209
  constantLengths.CONNECTION_LENGTH,
210
+ y: currentNode.position.y,
211
211
  };
212
212
 
213
213
  const { nodes, edges } = generateNodesAndEdges(
@@ -224,7 +224,7 @@ export const useCalculateNewNodesAndEdges = () => {
224
224
  createEdgeConnection({
225
225
  sourceId: currentNode.id,
226
226
  targetId: nodes[0].id,
227
- sourceHandle: `${currentNode.id}-bottom`,
227
+ sourceHandle: `${currentNode.id}-right`,
228
228
  })!,
229
229
  );
230
230
  }
@@ -115,13 +115,33 @@ 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
 
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]);
119
126
 
127
+ // Debounce layout recalculation to prevent continuous propagation
120
128
  useEffect(() => {
121
- getLayoutedElements();
129
+ // Clear any pending layout recalculation
130
+ if (layoutTimeoutRef.current) {
131
+ clearTimeout(layoutTimeoutRef.current);
132
+ }
122
133
 
123
- return () => { };
124
- }, [nodes.length, edges.length, contentHeights, diagramType]);
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]);
125
145
 
126
146
  // Listen for layout direction changes
127
147
  useEffect(() => {
@@ -208,6 +228,14 @@ export const DiagramContent: React.FC<DiagramContentProps> = ({
208
228
  edges={visibleEdges}
209
229
  onEdgesChange={onEdgesChange}
210
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}
211
239
  onNodeDragStart={onNodeDragStart}
212
240
  onNodeDragStop={onNodeDragEnd}
213
241
  onConnect={onConnect}
@@ -28,8 +28,8 @@ export const createUpdatedEdges = (
28
28
  ? {
29
29
  ...e,
30
30
  source,
31
- id: `${source}-bottomv${e.target}`, // Ensure edge ID includes handle
32
- sourceHandle: `${source}-bottom`, // Update the source handle ID
31
+ id: `${source}-rightv${e.target}`, // Ensure edge ID includes handle
32
+ sourceHandle: `${source}-right`, // Update the source handle ID for horizontal workflow
33
33
  }
34
34
  : e,
35
35
  );