feat: render doTry/doCatch/doFinally like route-level handler sections
Backend: DO_TRY compounds now use a virtual _TRY_BODY wrapper with LR layout for the try body, while DO_CATCH/DO_FINALLY stack below as separate sections (TB). Edges from DO_TRY are skipped like route-level handler edges. Removes ELK-v2 debug logging. Frontend: _TRY_BODY renders as transparent wrapper, DO_CATCH as red tinted section, DO_FINALLY as teal section. DO_FINALLY color changed from red to teal (completion handler, not error). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,47 @@ export function CompoundNode({
|
||||
e => descendantIds.has(e.sourceId) && descendantIds.has(e.targetId),
|
||||
);
|
||||
|
||||
const childProps = {
|
||||
edges, selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay,
|
||||
overlayActive, iterationState, onIterationChange,
|
||||
onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave,
|
||||
};
|
||||
|
||||
// _TRY_BODY: transparent wrapper — no header, no border, just layout
|
||||
if (node.type === '_TRY_BODY') {
|
||||
return (
|
||||
<g transform={`translate(${x}, ${y})`}>
|
||||
{renderInternalEdges(internalEdges, absX, absY, executionOverlay)}
|
||||
{renderChildren(node, absX, absY, childProps)}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
// DO_CATCH / DO_FINALLY: section-like styling (tinted bg, thin border, label)
|
||||
if (node.type === 'DO_CATCH' || node.type === 'DO_FINALLY') {
|
||||
const sectionLabel = node.type === 'DO_CATCH'
|
||||
? (node.label ? `catch: ${node.label}` : 'catch')
|
||||
: (node.label ? `finally: ${node.label}` : 'finally');
|
||||
|
||||
return (
|
||||
<g data-node-id={node.id} transform={`translate(${x}, ${y})`}>
|
||||
{/* Tinted background */}
|
||||
<rect x={0} y={0} width={w} height={h} rx={CORNER_RADIUS}
|
||||
fill={color} fillOpacity={0.06} />
|
||||
{/* Border */}
|
||||
<rect x={0} y={0} width={w} height={h} rx={CORNER_RADIUS}
|
||||
fill="none" stroke={color} strokeWidth={1} strokeOpacity={0.4} />
|
||||
{/* Section label */}
|
||||
<text x={8} y={12} fill={color} fontSize={10} fontWeight={600}>
|
||||
{sectionLabel}
|
||||
</text>
|
||||
{renderInternalEdges(internalEdges, absX, absY, executionOverlay)}
|
||||
{renderChildren(node, absX, absY, childProps)}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
// Default compound rendering (DO_TRY, EIP_CHOICE, etc.)
|
||||
return (
|
||||
<g data-node-id={node.id} transform={`translate(${x}, ${y})`}>
|
||||
{/* Container body */}
|
||||
@@ -110,46 +151,56 @@ export function CompoundNode({
|
||||
</foreignObject>
|
||||
)}
|
||||
|
||||
{/* Internal edges (rendered after background, before children) */}
|
||||
<g className="edges">
|
||||
{internalEdges.map((edge, i) => {
|
||||
const isTraversed = executionOverlay
|
||||
? (executionOverlay.has(edge.sourceId) && executionOverlay.has(edge.targetId))
|
||||
: undefined;
|
||||
return (
|
||||
<DiagramEdge
|
||||
key={`${edge.sourceId}-${edge.targetId}-${i}`}
|
||||
edge={{
|
||||
...edge,
|
||||
points: edge.points.map(p => [p[0] - absX, p[1] - absY]),
|
||||
}}
|
||||
traversed={isTraversed}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
{renderInternalEdges(internalEdges, absX, absY, executionOverlay)}
|
||||
{renderChildren(node, absX, absY, childProps)}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
{/* Children — recurse into compound children, render leaves as DiagramNode */}
|
||||
/** Render internal edges adjusted for compound coordinates */
|
||||
function renderInternalEdges(
|
||||
internalEdges: DiagramEdgeType[],
|
||||
absX: number, absY: number,
|
||||
executionOverlay?: Map<string, NodeExecutionState>,
|
||||
) {
|
||||
return (
|
||||
<g className="edges">
|
||||
{internalEdges.map((edge, i) => {
|
||||
const isTraversed = executionOverlay
|
||||
? (executionOverlay.has(edge.sourceId) && executionOverlay.has(edge.targetId))
|
||||
: undefined;
|
||||
return (
|
||||
<DiagramEdge
|
||||
key={`${edge.sourceId}-${edge.targetId}-${i}`}
|
||||
edge={{
|
||||
...edge,
|
||||
points: edge.points.map(p => [p[0] - absX, p[1] - absY]),
|
||||
}}
|
||||
traversed={isTraversed}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
/** Render children — compounds recurse, leaves become DiagramNode */
|
||||
function renderChildren(
|
||||
node: DiagramNodeType,
|
||||
absX: number, absY: number,
|
||||
props: Omit<CompoundNodeProps, 'node' | 'parentX' | 'parentY'>,
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
{node.children?.map(child => {
|
||||
if (isCompoundType(child.type) && child.children && child.children.length > 0) {
|
||||
return (
|
||||
<CompoundNode
|
||||
key={child.id}
|
||||
node={child}
|
||||
edges={edges}
|
||||
parentX={absX}
|
||||
parentY={absY}
|
||||
selectedNodeId={selectedNodeId}
|
||||
hoveredNodeId={hoveredNodeId}
|
||||
nodeConfigs={nodeConfigs}
|
||||
executionOverlay={executionOverlay}
|
||||
overlayActive={overlayActive}
|
||||
iterationState={iterationState}
|
||||
onIterationChange={onIterationChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
onNodeEnter={onNodeEnter}
|
||||
onNodeLeave={onNodeLeave}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -161,19 +212,19 @@ export function CompoundNode({
|
||||
x: (child.x ?? 0) - absX,
|
||||
y: (child.y ?? 0) - absY,
|
||||
}}
|
||||
isHovered={hoveredNodeId === child.id}
|
||||
isSelected={selectedNodeId === child.id}
|
||||
config={child.id ? nodeConfigs?.get(child.id) : undefined}
|
||||
executionState={executionOverlay?.get(child.id ?? '')}
|
||||
overlayActive={overlayActive}
|
||||
onClick={() => child.id && onNodeClick(child.id)}
|
||||
onDoubleClick={() => child.id && onNodeDoubleClick?.(child.id)}
|
||||
onMouseEnter={() => child.id && onNodeEnter(child.id)}
|
||||
onMouseLeave={onNodeLeave}
|
||||
isHovered={props.hoveredNodeId === child.id}
|
||||
isSelected={props.selectedNodeId === child.id}
|
||||
config={child.id ? props.nodeConfigs?.get(child.id) : undefined}
|
||||
executionState={props.executionOverlay?.get(child.id ?? '')}
|
||||
overlayActive={props.overlayActive}
|
||||
onClick={() => child.id && props.onNodeClick(child.id)}
|
||||
onDoubleClick={() => child.id && props.onNodeDoubleClick?.(child.id)}
|
||||
onMouseEnter={() => child.id && props.onNodeEnter(child.id)}
|
||||
onMouseLeave={props.onNodeLeave}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ const TYPE_MAP: Record<string, string> = {
|
||||
TRY_CATCH: ERROR_COLOR,
|
||||
DO_TRY: ERROR_COLOR,
|
||||
DO_CATCH: ERROR_COLOR,
|
||||
DO_FINALLY: ERROR_COLOR,
|
||||
DO_FINALLY: '#1A7F8E', // teal — completion handler, not error
|
||||
|
||||
ON_COMPLETION: '#1A7F8E', // --running (teal, lifecycle handler)
|
||||
|
||||
@@ -60,7 +60,7 @@ const TYPE_MAP: Record<string, string> = {
|
||||
const COMPOUND_TYPES = new Set([
|
||||
'EIP_CHOICE', 'EIP_WHEN', 'EIP_OTHERWISE',
|
||||
'EIP_SPLIT', 'TRY_CATCH',
|
||||
'DO_TRY', 'DO_CATCH', 'DO_FINALLY',
|
||||
'DO_TRY', 'DO_CATCH', 'DO_FINALLY', '_TRY_BODY',
|
||||
'EIP_LOOP', 'EIP_MULTICAST', 'EIP_AGGREGATE',
|
||||
'ON_EXCEPTION', 'ERROR_HANDLER',
|
||||
'ON_COMPLETION',
|
||||
|
||||
Reference in New Issue
Block a user