浏览代码

feat: organize button adds organization of nodes inside iteration/loop nodes (#17068)

tags/1.2.0
诗浓 7 个月前
父节点
当前提交
ac850e559f
没有帐户链接到提交者的电子邮件

+ 4
- 0
web/app/components/workflow/constants.ts 查看文件

@@ -416,6 +416,10 @@ export const LOOP_PADDING = {
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

if (process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT && process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT !== '')

+ 113
- 5
web/app/components/workflow/hooks/use-workflow-interactions.ts 查看文件

@@ -8,12 +8,15 @@ import produce from 'immer'
import { useStore, useWorkflowStore } from '../store'
import {
CUSTOM_NODE, DSL_EXPORT_CHECK,
NODE_LAYOUT_HORIZONTAL_PADDING,
NODE_LAYOUT_VERTICAL_PADDING,
WORKFLOW_DATA_UPDATE,
} from '../constants'
import type { Node, WorkflowDataUpdater } from '../types'
import { ControlMode } from '../types'
import { BlockEnum, ControlMode } from '../types'
import {
getLayoutByDagre,
getLayoutForChildNodes,
initialEdges,
initialNodes,
} from '../utils'
@@ -98,10 +101,81 @@ export const useWorkflowOrganize = () => {
} = store.getState()
const { setViewport } = reactflow
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) {
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) => {
if (!node.parentId && node.type === CUSTOM_NODE) {
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)
const zoom = 0.7
setViewport({
@@ -139,6 +246,7 @@ export const useWorkflowOrganize = () => {
handleSyncWorkflowDraft()
})
}, [getNodesReadOnly, store, reactflow, workflowStore, handleSyncWorkflowDraft, saveStateToHistory])

return {
handleLayout,
}

+ 133
- 1
web/app/components/workflow/utils.ts 查看文件

@@ -32,6 +32,9 @@ import {
ITERATION_NODE_Z_INDEX,
LOOP_CHILDREN_Z_INDEX,
LOOP_NODE_Z_INDEX,
NODE_LAYOUT_HORIZONTAL_PADDING,
NODE_LAYOUT_MIN_DISTANCE,
NODE_LAYOUT_VERTICAL_PADDING,
NODE_WIDTH_X_OFFSET,
START_INITIAL_POSITION,
} from './constants'
@@ -461,13 +464,142 @@ export const getLayoutByDagre = (originNodes: Node[], originEdges: Edge[]) => {
height: node.height!,
})
})

edges.forEach((edge) => {
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)

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
}


正在加载...
取消
保存