mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(workflow): hide idle nested subflow end handles (#6976)
* fix(workflow): hide idle nested subflow end handles * perf(workflow): avoid repeated subflow edge scans * perf(workflow): stabilize subflow edge selector --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> Co-authored-by: Waleed Latif <walif6@gmail.com>
This commit is contained in:
co-authored by
Bill Leoutsakos
Waleed Latif
parent
67fc2aeb30
commit
528b34f564
@@ -8,6 +8,7 @@ interface DocsContainerData {
|
||||
name: string
|
||||
blockType: string
|
||||
size?: { width: number; height: number }
|
||||
parentId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,6 +25,7 @@ export const DocsContainerNode = memo(function DocsContainerNode({
|
||||
name: data.name,
|
||||
width: data.size?.width,
|
||||
height: data.size?.height,
|
||||
parentId: data.parentId,
|
||||
isPreview: true,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { BLOCK_Z_BASE, CONTAINER_CHILD_Z_BASE, getEdgeZIndex } from '@sim/workflow-renderer'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { type PreviewBlock, type PreviewWorkflow, toReactFlowElements } from './workflow-data'
|
||||
|
||||
const block = (
|
||||
overrides: Partial<PreviewBlock> & Pick<PreviewBlock, 'id' | 'type'>
|
||||
): PreviewBlock => ({
|
||||
name: overrides.id,
|
||||
bgColor: '#000000',
|
||||
rows: [],
|
||||
position: { x: 0, y: 0 },
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const workflow: PreviewWorkflow = {
|
||||
id: 'nested-subflows',
|
||||
name: 'Nested subflows',
|
||||
blocks: [
|
||||
block({ id: 'start', type: 'starter' }),
|
||||
block({ id: 'loop', type: 'loop', size: { width: 500, height: 300 } }),
|
||||
block({
|
||||
id: 'parallel',
|
||||
type: 'parallel',
|
||||
parentId: 'loop',
|
||||
position: { x: 24, y: 64 },
|
||||
size: { width: 400, height: 200 },
|
||||
}),
|
||||
block({ id: 'agent', type: 'agent', parentId: 'loop', position: { x: 24, y: 140 } }),
|
||||
],
|
||||
edges: [
|
||||
{ id: 'start-loop', source: 'start', target: 'loop' },
|
||||
{ id: 'loop-parallel', source: 'loop', target: 'parallel' },
|
||||
{ id: 'loop-agent', source: 'loop', target: 'agent' },
|
||||
],
|
||||
}
|
||||
|
||||
describe('toReactFlowElements layering', () => {
|
||||
it('places incoming edges on their container target layer', () => {
|
||||
const { nodes, edges } = toReactFlowElements(workflow, false, {
|
||||
highlightEdge: 'loop-parallel',
|
||||
})
|
||||
const nodeById = new Map(nodes.map((node) => [node.id, node]))
|
||||
const edgeById = new Map(edges.map((edge) => [edge.id, edge]))
|
||||
|
||||
expect(nodeById.get('loop')?.zIndex).toBe(0)
|
||||
expect(nodeById.get('parallel')?.zIndex).toBe(1)
|
||||
expect(edgeById.get('start-loop')?.zIndex).toBe(0)
|
||||
expect(edgeById.get('loop-parallel')?.zIndex).toBe(1)
|
||||
})
|
||||
|
||||
it('keeps ordinary cards above normally layered edges', () => {
|
||||
const { nodes, edges } = toReactFlowElements(workflow)
|
||||
const nodeById = new Map(nodes.map((node) => [node.id, node]))
|
||||
const edgeById = new Map(edges.map((edge) => [edge.id, edge]))
|
||||
|
||||
expect(nodeById.get('start')?.zIndex).toBe(BLOCK_Z_BASE)
|
||||
expect(nodeById.get('agent')?.zIndex).toBe(CONTAINER_CHILD_Z_BASE)
|
||||
expect(edgeById.get('loop-agent')?.zIndex).toBe(getEdgeZIndex(0))
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
BLOCK_Z_BASE,
|
||||
CONTAINER_CHILD_Z_BASE,
|
||||
getEdgeZIndex,
|
||||
getEdgeZIndexForTarget,
|
||||
} from '@sim/workflow-renderer'
|
||||
import { type Edge, type Node, Position } from 'reactflow'
|
||||
|
||||
/**
|
||||
@@ -61,6 +67,24 @@ export interface HighlightOptions {
|
||||
selectedBlock?: string
|
||||
}
|
||||
|
||||
/** Semantic container depth used for z-order while docs positions stay flattened. */
|
||||
function getNestingDepth(block: PreviewBlock, blocksById: Map<string, PreviewBlock>): number {
|
||||
let depth = 0
|
||||
let parentId = block.parentId
|
||||
const visited = new Set<string>()
|
||||
|
||||
while (parentId && !visited.has(parentId)) {
|
||||
const parent = blocksById.get(parentId)
|
||||
if (!parent) break
|
||||
|
||||
visited.add(parentId)
|
||||
depth += 1
|
||||
parentId = parent.parentId
|
||||
}
|
||||
|
||||
return depth
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a {@link PreviewWorkflow} to React Flow nodes and edges.
|
||||
*
|
||||
@@ -81,6 +105,7 @@ export function toReactFlowElements(
|
||||
|
||||
const nodes: Node[] = workflow.blocks.map((block, index) => {
|
||||
const isContainer = Boolean(block.size)
|
||||
const nestingDepth = getNestingDepth(block, blocksById)
|
||||
// Nested blocks are authored relative to their container; render them at
|
||||
// absolute coordinates (not React Flow parentNode children) so the edges
|
||||
// between a container and its nested blocks render reliably and on top.
|
||||
@@ -92,7 +117,7 @@ export function toReactFlowElements(
|
||||
id: block.id,
|
||||
type: isContainer ? 'previewContainer' : 'previewBlock',
|
||||
position,
|
||||
zIndex: isContainer ? 0 : 1,
|
||||
zIndex: isContainer ? nestingDepth : block.parentId ? CONTAINER_CHILD_Z_BASE : BLOCK_Z_BASE,
|
||||
...(block.size ? { style: { width: block.size.width, height: block.size.height } } : {}),
|
||||
data: {
|
||||
name: block.name,
|
||||
@@ -103,6 +128,7 @@ export function toReactFlowElements(
|
||||
tools: block.tools,
|
||||
hideTargetHandle: block.hideTargetHandle,
|
||||
size: block.size,
|
||||
parentId: block.parentId,
|
||||
index,
|
||||
animate,
|
||||
isHighlighted: highlightBlock === block.id || selectedBlock === block.id,
|
||||
@@ -127,6 +153,14 @@ export function toReactFlowElements(
|
||||
// so edges into and out of Loop/Parallel containers still connect.
|
||||
const sourceBlock = blocksById.get(e.source)
|
||||
const targetBlock = blocksById.get(e.target)
|
||||
const parentContainer = blocksById.get(sourceBlock?.parentId ?? targetBlock?.parentId ?? '')
|
||||
const baseZIndex = getEdgeZIndex(
|
||||
parentContainer ? getNestingDepth(parentContainer, blocksById) : undefined,
|
||||
{ isHighlighted: isEdgeHighlight }
|
||||
)
|
||||
const targetContainerZIndex = targetBlock?.size
|
||||
? getNestingDepth(targetBlock, blocksById)
|
||||
: undefined
|
||||
const sourceHandle =
|
||||
e.sourceHandle ?? (sourceBlock?.size ? `${sourceBlock.type}-end-source` : 'source')
|
||||
const targetHandle = targetBlock?.size ? undefined : 'target'
|
||||
@@ -142,6 +176,7 @@ export function toReactFlowElements(
|
||||
},
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
zIndex: getEdgeZIndexForTarget(baseZIndex, targetContainerZIndex),
|
||||
data: {
|
||||
animate,
|
||||
delay: animate ? sourceIndex * BLOCK_STAGGER + BLOCK_STAGGER : 0,
|
||||
|
||||
Reference in New Issue
Block a user