From 8834e6e5318cfc64f9db4e32dc846620c147cdde Mon Sep 17 00:00:00 2001 From: zhsama Date: Sun, 4 Jan 2026 20:45:42 +0800 Subject: [PATCH] feat(workflow): enhance group node functionality with head and leaf node tracking - Added headNodeIds and leafNodeIds to GroupNodeData to track nodes that receive input and send output outside the group. - Updated useNodesInteractions hook to include headNodeIds in the group node data. - Modified isValidConnection logic in useWorkflow to validate connections based on leaf node types for group nodes. - Enhanced preprocessNodesAndEdges to rebuild temporary edges for group nodes, connecting them to external nodes for visual representation. --- .../workflow/hooks/use-nodes-interactions.ts | 5 ++ .../components/workflow/hooks/use-workflow.ts | 26 ++++--- .../components/workflow/nodes/group/types.ts | 2 + .../workflow/utils/workflow-init.ts | 68 ++++++++++++++++++- 4 files changed, 91 insertions(+), 10 deletions(-) diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 6ac1fcd655..6addd299b9 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -2411,6 +2411,9 @@ export const useNodesInteractions = () => { const handlers: GroupHandler[] = Array.from(handlerMap.values()) + // head nodes: nodes that receive input from outside the group + const headNodeIds = [...new Set(inboundEdges.map(edge => edge.target))] + // put the group node at the top-left corner of the selection, slightly offset const { x: minX, y: minY } = getTopLeftNodePosition(bundledNodes) @@ -2420,6 +2423,8 @@ export const useNodesInteractions = () => { type: BlockEnum.Group, members, handlers, + headNodeIds, + leafNodeIds, selected: true, } diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index 990c8c950d..6c776f4815 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -4,7 +4,6 @@ import type { import type { IterationNodeType } from '../nodes/iteration/types' import type { LoopNodeType } from '../nodes/loop/types' import type { - BlockEnum, Edge, Node, ValueSelector, @@ -28,14 +27,12 @@ import { } from '../constants' import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils' import { CUSTOM_NOTE_NODE } from '../note-node/constants' - import { useStore, useWorkflowStore, } from '../store' -import { - WorkflowRunningStatus, -} from '../types' + +import { BlockEnum, WorkflowRunningStatus } from '../types' import { getWorkflowEntryNode, isWorkflowEntryNode, @@ -381,7 +378,7 @@ export const useWorkflow = () => { return startNodes }, [nodesMap, getRootNodesById]) - const isValidConnection = useCallback(({ source, sourceHandle: _sourceHandle, target }: Connection) => { + const isValidConnection = useCallback(({ source, sourceHandle, target }: Connection) => { const { edges, getNodes, @@ -396,14 +393,27 @@ export const useWorkflow = () => { if (sourceNode.parentId !== targetNode.parentId) return false + // For Group nodes, use the leaf node's type for validation + // sourceHandle format: "${leafNodeId}-${originalSourceHandle}" + let actualSourceType = sourceNode.data.type + if (sourceNode.data.type === BlockEnum.Group && sourceHandle) { + const lastDashIndex = sourceHandle.lastIndexOf('-') + if (lastDashIndex > 0) { + const leafNodeId = sourceHandle.substring(0, lastDashIndex) + const leafNode = nodes.find(node => node.id === leafNodeId) + if (leafNode) + actualSourceType = leafNode.data.type + } + } + if (sourceNode && targetNode) { - const sourceNodeAvailableNextNodes = getAvailableBlocks(sourceNode.data.type, !!sourceNode.parentId).availableNextBlocks + const sourceNodeAvailableNextNodes = getAvailableBlocks(actualSourceType, !!sourceNode.parentId).availableNextBlocks const targetNodeAvailablePrevNodes = getAvailableBlocks(targetNode.data.type, !!targetNode.parentId).availablePrevBlocks if (!sourceNodeAvailableNextNodes.includes(targetNode.data.type)) return false - if (!targetNodeAvailablePrevNodes.includes(sourceNode.data.type)) + if (!targetNodeAvailablePrevNodes.includes(actualSourceType)) return false } diff --git a/web/app/components/workflow/nodes/group/types.ts b/web/app/components/workflow/nodes/group/types.ts index 92838357cf..5f16b0e981 100644 --- a/web/app/components/workflow/nodes/group/types.ts +++ b/web/app/components/workflow/nodes/group/types.ts @@ -16,4 +16,6 @@ export type GroupHandler = { export type GroupNodeData = CommonNodeType<{ members?: GroupMember[] handlers?: GroupHandler[] + headNodeIds?: string[] // nodes that receive input from outside the group + leafNodeIds?: string[] // nodes that send output to outside the group }> diff --git a/web/app/components/workflow/utils/workflow-init.ts b/web/app/components/workflow/utils/workflow-init.ts index 1f6c0151ac..7b39758106 100644 --- a/web/app/components/workflow/utils/workflow-init.ts +++ b/web/app/components/workflow/utils/workflow-init.ts @@ -1,4 +1,5 @@ import type { CustomGroupNodeData } from '../custom-group-node' +import type { GroupNodeData } from '../nodes/group/types' import type { IfElseNodeType } from '../nodes/if-else/types' import type { IterationNodeType } from '../nodes/iteration/types' import type { LoopNodeType } from '../nodes/loop/types' @@ -92,8 +93,9 @@ export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => { const hasIterationNode = nodes.some(node => node.data.type === BlockEnum.Iteration) const hasLoopNode = nodes.some(node => node.data.type === BlockEnum.Loop) const hasGroupNode = nodes.some(node => node.type === CUSTOM_GROUP_NODE) + const hasBusinessGroupNode = nodes.some(node => node.data.type === BlockEnum.Group) - if (!hasIterationNode && !hasLoopNode && !hasGroupNode) { + if (!hasIterationNode && !hasLoopNode && !hasGroupNode && !hasBusinessGroupNode) { return { nodes, edges, @@ -248,9 +250,71 @@ export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => { } } + // Rebuild isTemp edges for business Group nodes (BlockEnum.Group) + // These edges connect the group node to external nodes for visual display + const groupTempEdges: Edge[] = [] + const inboundEdgeIds = new Set() + + nodes.forEach((groupNode) => { + if (groupNode.data.type !== BlockEnum.Group) + return + + const groupData = groupNode.data as GroupNodeData + const { members = [], headNodeIds = [], leafNodeIds = [], handlers = [] } = groupData + const memberSet = new Set(members.map(m => m.id)) + const headSet = new Set(headNodeIds) + const leafSet = new Set(leafNodeIds) + + edges.forEach((edge) => { + // Inbound edge: source outside group, target is a head node + // Use Set to dedupe since multiple head nodes may share same external source + if (!memberSet.has(edge.source) && headSet.has(edge.target)) { + const edgeId = `${edge.source}-${edge.sourceHandle}-${groupNode.id}-target` + if (!inboundEdgeIds.has(edgeId)) { + inboundEdgeIds.add(edgeId) + groupTempEdges.push({ + id: edgeId, + type: 'custom', + source: edge.source, + sourceHandle: edge.sourceHandle, + target: groupNode.id, + targetHandle: 'target', + data: { + sourceType: edge.data?.sourceType, + targetType: BlockEnum.Group, + _isTemp: true, + }, + } as Edge) + } + } + + // Outbound edge: source is a leaf node, target outside group + if (leafSet.has(edge.source) && !memberSet.has(edge.target)) { + const handler = handlers.find( + h => h.nodeId === edge.source && h.sourceHandle === edge.sourceHandle, + ) + if (handler) { + groupTempEdges.push({ + id: `${groupNode.id}-${handler.id}-${edge.target}-${edge.targetHandle}`, + type: 'custom', + source: groupNode.id, + sourceHandle: handler.id, + target: edge.target!, + targetHandle: edge.targetHandle, + data: { + sourceType: BlockEnum.Group, + targetType: edge.data?.targetType, + _isTemp: true, + }, + } as Edge) + } + } + }) + }) + return { nodes: [...nodes, ...newIterationStartNodes, ...newLoopStartNodes], - edges: [...edges, ...newEdges, ...groupInternalEdges], + edges: [...edges, ...newEdges, ...groupInternalEdges, ...groupTempEdges], } }