Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 32 additions & 14 deletions plugins/workflow/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,7 @@ window.__ModuleLoader__.load({
const FLOW_PAR_PAD = 10
const FLOW_PAR_LABEL = 28
const FLOW_NODE_MIN_W = 96
const FLOW_WRAP_GAP = 12
function flowRows(workflow) {
const nodes = (workflow?.nodes ?? []).filter(node => node && typeof node.id === 'string')
const ids = new Set(nodes.map(node => node.id))
Expand All @@ -714,22 +715,39 @@ window.__ModuleLoader__.load({
rows.forEach((row, index) => {
const par = row.length > 1
const inner = width - FIT_PAD * 2 - (par ? FLOW_PAR_PAD * 2 : 0)
const w = Math.max(FLOW_NODE_MIN_W, Math.min(NODE_W, Math.floor((inner - FLOW_GAP_X * (row.length - 1)) / row.length)))
const total = w * row.length + FLOW_GAP_X * (row.length - 1)
const start = Math.round(width / 2 - total / 2)
// A layer wider than the plane wraps onto more lines inside the same frame
// instead of running past the panel edge, where the clipped branches cannot
// be reached at all (#1658). The column count comes from the width, so a
// 3-branch layer in a 400px drawer keeps one line.
const columns = Math.max(1, Math.min(row.length, Math.floor((inner + FLOW_GAP_X) / (FLOW_NODE_MIN_W + FLOW_GAP_X))))
const perLine = Math.ceil(row.length / Math.ceil(row.length / columns))
const lines = []
for (let at = 0; at < row.length; at += perLine) lines.push(row.slice(at, at + perLine))
const top = y + (par ? FLOW_PAR_LABEL : 0)
let rowH = 0
row.forEach((id, column) => {
const hgt = Math.max(24, Number(heights?.[id]) || NODE_H)
rowH = Math.max(rowH, hgt)
const left = start + column * (w + FLOW_GAP_X)
boxes.set(id, { id, row: index, left, top, w, h: hgt, x: left + w / 2 })
let cursor = top
let leftEdge = width
let rightEdge = 0
lines.forEach((line, lineIndex) => {
const w = Math.max(FLOW_NODE_MIN_W, Math.min(NODE_W, Math.floor((inner - FLOW_GAP_X * (line.length - 1)) / line.length)))
const total = w * line.length + FLOW_GAP_X * (line.length - 1)
const start = Math.round(width / 2 - total / 2)
let lineH = 0
line.forEach((id, column) => {
const hgt = Math.max(24, Number(heights?.[id]) || NODE_H)
lineH = Math.max(lineH, hgt)
const left = start + column * (w + FLOW_GAP_X)
boxes.set(id, { id, row: index, left, top: cursor, w, h: hgt, x: left + w / 2 })
})
leftEdge = Math.min(leftEdge, start)
rightEdge = Math.max(rightEdge, start + total)
cursor += lineH + (lineIndex < lines.length - 1 ? FLOW_WRAP_GAP : 0)
})
const bottom = top + rowH + (par ? FLOW_PAR_PAD : 0)
// The label rides the frame's top border; with two branches it fits between
// the two drops so no wire runs through it.
const labelMax = row.length === 2 ? w + FLOW_GAP_X - 16 : total
if (par) frames.push({ key: row.join('+'), left: start - FLOW_PAR_PAD, top: y, width: total + FLOW_PAR_PAD * 2, height: bottom - y, labelMax })
const bottom = cursor + (par ? FLOW_PAR_PAD : 0)
const span = rightEdge - leftEdge
// The label rides the frame's top border; with two branches one line, it fits
// between the two drops so no wire runs through it.
const labelMax = lines.length === 1 && row.length === 2 ? span + FLOW_GAP_X - 16 : span
if (par) frames.push({ key: row.join('+'), left: leftEdge - FLOW_PAR_PAD, top: y, width: span + FLOW_PAR_PAD * 2, height: bottom - y, labelMax })
bands.push({ top: y, bottom })
y = bottom + FLOW_ROW_GAP
})
Expand Down
53 changes: 53 additions & 0 deletions plugins/workflow/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2309,6 +2309,59 @@ for (const width of [364, 530, 300]) {
})
}

// A fan no single drawer row can hold: four branches in one layer (#1658).
function wideFork() {
const branches = ['r1', 'r2', 'r3', 'r4']
return {
...delivery(), id: 'wide-fork', name: 'Wide fork',
layers: [['fork'], branches, ['join']],
nodes: [
{ id: 'fork', name: 'Fork', role: '分叉' },
...branches.map(id => ({ id, name: id.toUpperCase(), role: '分支' })),
{ id: 'join', name: 'Join', role: '汇合' },
],
edges: [
...branches.map(id => ({ from: 'fork', to: id })),
...branches.map(id => ({ from: id, to: 'join' })),
],
}
}

for (const width of [400, 364]) {
test(`a four-way drawer layer stays inside ${width}px, one frame, every box and wire in the plane (#1658)`, () => {
const layout = client.flowLayout(wideFork(), width)
const boxes = [...layout.boxes.values()]
assert.equal(boxes.length, 6)
for (const box of boxes) {
assert.ok(box.left >= 0 && box.left + box.w <= width, `${box.id} box ${box.left}..${box.left + box.w} is outside 0..${width}`)
assert.ok(box.top >= 0 && box.top + box.h <= layout.height, `${box.id} box ${box.top}..${box.top + box.h} is outside 0..${layout.height}`)
}
for (const a of boxes) for (const b of boxes) {
if (a === b) continue
const apart = a.left + a.w <= b.left || b.left + b.w <= a.left || a.top + a.h <= b.top || b.top + b.h <= a.top
assert.ok(apart, `${a.id} overlaps ${b.id}`)
}
assert.equal(layout.frames.length, 1, 'the four branches stay in one dashed frame')
const frame = layout.frames[0]
assert.ok(frame.left >= 0 && frame.left + frame.width <= width, `frame ${frame.left}..${frame.left + frame.width} is outside 0..${width}`)
for (const id of ['r1', 'r2', 'r3', 'r4']) {
const box = layout.boxes.get(id)
assert.ok(box.left > frame.left && box.left + box.w < frame.left + frame.width, `${id} spills out of the parallel frame`)
assert.ok(box.top > frame.top && box.top + box.h < frame.top + frame.height, `${id} spills out of the parallel frame`)
}
for (const id of ['fork', 'join']) {
const box = layout.boxes.get(id)
assert.ok(box.top + box.h <= frame.top || box.top >= frame.top + frame.height, `${id} overlaps the parallel frame`)
}
assert.equal(layout.wires.filter(wire => wire.kind === 'fork' || wire.kind === 'join').length, 2)
for (const wire of layout.wires) {
for (const p of pointsOf(wire.d)) {
assert.ok(p.x >= 0 && p.x <= width && p.y >= 0 && p.y <= layout.height, `${wire.key} leaves the plane at ${p.x},${p.y}`)
}
}
})
}

test('the drawer flowchart draws the step layout, not the stored editor coordinates, and clamps a long run summary', async () => {
const graph = quickLane()
const long = 'p1641-r3 · #1641(Fixes #1640 social waiting-for-approval 卡不写目的地、没有跳转)在实时 head 重审并出 PASS 后入队。上一轮 run-d477062e 在 review 节点被 DSH 重启打断'
Expand Down
Loading