fix(node-editor): auto-layout on load + viewport to top-left

Run auto-layout automatically when a workflow is loaded so nodes are
tidily positioned instead of scattered at persisted positions. Position
the viewport at (0,0) with zoom 1 so the laid-out nodes are visible in
the top-left corner.

- workflow-auto-layout.ts: change ORIGIN_X/Y from 40 to 0
- WorkflowNodeEditor.tsx: apply autoLayout on load, setViewport to top-left
- WorkflowNodeEditor.test.tsx: update test to verify layout runs on load
This commit is contained in:
gsxdsm
2026-06-06 00:56:12 -07:00
parent e1d6e4ecdc
commit d521661862
3 changed files with 32 additions and 12 deletions

View File

@@ -9,6 +9,7 @@ import {
MiniMap,
useNodesState,
useEdgesState,
useReactFlow,
type Connection,
type Node as FlowNode,
type Edge as FlowEdge,
@@ -750,6 +751,8 @@ function InnerEditor({
// localStorage unavailable (private mode / SSR): non-fatal.
}
}, [settingsCollapsed]);
// React Flow instance for programmatic viewport control (auto-layout on load).
const { setViewport } = useReactFlow();
// Wrapper around <ReactFlow> so keyboard deletion can return focus to the
// canvas container (R6) instead of leaving it on a now-removed node.
const canvasRef = useRef<HTMLDivElement>(null);
@@ -1070,11 +1073,15 @@ function InnerEditor({
return;
}
const flow = irToFlow(activeWorkflow);
setNodes(flow.nodes);
setEdges(flow.edges);
const loadedColumns = columnsOf(activeWorkflow);
const loadedFields = fieldsOf(activeWorkflow);
const loadedSettings = settingsOf(activeWorkflow);
// Auto-layout on load: compute tidy positions and apply them before the
// first render so nodes are visible in the top-left viewport.
const layoutPositions = autoLayout(flow.nodes, flow.edges, loadedColumns);
const laidOutNodes = applyAutoLayout(flow.nodes, layoutPositions);
setNodes(laidOutNodes);
setEdges(flow.edges);
setColumns(loadedColumns);
setFields(loadedFields);
setSettings(loadedSettings);
@@ -1087,7 +1094,7 @@ function InnerEditor({
loadedSnapshotRef.current = serializeGraph(
activeWorkflow.name,
activeWorkflow.description ?? "",
flow.nodes,
laidOutNodes,
flow.edges,
loadedColumns,
loadedFields,
@@ -1096,6 +1103,8 @@ function InnerEditor({
setSelectedNodeId(null);
setSelectedEdgeId(null);
setValidationError(null);
// Position viewport at top-left so the laid-out nodes are visible.
setViewport({ x: 0, y: 0, zoom: 1 }, { duration: 0 });
// Honor a pending AI interpreter-only flag exactly once for the workflow it
// just activated; otherwise the banner clears on load (U10/R11).
if (pendingInterpreterOnlyRef.current) {
@@ -1104,7 +1113,7 @@ function InnerEditor({
} else {
setInterpreterOnly(false);
}
}, [activeWorkflow, setNodes, setEdges]);
}, [activeWorkflow, setNodes, setEdges, setViewport]);
// `?panel=settings` deep link (U6/U9 redirect stubs): once the active workflow
// has loaded, scroll the settings panel into view. Runs once per editor open.

View File

@@ -427,7 +427,7 @@ describe("WorkflowNodeEditor — U5 auto-layout", () => {
expect(screen.queryByTestId("wf-auto-layout")).not.toBeInTheDocument();
});
it("repositions nodes on click (a node's transform changes)", async () => {
it("runs auto-layout on load (nodes are positioned at layout positions)", async () => {
vi.mocked(fetchWorkflows).mockResolvedValue([v2Def()]);
const { container } = render(
<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />,
@@ -436,14 +436,25 @@ describe("WorkflowNodeEditor — U5 auto-layout", () => {
// React Flow positions step nodes via a translate transform on their wrapper.
const wrapperFor = (id: string) =>
container.querySelector<HTMLElement>(`.react-flow__node[data-id="${id}"]`);
const before = wrapperFor("step")?.style.transform ?? "";
fireEvent.click(screen.getByTestId("wf-auto-layout"));
// After load, the step node should have been auto-laid-out (positioned).
await waitFor(() => {
const after = wrapperFor("step")?.style.transform ?? "";
expect(after).not.toBe("");
expect(after).not.toBe(before);
const transform = wrapperFor("step")?.style.transform ?? "";
expect(transform).not.toBe("");
});
});
it("clicking auto-layout still works after initial load", async () => {
vi.mocked(fetchWorkflows).mockResolvedValue([v2Def()]);
render(
<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />,
);
await screen.findByTestId("wf-node-start");
// The auto-layout button should still be present and clickable.
const btn = screen.getByTestId("wf-auto-layout");
expect(btn).toBeInTheDocument();
// Clicking it should not throw.
fireEvent.click(btn);
});
});
// ── U8: step-inversion authoring (foreach/step-review/parse-steps/code) ──────

View File

@@ -37,8 +37,8 @@ export const WF_AUTO_LAYOUT_BAND_PADDING = 16;
export const WF_AUTO_LAYOUT_SPACING = WF_CARD_MAX_WIDTH + WF_AUTO_LAYOUT_GAP_X;
/** Left/top origin for the laid-out graph. */
const ORIGIN_X = 40;
const ORIGIN_Y = 40;
const ORIGIN_X = 0;
const ORIGIN_Y = 0;
/** Row height for a stacked card (card + vertical gap). */
const ROW_HEIGHT = WF_CARD_HEIGHT + WF_AUTO_LAYOUT_GAP_Y;