fix: diagram rendering improvements
- Recursive compound rendering: CompoundNode checks if children are themselves compound types (WHEN inside CHOICE) and renders them recursively. Added EIP_WHEN, EIP_OTHERWISE, DO_CATCH, DO_FINALLY to frontend COMPOUND_TYPES. - Edge z-ordering: edges are distributed to their containing compound and rendered after the background rect, so they're not hidden behind compound containers. - Error section sizing: normalize error handler node coordinates to start at (0,0), compute red tint background height from actual content with symmetric padding for vertical centering. - Toolbar as HTML overlay: moved from SVG foreignObject to absolute- positioned HTML div so it stays fixed size at any zoom level. Uses design system tokens for consistent styling. - Zoom: replaced viewBox approach with CSS transform on content group. Default zoom is 100% anchored top-left. Fit-to-view still available via button. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,19 @@
|
||||
import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
|
||||
import type { DiagramNode as DiagramNodeType, DiagramEdge as DiagramEdgeType } from '../../api/queries/diagrams';
|
||||
import type { NodeConfig } from './types';
|
||||
import { colorForType } from './node-colors';
|
||||
import { colorForType, isCompoundType } from './node-colors';
|
||||
import { DiagramNode } from './DiagramNode';
|
||||
import { DiagramEdge } from './DiagramEdge';
|
||||
|
||||
const HEADER_HEIGHT = 22;
|
||||
const CORNER_RADIUS = 4;
|
||||
|
||||
interface CompoundNodeProps {
|
||||
node: DiagramNodeType;
|
||||
/** All edges for this section — compound filters to its own internal edges */
|
||||
edges: DiagramEdgeType[];
|
||||
/** Absolute offset of the nearest compound ancestor (for coordinate adjustment) */
|
||||
parentX?: number;
|
||||
parentY?: number;
|
||||
selectedNodeId?: string;
|
||||
hoveredNodeId: string | null;
|
||||
nodeConfigs?: Map<string, NodeConfig>;
|
||||
@@ -17,17 +23,28 @@ interface CompoundNodeProps {
|
||||
}
|
||||
|
||||
export function CompoundNode({
|
||||
node, selectedNodeId, hoveredNodeId, nodeConfigs,
|
||||
node, edges, parentX = 0, parentY = 0,
|
||||
selectedNodeId, hoveredNodeId, nodeConfigs,
|
||||
onNodeClick, onNodeEnter, onNodeLeave,
|
||||
}: CompoundNodeProps) {
|
||||
const x = node.x ?? 0;
|
||||
const y = node.y ?? 0;
|
||||
const x = (node.x ?? 0) - parentX;
|
||||
const y = (node.y ?? 0) - parentY;
|
||||
const absX = node.x ?? 0;
|
||||
const absY = node.y ?? 0;
|
||||
const w = node.width ?? 200;
|
||||
const h = node.height ?? 100;
|
||||
const color = colorForType(node.type);
|
||||
const typeName = node.type?.replace(/^EIP_/, '').replace(/_/g, ' ') ?? '';
|
||||
const label = node.label ? `${typeName}: ${node.label}` : typeName;
|
||||
|
||||
// Collect all descendant node IDs to filter edges that belong inside this compound
|
||||
const descendantIds = new Set<string>();
|
||||
collectIds(node.children ?? [], descendantIds);
|
||||
|
||||
const internalEdges = edges.filter(
|
||||
e => descendantIds.has(e.sourceId) && descendantIds.has(e.targetId),
|
||||
);
|
||||
|
||||
return (
|
||||
<g data-node-id={node.id} transform={`translate(${x}, ${y})`}>
|
||||
{/* Container body */}
|
||||
@@ -58,25 +75,62 @@ export function CompoundNode({
|
||||
{label}
|
||||
</text>
|
||||
|
||||
{/* Children nodes (positioned relative to compound) */}
|
||||
{node.children?.map(child => (
|
||||
<DiagramNode
|
||||
key={child.id}
|
||||
node={{
|
||||
...child,
|
||||
// Children have absolute coordinates from the backend,
|
||||
// but since we're inside the compound's translate, subtract parent offset
|
||||
x: (child.x ?? 0) - x,
|
||||
y: (child.y ?? 0) - y,
|
||||
}}
|
||||
isHovered={hoveredNodeId === child.id}
|
||||
isSelected={selectedNodeId === child.id}
|
||||
config={child.id ? nodeConfigs?.get(child.id) : undefined}
|
||||
onClick={() => child.id && onNodeClick(child.id)}
|
||||
onMouseEnter={() => child.id && onNodeEnter(child.id)}
|
||||
onMouseLeave={onNodeLeave}
|
||||
/>
|
||||
))}
|
||||
{/* Internal edges (rendered after background, before children) */}
|
||||
<g className="edges">
|
||||
{internalEdges.map((edge, i) => (
|
||||
<DiagramEdge
|
||||
key={`${edge.sourceId}-${edge.targetId}-${i}`}
|
||||
edge={{
|
||||
...edge,
|
||||
points: edge.points.map(p => [p[0] - absX, p[1] - absY]),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
|
||||
{/* Children — recurse into compound children, render leaves as DiagramNode */}
|
||||
{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}
|
||||
onNodeClick={onNodeClick}
|
||||
onNodeEnter={onNodeEnter}
|
||||
onNodeLeave={onNodeLeave}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<DiagramNode
|
||||
key={child.id}
|
||||
node={{
|
||||
...child,
|
||||
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}
|
||||
onClick={() => child.id && onNodeClick(child.id)}
|
||||
onMouseEnter={() => child.id && onNodeEnter(child.id)}
|
||||
onMouseLeave={onNodeLeave}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
function collectIds(nodes: DiagramNodeType[], set: Set<string>) {
|
||||
for (const n of nodes) {
|
||||
if (n.id) set.add(n.id);
|
||||
if (n.children) collectIds(n.children, set);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user