mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-13 04:28:58 +08:00
feat: organize button adds organization of nodes inside iteration/loop nodes (#17068)
This commit is contained in:
parent
7df36fe9f5
commit
ac850e559f
@ -416,6 +416,10 @@ export const LOOP_PADDING = {
|
|||||||
left: 16,
|
left: 16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const NODE_LAYOUT_HORIZONTAL_PADDING = 60
|
||||||
|
export const NODE_LAYOUT_VERTICAL_PADDING = 60
|
||||||
|
export const NODE_LAYOUT_MIN_DISTANCE = 100
|
||||||
|
|
||||||
let maxParallelLimit = 10
|
let maxParallelLimit = 10
|
||||||
|
|
||||||
if (process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT && process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT !== '')
|
if (process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT && process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT !== '')
|
||||||
|
@ -8,12 +8,15 @@ import produce from 'immer'
|
|||||||
import { useStore, useWorkflowStore } from '../store'
|
import { useStore, useWorkflowStore } from '../store'
|
||||||
import {
|
import {
|
||||||
CUSTOM_NODE, DSL_EXPORT_CHECK,
|
CUSTOM_NODE, DSL_EXPORT_CHECK,
|
||||||
|
NODE_LAYOUT_HORIZONTAL_PADDING,
|
||||||
|
NODE_LAYOUT_VERTICAL_PADDING,
|
||||||
WORKFLOW_DATA_UPDATE,
|
WORKFLOW_DATA_UPDATE,
|
||||||
} from '../constants'
|
} from '../constants'
|
||||||
import type { Node, WorkflowDataUpdater } from '../types'
|
import type { Node, WorkflowDataUpdater } from '../types'
|
||||||
import { ControlMode } from '../types'
|
import { BlockEnum, ControlMode } from '../types'
|
||||||
import {
|
import {
|
||||||
getLayoutByDagre,
|
getLayoutByDagre,
|
||||||
|
getLayoutForChildNodes,
|
||||||
initialEdges,
|
initialEdges,
|
||||||
initialNodes,
|
initialNodes,
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
@ -98,10 +101,81 @@ export const useWorkflowOrganize = () => {
|
|||||||
} = store.getState()
|
} = store.getState()
|
||||||
const { setViewport } = reactflow
|
const { setViewport } = reactflow
|
||||||
const nodes = getNodes()
|
const nodes = getNodes()
|
||||||
const layout = getLayoutByDagre(nodes, edges)
|
|
||||||
const rankMap = {} as Record<string, Node>
|
|
||||||
|
|
||||||
nodes.forEach((node) => {
|
const loopAndIterationNodes = nodes.filter(
|
||||||
|
node => (node.data.type === BlockEnum.Loop || node.data.type === BlockEnum.Iteration)
|
||||||
|
&& !node.parentId
|
||||||
|
&& node.type === CUSTOM_NODE,
|
||||||
|
)
|
||||||
|
|
||||||
|
const childLayoutsMap: Record<string, any> = {}
|
||||||
|
loopAndIterationNodes.forEach((node) => {
|
||||||
|
childLayoutsMap[node.id] = getLayoutForChildNodes(node.id, nodes, edges)
|
||||||
|
})
|
||||||
|
|
||||||
|
const containerSizeChanges: Record<string, { width: number, height: number }> = {}
|
||||||
|
|
||||||
|
loopAndIterationNodes.forEach((parentNode) => {
|
||||||
|
const childLayout = childLayoutsMap[parentNode.id]
|
||||||
|
if (!childLayout) return
|
||||||
|
|
||||||
|
let minX = Infinity
|
||||||
|
let minY = Infinity
|
||||||
|
let maxX = -Infinity
|
||||||
|
let maxY = -Infinity
|
||||||
|
let hasChildren = false
|
||||||
|
|
||||||
|
const childNodes = nodes.filter(node => node.parentId === parentNode.id)
|
||||||
|
|
||||||
|
childNodes.forEach((node) => {
|
||||||
|
if (childLayout.node(node.id)) {
|
||||||
|
hasChildren = true
|
||||||
|
const childNodeWithPosition = childLayout.node(node.id)
|
||||||
|
|
||||||
|
const nodeX = childNodeWithPosition.x - node.width! / 2
|
||||||
|
const nodeY = childNodeWithPosition.y - node.height! / 2
|
||||||
|
|
||||||
|
minX = Math.min(minX, nodeX)
|
||||||
|
minY = Math.min(minY, nodeY)
|
||||||
|
maxX = Math.max(maxX, nodeX + node.width!)
|
||||||
|
maxY = Math.max(maxY, nodeY + node.height!)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hasChildren) {
|
||||||
|
const requiredWidth = maxX - minX + NODE_LAYOUT_HORIZONTAL_PADDING * 2
|
||||||
|
const requiredHeight = maxY - minY + NODE_LAYOUT_VERTICAL_PADDING * 2
|
||||||
|
|
||||||
|
containerSizeChanges[parentNode.id] = {
|
||||||
|
width: Math.max(parentNode.width || 0, requiredWidth),
|
||||||
|
height: Math.max(parentNode.height || 0, requiredHeight),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const nodesWithUpdatedSizes = produce(nodes, (draft) => {
|
||||||
|
draft.forEach((node) => {
|
||||||
|
if ((node.data.type === BlockEnum.Loop || node.data.type === BlockEnum.Iteration)
|
||||||
|
&& containerSizeChanges[node.id]) {
|
||||||
|
node.width = containerSizeChanges[node.id].width
|
||||||
|
node.height = containerSizeChanges[node.id].height
|
||||||
|
|
||||||
|
if (node.data.type === BlockEnum.Loop) {
|
||||||
|
node.data.width = containerSizeChanges[node.id].width
|
||||||
|
node.data.height = containerSizeChanges[node.id].height
|
||||||
|
}
|
||||||
|
else if (node.data.type === BlockEnum.Iteration) {
|
||||||
|
node.data.width = containerSizeChanges[node.id].width
|
||||||
|
node.data.height = containerSizeChanges[node.id].height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const layout = getLayoutByDagre(nodesWithUpdatedSizes, edges)
|
||||||
|
|
||||||
|
const rankMap = {} as Record<string, Node>
|
||||||
|
nodesWithUpdatedSizes.forEach((node) => {
|
||||||
if (!node.parentId && node.type === CUSTOM_NODE) {
|
if (!node.parentId && node.type === CUSTOM_NODE) {
|
||||||
const rank = layout.node(node.id).rank!
|
const rank = layout.node(node.id).rank!
|
||||||
|
|
||||||
@ -115,7 +189,7 @@ export const useWorkflowOrganize = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const newNodes = produce(nodes, (draft) => {
|
const newNodes = produce(nodesWithUpdatedSizes, (draft) => {
|
||||||
draft.forEach((node) => {
|
draft.forEach((node) => {
|
||||||
if (!node.parentId && node.type === CUSTOM_NODE) {
|
if (!node.parentId && node.type === CUSTOM_NODE) {
|
||||||
const nodeWithPosition = layout.node(node.id)
|
const nodeWithPosition = layout.node(node.id)
|
||||||
@ -126,7 +200,40 @@ export const useWorkflowOrganize = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
loopAndIterationNodes.forEach((parentNode) => {
|
||||||
|
const childLayout = childLayoutsMap[parentNode.id]
|
||||||
|
if (!childLayout) return
|
||||||
|
|
||||||
|
const childNodes = draft.filter(node => node.parentId === parentNode.id)
|
||||||
|
|
||||||
|
let minX = Infinity
|
||||||
|
let minY = Infinity
|
||||||
|
|
||||||
|
childNodes.forEach((node) => {
|
||||||
|
if (childLayout.node(node.id)) {
|
||||||
|
const childNodeWithPosition = childLayout.node(node.id)
|
||||||
|
const nodeX = childNodeWithPosition.x - node.width! / 2
|
||||||
|
const nodeY = childNodeWithPosition.y - node.height! / 2
|
||||||
|
|
||||||
|
minX = Math.min(minX, nodeX)
|
||||||
|
minY = Math.min(minY, nodeY)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
childNodes.forEach((node) => {
|
||||||
|
if (childLayout.node(node.id)) {
|
||||||
|
const childNodeWithPosition = childLayout.node(node.id)
|
||||||
|
|
||||||
|
node.position = {
|
||||||
|
x: NODE_LAYOUT_HORIZONTAL_PADDING + (childNodeWithPosition.x - node.width! / 2 - minX),
|
||||||
|
y: NODE_LAYOUT_VERTICAL_PADDING + (childNodeWithPosition.y - node.height! / 2 - minY),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
setNodes(newNodes)
|
setNodes(newNodes)
|
||||||
const zoom = 0.7
|
const zoom = 0.7
|
||||||
setViewport({
|
setViewport({
|
||||||
@ -139,6 +246,7 @@ export const useWorkflowOrganize = () => {
|
|||||||
handleSyncWorkflowDraft()
|
handleSyncWorkflowDraft()
|
||||||
})
|
})
|
||||||
}, [getNodesReadOnly, store, reactflow, workflowStore, handleSyncWorkflowDraft, saveStateToHistory])
|
}, [getNodesReadOnly, store, reactflow, workflowStore, handleSyncWorkflowDraft, saveStateToHistory])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleLayout,
|
handleLayout,
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,9 @@ import {
|
|||||||
ITERATION_NODE_Z_INDEX,
|
ITERATION_NODE_Z_INDEX,
|
||||||
LOOP_CHILDREN_Z_INDEX,
|
LOOP_CHILDREN_Z_INDEX,
|
||||||
LOOP_NODE_Z_INDEX,
|
LOOP_NODE_Z_INDEX,
|
||||||
|
NODE_LAYOUT_HORIZONTAL_PADDING,
|
||||||
|
NODE_LAYOUT_MIN_DISTANCE,
|
||||||
|
NODE_LAYOUT_VERTICAL_PADDING,
|
||||||
NODE_WIDTH_X_OFFSET,
|
NODE_WIDTH_X_OFFSET,
|
||||||
START_INITIAL_POSITION,
|
START_INITIAL_POSITION,
|
||||||
} from './constants'
|
} from './constants'
|
||||||
@ -461,13 +464,142 @@ export const getLayoutByDagre = (originNodes: Node[], originEdges: Edge[]) => {
|
|||||||
height: node.height!,
|
height: node.height!,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
edges.forEach((edge) => {
|
edges.forEach((edge) => {
|
||||||
dagreGraph.setEdge(edge.source, edge.target)
|
dagreGraph.setEdge(edge.source, edge.target)
|
||||||
})
|
})
|
||||||
|
dagre.layout(dagreGraph)
|
||||||
|
return dagreGraph
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLayoutForChildNodes = (parentNodeId: string, originNodes: Node[], originEdges: Edge[]) => {
|
||||||
|
const dagreGraph = new dagre.graphlib.Graph()
|
||||||
|
dagreGraph.setDefaultEdgeLabel(() => ({}))
|
||||||
|
|
||||||
|
const nodes = cloneDeep(originNodes).filter(node => node.parentId === parentNodeId)
|
||||||
|
const edges = cloneDeep(originEdges).filter(edge =>
|
||||||
|
(edge.data?.isInIteration && edge.data?.iteration_id === parentNodeId)
|
||||||
|
|| (edge.data?.isInLoop && edge.data?.loop_id === parentNodeId),
|
||||||
|
)
|
||||||
|
|
||||||
|
const startNode = nodes.find(node =>
|
||||||
|
node.type === CUSTOM_ITERATION_START_NODE
|
||||||
|
|| node.type === CUSTOM_LOOP_START_NODE
|
||||||
|
|| node.data?.type === BlockEnum.LoopStart
|
||||||
|
|| node.data?.type === BlockEnum.IterationStart,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!startNode) {
|
||||||
|
dagreGraph.setGraph({
|
||||||
|
rankdir: 'LR',
|
||||||
|
align: 'UL',
|
||||||
|
nodesep: 40,
|
||||||
|
ranksep: 60,
|
||||||
|
marginx: NODE_LAYOUT_HORIZONTAL_PADDING,
|
||||||
|
marginy: NODE_LAYOUT_VERTICAL_PADDING,
|
||||||
|
})
|
||||||
|
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
dagreGraph.setNode(node.id, {
|
||||||
|
width: node.width || 244,
|
||||||
|
height: node.height || 100,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
edges.forEach((edge) => {
|
||||||
|
dagreGraph.setEdge(edge.source, edge.target)
|
||||||
|
})
|
||||||
|
|
||||||
|
dagre.layout(dagreGraph)
|
||||||
|
return dagreGraph
|
||||||
|
}
|
||||||
|
|
||||||
|
const startNodeOutEdges = edges.filter(edge => edge.source === startNode.id)
|
||||||
|
const firstConnectedNodes = startNodeOutEdges.map(edge =>
|
||||||
|
nodes.find(node => node.id === edge.target),
|
||||||
|
).filter(Boolean) as Node[]
|
||||||
|
|
||||||
|
const nonStartNodes = nodes.filter(node => node.id !== startNode.id)
|
||||||
|
const nonStartEdges = edges.filter(edge => edge.source !== startNode.id && edge.target !== startNode.id)
|
||||||
|
|
||||||
|
dagreGraph.setGraph({
|
||||||
|
rankdir: 'LR',
|
||||||
|
align: 'UL',
|
||||||
|
nodesep: 40,
|
||||||
|
ranksep: 60,
|
||||||
|
marginx: NODE_LAYOUT_HORIZONTAL_PADDING / 2,
|
||||||
|
marginy: NODE_LAYOUT_VERTICAL_PADDING / 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
nonStartNodes.forEach((node) => {
|
||||||
|
dagreGraph.setNode(node.id, {
|
||||||
|
width: node.width || 244,
|
||||||
|
height: node.height || 100,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
nonStartEdges.forEach((edge) => {
|
||||||
|
dagreGraph.setEdge(edge.source, edge.target)
|
||||||
|
})
|
||||||
|
|
||||||
dagre.layout(dagreGraph)
|
dagre.layout(dagreGraph)
|
||||||
|
|
||||||
|
const startNodeSize = {
|
||||||
|
width: startNode.width || 44,
|
||||||
|
height: startNode.height || 48,
|
||||||
|
}
|
||||||
|
|
||||||
|
const startNodeX = NODE_LAYOUT_HORIZONTAL_PADDING / 1.5
|
||||||
|
let startNodeY = 100
|
||||||
|
|
||||||
|
let minFirstLayerX = Infinity
|
||||||
|
let avgFirstLayerY = 0
|
||||||
|
let firstLayerCount = 0
|
||||||
|
|
||||||
|
if (firstConnectedNodes.length > 0) {
|
||||||
|
firstConnectedNodes.forEach((node) => {
|
||||||
|
if (dagreGraph.node(node.id)) {
|
||||||
|
const nodePos = dagreGraph.node(node.id)
|
||||||
|
avgFirstLayerY += nodePos.y
|
||||||
|
firstLayerCount++
|
||||||
|
minFirstLayerX = Math.min(minFirstLayerX, nodePos.x - nodePos.width / 2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (firstLayerCount > 0) {
|
||||||
|
avgFirstLayerY /= firstLayerCount
|
||||||
|
startNodeY = avgFirstLayerY
|
||||||
|
}
|
||||||
|
|
||||||
|
const minRequiredX = startNodeX + startNodeSize.width + NODE_LAYOUT_MIN_DISTANCE
|
||||||
|
|
||||||
|
if (minFirstLayerX < minRequiredX) {
|
||||||
|
const shiftX = minRequiredX - minFirstLayerX
|
||||||
|
|
||||||
|
nonStartNodes.forEach((node) => {
|
||||||
|
if (dagreGraph.node(node.id)) {
|
||||||
|
const nodePos = dagreGraph.node(node.id)
|
||||||
|
dagreGraph.setNode(node.id, {
|
||||||
|
x: nodePos.x + shiftX,
|
||||||
|
y: nodePos.y,
|
||||||
|
width: nodePos.width,
|
||||||
|
height: nodePos.height,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dagreGraph.setNode(startNode.id, {
|
||||||
|
x: startNodeX + startNodeSize.width / 2,
|
||||||
|
y: startNodeY,
|
||||||
|
width: startNodeSize.width,
|
||||||
|
height: startNodeSize.height,
|
||||||
|
})
|
||||||
|
|
||||||
|
startNodeOutEdges.forEach((edge) => {
|
||||||
|
dagreGraph.setEdge(edge.source, edge.target)
|
||||||
|
})
|
||||||
|
|
||||||
return dagreGraph
|
return dagreGraph
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user