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:
Bill Leoutsakos
2026-08-24 00:58:03 -07:00
committed by GitHub
co-authored by Bill Leoutsakos Waleed Latif
parent 67fc2aeb30
commit 528b34f564
15 changed files with 383 additions and 20 deletions
@@ -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,
+3 -1
View File
@@ -10,6 +10,7 @@
"build": "fumadocs-mdx && NODE_OPTIONS='--max-old-space-size=8192' next build",
"start": "next start",
"postinstall": "fumadocs-mdx",
"test": "vitest run",
"type-check": "fumadocs-mdx && tsc --noEmit",
"lint": "biome check --write --unsafe .",
"lint:check": "biome check .",
@@ -47,6 +48,7 @@
"@types/react-dom": "^19.0.4",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.12",
"typescript": "^7.0.2"
"typescript": "^7.0.2",
"vitest": "^4.1.0"
}
}