The root cause of the Y-offset: ELK places main flow nodes at arbitrary positions (e.g., y=679) within its root graph, and the frontend rendered them at those raw positions. Handler sections were already normalized via shiftNodes, but the main section was not. Now useDiagramData.ts applies the same normalization to the main section: computes bounding box, shifts nodes and edges so the section starts at (0,0). This fixes the Y-offset regardless of what ELK produces internally. Removed the backend normalizePositions (was ineffective because handler nodes at y=12 dominated the global minimum, preventing meaningful shift of main flow nodes at y=679). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
170 lines
5.6 KiB
TypeScript
170 lines
5.6 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { useDiagramByRoute } from '../../api/queries/diagrams';
|
|
import type { DiagramNode, DiagramEdge, DiagramLayout } from '../../api/queries/diagrams';
|
|
import type { DiagramSection } from './types';
|
|
import { isErrorCompoundType, isCompletionCompoundType } from './node-colors';
|
|
|
|
const SECTION_GAP = 80;
|
|
|
|
export function useDiagramData(
|
|
application: string,
|
|
routeId: string,
|
|
direction: 'LR' | 'TB' = 'LR',
|
|
preloadedLayout?: DiagramLayout,
|
|
) {
|
|
// When a preloaded layout is provided, disable the internal fetch
|
|
const fetchApp = preloadedLayout ? undefined : application;
|
|
const fetchRoute = preloadedLayout ? undefined : routeId;
|
|
const { data: fetchedLayout, isLoading, error } = useDiagramByRoute(fetchApp, fetchRoute, direction);
|
|
|
|
const layout = preloadedLayout ?? fetchedLayout;
|
|
|
|
const result = useMemo(() => {
|
|
if (!layout?.nodes) {
|
|
return { sections: [], totalWidth: 0, totalHeight: 0 };
|
|
}
|
|
|
|
const allEdges = layout.edges ?? [];
|
|
|
|
// Separate main nodes from completion and error handler compound sections
|
|
const mainNodes: DiagramNode[] = [];
|
|
const completionSections: { label: string; nodes: DiagramNode[] }[] = [];
|
|
const errorSections: { label: string; nodes: DiagramNode[] }[] = [];
|
|
|
|
for (const node of layout.nodes) {
|
|
if (isCompletionCompoundType(node.type) && node.children && node.children.length > 0) {
|
|
completionSections.push({
|
|
label: node.label || 'onCompletion',
|
|
nodes: node.children,
|
|
});
|
|
} else if (isErrorCompoundType(node.type) && node.children && node.children.length > 0) {
|
|
errorSections.push({
|
|
label: node.label || 'Error Handler',
|
|
nodes: node.children,
|
|
});
|
|
} else {
|
|
mainNodes.push(node);
|
|
}
|
|
}
|
|
|
|
// Collect node IDs for edge filtering
|
|
const mainNodeIds = new Set<string>();
|
|
collectNodeIds(mainNodes, mainNodeIds);
|
|
|
|
const mainEdges = allEdges.filter(
|
|
e => mainNodeIds.has(e.sourceId) && mainNodeIds.has(e.targetId),
|
|
);
|
|
|
|
// Normalize main section to start at (0, 0) — ELK can place nodes
|
|
// at arbitrary positions within its root graph
|
|
const mainBounds = computeBounds(mainNodes);
|
|
const mainOffX = mainBounds.minX;
|
|
const mainOffY = mainBounds.minY;
|
|
const shiftedMainNodes = shiftNodes(mainNodes, mainOffX, mainOffY);
|
|
const shiftedMainEdges = mainEdges.map(e => ({
|
|
...e,
|
|
points: e.points.map(p => [p[0] - mainOffX, p[1] - mainOffY]),
|
|
}));
|
|
const mainWidth = mainBounds.maxX - mainBounds.minX;
|
|
const mainHeight = mainBounds.maxY - mainBounds.minY;
|
|
|
|
const sections: DiagramSection[] = [
|
|
{
|
|
label: 'Main Route',
|
|
nodes: shiftedMainNodes,
|
|
edges: shiftedMainEdges,
|
|
offsetY: 0,
|
|
},
|
|
];
|
|
|
|
let currentY = mainHeight + SECTION_GAP;
|
|
let maxWidth = mainWidth;
|
|
|
|
const addHandlerSections = (
|
|
handlers: { label: string; nodes: DiagramNode[] }[],
|
|
variant: 'completion' | 'error',
|
|
) => {
|
|
for (const hs of handlers) {
|
|
const bounds = computeBounds(hs.nodes);
|
|
const offX = bounds.minX;
|
|
const offY = bounds.minY;
|
|
const shiftedNodes = shiftNodes(hs.nodes, offX, offY);
|
|
const nodeIds = new Set<string>();
|
|
collectNodeIds(hs.nodes, nodeIds);
|
|
const edges = allEdges
|
|
.filter(e => nodeIds.has(e.sourceId) && nodeIds.has(e.targetId))
|
|
.map(e => ({
|
|
...e,
|
|
points: e.points.map(p => [p[0] - offX, p[1] - offY]),
|
|
}));
|
|
const sectionHeight = bounds.maxY - bounds.minY;
|
|
const sectionWidth = bounds.maxX - bounds.minX;
|
|
sections.push({
|
|
label: hs.label,
|
|
nodes: shiftedNodes,
|
|
edges,
|
|
offsetY: currentY,
|
|
variant,
|
|
});
|
|
currentY += sectionHeight + SECTION_GAP;
|
|
if (sectionWidth > maxWidth) maxWidth = sectionWidth;
|
|
}
|
|
};
|
|
|
|
// Completion handlers first (above error handlers)
|
|
addHandlerSections(completionSections, 'completion');
|
|
// Then error handlers
|
|
addHandlerSections(errorSections, 'error');
|
|
|
|
const totalWidth = Math.max(layout.width ?? 0, mainWidth, maxWidth);
|
|
const totalHeight = currentY;
|
|
|
|
return { sections, totalWidth, totalHeight };
|
|
}, [layout]);
|
|
|
|
return {
|
|
...result,
|
|
isLoading: preloadedLayout ? false : isLoading,
|
|
error: preloadedLayout ? null : error,
|
|
};
|
|
}
|
|
|
|
/** Shift all node coordinates by subtracting an offset, recursively. */
|
|
function shiftNodes(nodes: DiagramNode[], offX: number, offY: number): DiagramNode[] {
|
|
return nodes.map(n => ({
|
|
...n,
|
|
x: (n.x ?? 0) - offX,
|
|
y: (n.y ?? 0) - offY,
|
|
children: n.children ? shiftNodes(n.children, offX, offY) : undefined,
|
|
}));
|
|
}
|
|
|
|
function collectNodeIds(nodes: DiagramNode[], set: Set<string>) {
|
|
for (const n of nodes) {
|
|
if (n.id) set.add(n.id);
|
|
if (n.children) collectNodeIds(n.children, set);
|
|
}
|
|
}
|
|
|
|
function computeBounds(nodes: DiagramNode[]): {
|
|
minX: number; minY: number; maxX: number; maxY: number;
|
|
} {
|
|
let minX = Infinity, minY = Infinity, maxX = 0, maxY = 0;
|
|
for (const n of nodes) {
|
|
const x = n.x ?? 0;
|
|
const y = n.y ?? 0;
|
|
const w = n.width ?? 80;
|
|
const h = n.height ?? 40;
|
|
if (x < minX) minX = x;
|
|
if (y < minY) minY = y;
|
|
if (x + w > maxX) maxX = x + w;
|
|
if (y + h > maxY) maxY = y + h;
|
|
if (n.children) {
|
|
const childBounds = computeBounds(n.children);
|
|
if (childBounds.maxX > maxX) maxX = childBounds.maxX;
|
|
if (childBounds.maxY > maxY) maxY = childBounds.maxY;
|
|
}
|
|
}
|
|
return { minX: minX === Infinity ? 0 : minX, minY: minY === Infinity ? 0 : minY, maxX, maxY };
|
|
}
|