mirror of https://github.com/langgenius/dify.git
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:
parent
39010fd153
commit
8834e6e531
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}>
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue