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.
This commit is contained in:
zhsama 2026-01-04 20:45:42 +08:00
parent 39010fd153
commit 8834e6e531
4 changed files with 91 additions and 10 deletions

View File

@ -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,
}

View File

@ -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
}

View File

@ -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
}>

View File

@ -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<string>()
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],
}
}