Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/edges-routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@serverlessworkflow/diagram-editor": minor
---

Apply auto-layout calculated waypoints to edges defined into parent nodes. Add explicit north / south fixed ports to nodes defined in ELK graph.
101 changes: 101 additions & 0 deletions packages/serverless-workflow-diagram-editor/src/core/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,39 @@ export function getNodesByType(graph: FlatGraph, type: GraphNodeType): FlatGraph
return graph.nodes.filter((node) => node.type === type);
}

// Helper function to check if target is outside source's parent hierarchy
export function isTargetOutsideSourceParent(
sourceNode: FlatGraphNode,
targetNode: FlatGraphNode,
nodeMap: Map<string, FlatGraphNode>,
): boolean {
if (!sourceNode.parentId) {
return false;
}

// Check if target is the source's parent itself
if (targetNode.id === sourceNode.parentId) {
return false;
}

// Check if target shares the same parent
if (targetNode.parentId === sourceNode.parentId) {
return false;
}

// Check if target is inside source's parent hierarchy
let currentParentId: string | undefined = targetNode.parentId;
while (currentParentId) {
if (currentParentId === sourceNode.parentId) {
return false;
}
const parentNode = nodeMap.get(currentParentId);
currentParentId = parentNode?.parentId;
}

return true;
}

// Inner entry and exit nodes cannot be connected external nodes so connections shall be moved to parent node
export function fixNodesConnections(graph: FlatGraph): FlatGraph {
const entryNodes = getNodesByType(graph, GraphNodeType.Entry);
Expand All @@ -40,7 +73,22 @@ export function fixNodesConnections(graph: FlatGraph): FlatGraph {
}
});

// Build a map of nodeId -> node for quick lookups
const nodeMap = new Map<string, FlatGraphNode>();
graph.nodes.forEach((node) => {
nodeMap.set(node.id, node);
});

// Build a map of parentId -> exitNodeId
const parentToExitNode = new Map<string, string>();
exitNodes.forEach((node) => {
if (node.parentId) {
parentToExitNode.set(node.parentId, node.id);
}
});

const graphClone = structuredClone(graph);
const newEdges: typeof graphClone.edges = [];

// Single pass over edges to rewrite sourceId/targetId
graphClone.edges.forEach((edge) => {
Expand All @@ -55,7 +103,60 @@ export function fixNodesConnections(graph: FlatGraph): FlatGraph {
if (exitParent) {
edge.sourceId = exitParent;
}

// Check if source node is inside a parent and points outside that parent
const sourceNode = nodeMap.get(edge.sourceId);
const targetNode = nodeMap.get(edge.targetId);

if (
sourceNode &&
targetNode &&
sourceNode.parentId &&
isTargetOutsideSourceParent(sourceNode, targetNode, nodeMap)
) {
// Find the topmost parent that the target is outside of
let currentNode = sourceNode;
let topmostParentId = sourceNode.parentId;

// Walk up the parent hierarchy to find the topmost parent that the target is outside of
while (currentNode.parentId) {
const parentNode = nodeMap.get(currentNode.parentId);
if (!parentNode) break;

// Check if target is outside this parent
if (parentNode.parentId && isTargetOutsideSourceParent(parentNode, targetNode, nodeMap)) {
// Target is also outside this parent's parent, keep going up
topmostParentId = parentNode.parentId;
currentNode = parentNode;
} else {
// Target is not outside this parent's parent (or parent has no parent)
// This means the current parent is the topmost one we need
topmostParentId = currentNode.parentId;
break;
}
}

// Use the immediate parent's exit node
const exitNodeToUse = parentToExitNode.get(sourceNode.parentId);

if (exitNodeToUse) {
// Redirect the edge to the appropriate exit node
edge.targetId = exitNodeToUse;

// Create a new edge from the TOPMOST parent to the original target
// All edges are preserved to maintain complete connection information
newEdges.push({
id: `${edge.id}-redirected`,
sourceId: topmostParentId,
targetId: targetNode.id,
label: edge.label || "",
});
}
}
});

// Add the new edges to the graph
graphClone.edges.push(...newEdges);

return graphClone;
}
Loading