@flowuent-org/diagramming-core 1.1.4 → 1.1.6
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 +1 -1
- package/packages/diagrams/src/index.ts +1 -0
- package/packages/diagrams/src/lib/contexts/DiagramProvider.tsx +17 -0
- package/packages/diagrams/src/lib/contexts/diagramStoreTypes.tsx +1 -0
- package/packages/diagrams/src/lib/contexts/onReconnect.ts +59 -0
- package/packages/diagrams/src/lib/templates/DiagramContent.tsx +548 -338
- package/packages/diagrams/src/lib/utils/nodeOrderByEdges.ts +437 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { Node, Edge } from '@xyflow/react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for ordering nodes by edges
|
|
5
|
+
*/
|
|
6
|
+
export interface NodeOrderOptions {
|
|
7
|
+
/** Whether to include orphan nodes (nodes with no connections) at the end */
|
|
8
|
+
includeOrphanNodes?: boolean;
|
|
9
|
+
/** Strategy for handling branches: 'breadth-first' or 'depth-first' */
|
|
10
|
+
traversalStrategy?: 'breadth-first' | 'depth-first';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Result of topological sort with additional metadata
|
|
15
|
+
*/
|
|
16
|
+
export interface NodeOrderResult<T extends Node = Node> {
|
|
17
|
+
/** Ordered nodes following the arrow direction */
|
|
18
|
+
orderedNodes: T[];
|
|
19
|
+
/** Nodes that have no incoming edges (start nodes) */
|
|
20
|
+
startNodes: T[];
|
|
21
|
+
/** Nodes that have no outgoing edges (end nodes) */
|
|
22
|
+
endNodes: T[];
|
|
23
|
+
/** Nodes with no connections (orphan nodes) */
|
|
24
|
+
orphanNodes: T[];
|
|
25
|
+
/** Whether the graph has cycles */
|
|
26
|
+
hasCycles: boolean;
|
|
27
|
+
/** Execution levels - nodes grouped by their depth in the flow */
|
|
28
|
+
levels: T[][];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Orders nodes based on edge connections (arrow direction from source to target).
|
|
33
|
+
* Uses topological sort to determine the correct execution order.
|
|
34
|
+
*
|
|
35
|
+
* @param nodes - Array of nodes to order
|
|
36
|
+
* @param edges - Array of edges connecting the nodes
|
|
37
|
+
* @param options - Optional configuration for ordering
|
|
38
|
+
* @returns NodeOrderResult with ordered nodes and metadata
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* import { orderNodesByEdges } from '@diagrams/utils/nodeOrderByEdges';
|
|
43
|
+
*
|
|
44
|
+
* const { orderedNodes, startNodes, endNodes } = orderNodesByEdges(nodes, edges);
|
|
45
|
+
*
|
|
46
|
+
* // Execute nodes in order
|
|
47
|
+
* for (const node of orderedNodes) {
|
|
48
|
+
* await executeNode(node);
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function orderNodesByEdges<T extends Node = Node>(
|
|
53
|
+
nodes: T[],
|
|
54
|
+
edges: Edge[],
|
|
55
|
+
options: NodeOrderOptions = {}
|
|
56
|
+
): NodeOrderResult<T> {
|
|
57
|
+
const { includeOrphanNodes = true, traversalStrategy = 'breadth-first' } = options;
|
|
58
|
+
|
|
59
|
+
// Build adjacency list and in-degree map
|
|
60
|
+
const adjacencyList = new Map<string, string[]>();
|
|
61
|
+
const inDegree = new Map<string, number>();
|
|
62
|
+
const nodeMap = new Map<string, T>();
|
|
63
|
+
|
|
64
|
+
// Initialize maps
|
|
65
|
+
nodes.forEach((node) => {
|
|
66
|
+
nodeMap.set(node.id, node);
|
|
67
|
+
adjacencyList.set(node.id, []);
|
|
68
|
+
inDegree.set(node.id, 0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Build graph from edges
|
|
72
|
+
edges.forEach((edge) => {
|
|
73
|
+
const sourceId = edge.source;
|
|
74
|
+
const targetId = edge.target;
|
|
75
|
+
|
|
76
|
+
// Only process edges where both source and target exist
|
|
77
|
+
if (nodeMap.has(sourceId) && nodeMap.has(targetId)) {
|
|
78
|
+
adjacencyList.get(sourceId)!.push(targetId);
|
|
79
|
+
inDegree.set(targetId, (inDegree.get(targetId) || 0) + 1);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Find start nodes (no incoming edges)
|
|
84
|
+
const startNodes: T[] = [];
|
|
85
|
+
const orphanNodes: T[] = [];
|
|
86
|
+
|
|
87
|
+
nodes.forEach((node) => {
|
|
88
|
+
const hasIncoming = inDegree.get(node.id)! > 0;
|
|
89
|
+
const hasOutgoing = (adjacencyList.get(node.id)?.length || 0) > 0;
|
|
90
|
+
|
|
91
|
+
if (!hasIncoming && !hasOutgoing) {
|
|
92
|
+
orphanNodes.push(node);
|
|
93
|
+
} else if (!hasIncoming) {
|
|
94
|
+
startNodes.push(node);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Find end nodes (no outgoing edges)
|
|
99
|
+
const endNodes: T[] = nodes.filter((node) => {
|
|
100
|
+
const hasOutgoing = (adjacencyList.get(node.id)?.length || 0) > 0;
|
|
101
|
+
const hasIncoming = inDegree.get(node.id)! > 0;
|
|
102
|
+
return !hasOutgoing && hasIncoming;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Perform topological sort (Kahn's algorithm with BFS or DFS)
|
|
106
|
+
const orderedNodes: T[] = [];
|
|
107
|
+
const levels: T[][] = [];
|
|
108
|
+
const visited = new Set<string>();
|
|
109
|
+
|
|
110
|
+
if (traversalStrategy === 'breadth-first') {
|
|
111
|
+
// BFS-based topological sort
|
|
112
|
+
const queue: string[] = startNodes.map((n) => n.id);
|
|
113
|
+
const tempInDegree = new Map(inDegree);
|
|
114
|
+
|
|
115
|
+
while (queue.length > 0) {
|
|
116
|
+
const currentLevel: T[] = [];
|
|
117
|
+
const levelSize = queue.length;
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < levelSize; i++) {
|
|
120
|
+
const nodeId = queue.shift()!;
|
|
121
|
+
|
|
122
|
+
if (visited.has(nodeId)) continue;
|
|
123
|
+
visited.add(nodeId);
|
|
124
|
+
|
|
125
|
+
const node = nodeMap.get(nodeId);
|
|
126
|
+
if (node) {
|
|
127
|
+
orderedNodes.push(node);
|
|
128
|
+
currentLevel.push(node);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Process neighbors
|
|
132
|
+
const neighbors = adjacencyList.get(nodeId) || [];
|
|
133
|
+
neighbors.forEach((neighborId) => {
|
|
134
|
+
tempInDegree.set(neighborId, (tempInDegree.get(neighborId) || 1) - 1);
|
|
135
|
+
if (tempInDegree.get(neighborId) === 0 && !visited.has(neighborId)) {
|
|
136
|
+
queue.push(neighborId);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (currentLevel.length > 0) {
|
|
142
|
+
levels.push(currentLevel);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
// DFS-based topological sort
|
|
147
|
+
const stack: string[] = [];
|
|
148
|
+
const tempVisited = new Set<string>();
|
|
149
|
+
|
|
150
|
+
const dfs = (nodeId: string) => {
|
|
151
|
+
if (tempVisited.has(nodeId)) return;
|
|
152
|
+
tempVisited.add(nodeId);
|
|
153
|
+
|
|
154
|
+
const neighbors = adjacencyList.get(nodeId) || [];
|
|
155
|
+
neighbors.forEach((neighborId) => {
|
|
156
|
+
if (!tempVisited.has(neighborId)) {
|
|
157
|
+
dfs(neighborId);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
stack.push(nodeId);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
startNodes.forEach((node) => dfs(node.id));
|
|
165
|
+
|
|
166
|
+
// Reverse to get correct order
|
|
167
|
+
while (stack.length > 0) {
|
|
168
|
+
const nodeId = stack.pop()!;
|
|
169
|
+
visited.add(nodeId);
|
|
170
|
+
const node = nodeMap.get(nodeId);
|
|
171
|
+
if (node) {
|
|
172
|
+
orderedNodes.push(node);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Build levels for DFS (approximate - based on longest path from start)
|
|
177
|
+
const levelMap = new Map<string, number>();
|
|
178
|
+
orderedNodes.forEach((node, index) => {
|
|
179
|
+
const neighbors = adjacencyList.get(node.id) || [];
|
|
180
|
+
let maxParentLevel = -1;
|
|
181
|
+
|
|
182
|
+
edges.forEach((edge) => {
|
|
183
|
+
if (edge.target === node.id && levelMap.has(edge.source)) {
|
|
184
|
+
maxParentLevel = Math.max(maxParentLevel, levelMap.get(edge.source)!);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
levelMap.set(node.id, maxParentLevel + 1);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Group by levels
|
|
192
|
+
const levelGroups = new Map<number, T[]>();
|
|
193
|
+
orderedNodes.forEach((node) => {
|
|
194
|
+
const level = levelMap.get(node.id) || 0;
|
|
195
|
+
if (!levelGroups.has(level)) {
|
|
196
|
+
levelGroups.set(level, []);
|
|
197
|
+
}
|
|
198
|
+
levelGroups.get(level)!.push(node);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Convert to array
|
|
202
|
+
const sortedLevels = Array.from(levelGroups.keys()).sort((a, b) => a - b);
|
|
203
|
+
sortedLevels.forEach((level) => {
|
|
204
|
+
levels.push(levelGroups.get(level)!);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check for cycles (nodes not visited = cycle exists)
|
|
209
|
+
const hasCycles = visited.size < (nodes.length - orphanNodes.length);
|
|
210
|
+
|
|
211
|
+
// Add orphan nodes at the end if requested
|
|
212
|
+
if (includeOrphanNodes) {
|
|
213
|
+
orderedNodes.push(...orphanNodes);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
orderedNodes,
|
|
218
|
+
startNodes,
|
|
219
|
+
endNodes,
|
|
220
|
+
orphanNodes,
|
|
221
|
+
hasCycles,
|
|
222
|
+
levels,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Gets the next nodes that follow a given node based on edge connections
|
|
228
|
+
*
|
|
229
|
+
* @param nodeId - ID of the current node
|
|
230
|
+
* @param edges - Array of edges
|
|
231
|
+
* @param nodes - Array of nodes
|
|
232
|
+
* @returns Array of next nodes (targets of edges from this node)
|
|
233
|
+
*/
|
|
234
|
+
export function getNextNodes<T extends Node = Node>(
|
|
235
|
+
nodeId: string,
|
|
236
|
+
edges: Edge[],
|
|
237
|
+
nodes: T[]
|
|
238
|
+
): T[] {
|
|
239
|
+
const outgoingEdges = edges.filter((edge) => edge.source === nodeId);
|
|
240
|
+
const nextNodeIds = outgoingEdges.map((edge) => edge.target);
|
|
241
|
+
return nodes.filter((node) => nextNodeIds.includes(node.id));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Gets the previous nodes that lead to a given node based on edge connections
|
|
246
|
+
*
|
|
247
|
+
* @param nodeId - ID of the current node
|
|
248
|
+
* @param edges - Array of edges
|
|
249
|
+
* @param nodes - Array of nodes
|
|
250
|
+
* @returns Array of previous nodes (sources of edges to this node)
|
|
251
|
+
*/
|
|
252
|
+
export function getPreviousNodes<T extends Node = Node>(
|
|
253
|
+
nodeId: string,
|
|
254
|
+
edges: Edge[],
|
|
255
|
+
nodes: T[]
|
|
256
|
+
): T[] {
|
|
257
|
+
const incomingEdges = edges.filter((edge) => edge.target === nodeId);
|
|
258
|
+
const prevNodeIds = incomingEdges.map((edge) => edge.source);
|
|
259
|
+
return nodes.filter((node) => prevNodeIds.includes(node.id));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Finds all paths from start nodes to end nodes
|
|
264
|
+
*
|
|
265
|
+
* @param nodes - Array of nodes
|
|
266
|
+
* @param edges - Array of edges
|
|
267
|
+
* @returns Array of paths, where each path is an array of node IDs
|
|
268
|
+
*/
|
|
269
|
+
export function findAllPaths<T extends Node = Node>(
|
|
270
|
+
nodes: T[],
|
|
271
|
+
edges: Edge[]
|
|
272
|
+
): string[][] {
|
|
273
|
+
const { startNodes, endNodes } = orderNodesByEdges(nodes, edges);
|
|
274
|
+
const paths: string[][] = [];
|
|
275
|
+
|
|
276
|
+
const adjacencyList = new Map<string, string[]>();
|
|
277
|
+
nodes.forEach((node) => adjacencyList.set(node.id, []));
|
|
278
|
+
edges.forEach((edge) => {
|
|
279
|
+
if (adjacencyList.has(edge.source)) {
|
|
280
|
+
adjacencyList.get(edge.source)!.push(edge.target);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const dfs = (currentId: string, currentPath: string[], visited: Set<string>) => {
|
|
285
|
+
currentPath.push(currentId);
|
|
286
|
+
visited.add(currentId);
|
|
287
|
+
|
|
288
|
+
const neighbors = adjacencyList.get(currentId) || [];
|
|
289
|
+
|
|
290
|
+
if (neighbors.length === 0 || endNodes.some((n) => n.id === currentId)) {
|
|
291
|
+
// Reached an end node or leaf
|
|
292
|
+
paths.push([...currentPath]);
|
|
293
|
+
} else {
|
|
294
|
+
neighbors.forEach((neighborId) => {
|
|
295
|
+
if (!visited.has(neighborId)) {
|
|
296
|
+
dfs(neighborId, currentPath, new Set(visited));
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
currentPath.pop();
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
startNodes.forEach((startNode) => {
|
|
305
|
+
dfs(startNode.id, [], new Set());
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return paths;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Gets the execution order of nodes for a workflow, handling branches
|
|
313
|
+
*
|
|
314
|
+
* @param nodes - Array of nodes
|
|
315
|
+
* @param edges - Array of edges
|
|
316
|
+
* @returns Object with execution sequence and branch information
|
|
317
|
+
*/
|
|
318
|
+
export function getWorkflowExecutionOrder<T extends Node = Node>(
|
|
319
|
+
nodes: T[],
|
|
320
|
+
edges: Edge[]
|
|
321
|
+
): {
|
|
322
|
+
sequence: T[];
|
|
323
|
+
branches: { branchPoint: T; branches: T[][] }[];
|
|
324
|
+
} {
|
|
325
|
+
const { orderedNodes, startNodes } = orderNodesByEdges(nodes, edges);
|
|
326
|
+
|
|
327
|
+
const adjacencyList = new Map<string, string[]>();
|
|
328
|
+
nodes.forEach((node) => adjacencyList.set(node.id, []));
|
|
329
|
+
edges.forEach((edge) => {
|
|
330
|
+
if (adjacencyList.has(edge.source)) {
|
|
331
|
+
adjacencyList.get(edge.source)!.push(edge.target);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const nodeMap = new Map<string, T>();
|
|
336
|
+
nodes.forEach((node) => nodeMap.set(node.id, node));
|
|
337
|
+
|
|
338
|
+
// Find branch points (nodes with multiple outgoing edges)
|
|
339
|
+
const branches: { branchPoint: T; branches: T[][] }[] = [];
|
|
340
|
+
|
|
341
|
+
orderedNodes.forEach((node) => {
|
|
342
|
+
const outgoing = adjacencyList.get(node.id) || [];
|
|
343
|
+
if (outgoing.length > 1) {
|
|
344
|
+
const branchPaths = outgoing.map((targetId) => {
|
|
345
|
+
const path: T[] = [];
|
|
346
|
+
let currentId = targetId;
|
|
347
|
+
const visited = new Set<string>();
|
|
348
|
+
|
|
349
|
+
while (currentId && !visited.has(currentId)) {
|
|
350
|
+
visited.add(currentId);
|
|
351
|
+
const currentNode = nodeMap.get(currentId);
|
|
352
|
+
if (currentNode) {
|
|
353
|
+
path.push(currentNode);
|
|
354
|
+
}
|
|
355
|
+
const nextNodes = adjacencyList.get(currentId) || [];
|
|
356
|
+
currentId = nextNodes[0]; // Follow first path for simplicity
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return path;
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
branches.push({
|
|
363
|
+
branchPoint: node,
|
|
364
|
+
branches: branchPaths,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
sequence: orderedNodes,
|
|
371
|
+
branches,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Validates the node graph for execution
|
|
377
|
+
*
|
|
378
|
+
* @param nodes - Array of nodes
|
|
379
|
+
* @param edges - Array of edges
|
|
380
|
+
* @returns Validation result with any errors
|
|
381
|
+
*/
|
|
382
|
+
export function validateNodeGraph<T extends Node = Node>(
|
|
383
|
+
nodes: T[],
|
|
384
|
+
edges: Edge[]
|
|
385
|
+
): {
|
|
386
|
+
isValid: boolean;
|
|
387
|
+
errors: string[];
|
|
388
|
+
warnings: string[];
|
|
389
|
+
} {
|
|
390
|
+
const errors: string[] = [];
|
|
391
|
+
const warnings: string[] = [];
|
|
392
|
+
|
|
393
|
+
const { startNodes, endNodes, orphanNodes, hasCycles } = orderNodesByEdges(nodes, edges);
|
|
394
|
+
|
|
395
|
+
// Check for start nodes
|
|
396
|
+
if (startNodes.length === 0) {
|
|
397
|
+
errors.push('No start node found (no nodes without incoming edges)');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Check for multiple start nodes
|
|
401
|
+
if (startNodes.length > 1) {
|
|
402
|
+
warnings.push(`Multiple start nodes found: ${startNodes.map((n) => n.id).join(', ')}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Check for end nodes
|
|
406
|
+
if (endNodes.length === 0 && nodes.length > 0) {
|
|
407
|
+
warnings.push('No end node found (no nodes without outgoing edges)');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Check for orphan nodes
|
|
411
|
+
if (orphanNodes.length > 0) {
|
|
412
|
+
warnings.push(`Found ${orphanNodes.length} orphan node(s) with no connections: ${orphanNodes.map((n) => n.id).join(', ')}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Check for cycles
|
|
416
|
+
if (hasCycles) {
|
|
417
|
+
errors.push('Graph contains cycles - execution order may be undefined');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Check for disconnected edges
|
|
421
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
422
|
+
edges.forEach((edge) => {
|
|
423
|
+
if (!nodeIds.has(edge.source)) {
|
|
424
|
+
errors.push(`Edge ${edge.id} has invalid source: ${edge.source}`);
|
|
425
|
+
}
|
|
426
|
+
if (!nodeIds.has(edge.target)) {
|
|
427
|
+
errors.push(`Edge ${edge.id} has invalid target: ${edge.target}`);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
isValid: errors.length === 0,
|
|
433
|
+
errors,
|
|
434
|
+
warnings,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|