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:
@@ -33,10 +33,10 @@ export function ProcessDiagram({
|
||||
const contentWidth = totalWidth + PADDING * 2;
|
||||
const contentHeight = totalHeight + PADDING * 2;
|
||||
|
||||
// Fit to view on first data load
|
||||
// Reset to 100% at top-left on first data load
|
||||
useEffect(() => {
|
||||
if (totalWidth > 0 && totalHeight > 0) {
|
||||
zoom.fitToView(contentWidth, contentHeight);
|
||||
zoom.resetView();
|
||||
}
|
||||
}, [totalWidth, totalHeight]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -98,7 +98,6 @@ export function ProcessDiagram({
|
||||
>
|
||||
<svg
|
||||
className={styles.svg}
|
||||
viewBox={zoom.viewBox(contentWidth, contentHeight)}
|
||||
onWheel={zoom.onWheel}
|
||||
onPointerDown={zoom.onPointerDown}
|
||||
onPointerMove={zoom.onPointerMove}
|
||||
@@ -120,10 +119,10 @@ export function ProcessDiagram({
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<g transform={`translate(${PADDING}, ${PADDING})`}>
|
||||
{/* Main section edges */}
|
||||
<g style={{ transform: zoom.transform, transformOrigin: '0 0' }}>
|
||||
{/* Main section top-level edges (not inside compounds) */}
|
||||
<g className="edges">
|
||||
{mainSection.edges.map((edge, i) => (
|
||||
{mainSection.edges.filter(e => topLevelEdge(e, mainSection.nodes)).map((edge, i) => (
|
||||
<DiagramEdge key={`${edge.sourceId}-${edge.targetId}-${i}`} edge={edge} />
|
||||
))}
|
||||
</g>
|
||||
@@ -136,6 +135,7 @@ export function ProcessDiagram({
|
||||
<CompoundNode
|
||||
key={node.id}
|
||||
node={node}
|
||||
edges={mainSection.edges}
|
||||
selectedNodeId={selectedNodeId}
|
||||
hoveredNodeId={toolbar.hoveredNodeId}
|
||||
nodeConfigs={nodeConfigs}
|
||||
@@ -160,22 +160,7 @@ export function ProcessDiagram({
|
||||
})}
|
||||
</g>
|
||||
|
||||
{/* Toolbar for hovered node */}
|
||||
{toolbar.hoveredNodeId && onNodeAction && (() => {
|
||||
const hNode = findNodeById(sections, toolbar.hoveredNodeId!);
|
||||
if (!hNode) return null;
|
||||
return (
|
||||
<NodeToolbar
|
||||
nodeId={toolbar.hoveredNodeId!}
|
||||
nodeX={hNode.x ?? 0}
|
||||
nodeY={hNode.y ?? 0}
|
||||
nodeWidth={hNode.width ?? 120}
|
||||
onAction={handleNodeAction}
|
||||
onMouseEnter={toolbar.onToolbarEnter}
|
||||
onMouseLeave={toolbar.onToolbarLeave}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
{/* Toolbar rendered as HTML overlay below */}
|
||||
|
||||
{/* Error handler sections */}
|
||||
{errorSections.map((section, i) => (
|
||||
@@ -194,6 +179,27 @@ export function ProcessDiagram({
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
{/* Node toolbar — HTML overlay, fixed size regardless of zoom */}
|
||||
{toolbar.hoveredNodeId && onNodeAction && (() => {
|
||||
const hNode = findNodeById(sections, toolbar.hoveredNodeId!);
|
||||
if (!hNode) return null;
|
||||
// Convert SVG coordinates to screen-space using zoom transform
|
||||
const nodeCenter = (hNode.x ?? 0) + (hNode.width ?? 160) / 2;
|
||||
const nodeTop = hNode.y ?? 0;
|
||||
const screenX = nodeCenter * zoom.state.scale + zoom.state.translateX;
|
||||
const screenY = nodeTop * zoom.state.scale + zoom.state.translateY;
|
||||
return (
|
||||
<NodeToolbar
|
||||
nodeId={toolbar.hoveredNodeId!}
|
||||
screenX={screenX}
|
||||
screenY={screenY}
|
||||
onAction={handleNodeAction}
|
||||
onMouseEnter={toolbar.onToolbarEnter}
|
||||
onMouseLeave={toolbar.onToolbarLeave}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
|
||||
<ZoomControls
|
||||
onZoomIn={zoom.zoomIn}
|
||||
onZoomOut={zoom.zoomOut}
|
||||
@@ -212,10 +218,49 @@ function findNodeById(
|
||||
for (const node of section.nodes) {
|
||||
if (node.id === nodeId) return node;
|
||||
if (node.children) {
|
||||
const child = node.children.find(c => c.id === nodeId);
|
||||
if (child) return child;
|
||||
const found = findInChildren(node.children, nodeId);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findInChildren(
|
||||
nodes: import('../../api/queries/diagrams').DiagramNode[],
|
||||
nodeId: string,
|
||||
): import('../../api/queries/diagrams').DiagramNode | undefined {
|
||||
for (const n of nodes) {
|
||||
if (n.id === nodeId) return n;
|
||||
if (n.children) {
|
||||
const found = findInChildren(n.children, nodeId);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Returns true if the edge connects two top-level nodes (not inside any compound). */
|
||||
function topLevelEdge(
|
||||
edge: import('../../api/queries/diagrams').DiagramEdge,
|
||||
nodes: import('../../api/queries/diagrams').DiagramNode[],
|
||||
): boolean {
|
||||
// Collect all IDs that are children of compound nodes (at any depth)
|
||||
const compoundChildIds = new Set<string>();
|
||||
for (const n of nodes) {
|
||||
if (n.children && n.children.length > 0) {
|
||||
collectDescendantIds(n.children, compoundChildIds);
|
||||
}
|
||||
}
|
||||
return !compoundChildIds.has(edge.sourceId) && !compoundChildIds.has(edge.targetId);
|
||||
}
|
||||
|
||||
function collectDescendantIds(
|
||||
nodes: import('../../api/queries/diagrams').DiagramNode[],
|
||||
set: Set<string>,
|
||||
) {
|
||||
for (const n of nodes) {
|
||||
if (n.id) set.add(n.id);
|
||||
if (n.children) collectDescendantIds(n.children, set);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user