Merge pull request #1465 from Runfusion/gsxdsm/node-editor-fix
fix(node-editor): auto-layout on load + viewport to top-left
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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) ──────
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user