From 39af1ded5f83a93d07e643f1c3ebf747125ab876 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:13:11 -0300 Subject: [PATCH] fix: implemented cycle check on isValidConnection (#8716) * Implemented cycle check on isValidConnection * Implemented loop check if the loop done results in a loopcomponent loop --- src/frontend/src/utils/reactflowUtils.ts | 79 +++++++++++++++++++----- 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index 6fc3f3b4a..218902df9 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -329,10 +329,11 @@ export function unselectAllNodesEdges(nodes: Node[], edges: Edge[]) { } export function isValidConnection( - { source, target, sourceHandle, targetHandle }: Connection, + connection: Connection, nodes?: AllNodeType[], edges?: EdgeType[], ): boolean { + const { source, target, sourceHandle, targetHandle } = connection; if (source === target) { return false; } @@ -342,6 +343,33 @@ export function isValidConnection( const targetHandleObject: targetHandleType = scapeJSONParse(targetHandle!); const sourceHandleObject: sourceHandleType = scapeJSONParse(sourceHandle!); + + // Helper to find the edge between two nodes + function findEdgeBetween(srcId: string, tgtId: string) { + return edgesArray.find((e) => e.source === srcId && e.target === tgtId); + } + + // Modified hasCycle to return the path of edges forming the loop + const findCyclePath = ( + node: AllNodeType, + visited = new Set(), + path: EdgeType[] = [], + ): EdgeType[] | null => { + if (visited.has(node.id)) return null; + visited.add(node.id); + for (const outgoer of getOutgoers(node, nodesArray, edgesArray)) { + const edge = findEdgeBetween(node.id, outgoer.id); + if (!edge) continue; + if (outgoer.id === source) { + // This edge would close the loop + return [...path, edge]; + } + const result = findCyclePath(outgoer, visited, [...path, edge]); + if (result) return result; + } + return null; + }; + if ( targetHandleObject.inputTypes?.some( (n) => n === sourceHandleObject.dataType, @@ -359,22 +387,43 @@ export function isValidConnection( t === targetHandleObject.type, ) ) { - let targetNode = nodesArray.find((node) => node.id === target!)?.data?.node; - if (!targetNode) { - if (!edgesArray.find((e) => e.targetHandle === targetHandle)) { + let targetNode = nodesArray.find((node) => node.id === target!); + let targetNodeDataNode = targetNode?.data?.node; + if ( + (!targetNodeDataNode && + !edgesArray.find((e) => e.targetHandle === targetHandle)) || + (targetNodeDataNode && + targetHandleObject.output_types && + !edgesArray.find((e) => e.targetHandle === targetHandle)) || + (targetNodeDataNode && + !targetHandleObject.output_types && + ((!targetNodeDataNode.template[targetHandleObject.fieldName].list && + !edgesArray.find((e) => e.targetHandle === targetHandle)) || + targetNodeDataNode.template[targetHandleObject.fieldName].list)) + ) { + // If the current target handle is a loop component, allow connection immediately + if (targetHandleObject.output_types) { return true; } - } else if ( - targetHandleObject.output_types && - !edgesArray.find((e) => e.targetHandle === targetHandle) - ) { - return true; - } else if ( - !targetHandleObject.output_types && - ((!targetNode.template[targetHandleObject.fieldName].list && - !edgesArray.find((e) => e.targetHandle === targetHandle)) || - targetNode.template[targetHandleObject.fieldName].list) - ) { + // Check for loop and if any edge in the loop is a loop component + let cyclePath: EdgeType[] | null = null; + if (targetNode) { + cyclePath = findCyclePath(targetNode); + } + if (cyclePath) { + // Check if any edge in the cycle path is a loop component + const hasLoopComponent = cyclePath.some((edge) => { + try { + const th = scapeJSONParse(edge.targetHandle!); + return !!th.output_types; + } catch { + return false; + } + }); + if (!hasLoopComponent) { + return false; + } + } return true; } }