From a8d2cbdab075797b0ae3798afe95ef636356728e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 14:25:48 +0000 Subject: [PATCH] feat(editor): round-trip pinned_input and pinned_output through graphAdapter --- frontend/src/api/types.ts | 2 + frontend/src/editor/graphAdapter.test.ts | 70 ++++++++++++++++++++++++ frontend/src/editor/graphAdapter.ts | 23 +++++--- 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 3e24cbc..79b9f6f 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -47,6 +47,8 @@ export interface GraphNode { position: { x: number; y: number }; config: Record; connection_id?: string | null; + pinned_input?: unknown; + pinned_output?: unknown; } export interface GraphEdge { diff --git a/frontend/src/editor/graphAdapter.test.ts b/frontend/src/editor/graphAdapter.test.ts index 2cdbc8e..fdf4bc2 100644 --- a/frontend/src/editor/graphAdapter.test.ts +++ b/frontend/src/editor/graphAdapter.test.ts @@ -164,3 +164,73 @@ describe("toGraph — schema-typed coercion on save", () => { expect(g.nodes[0].config.ratio).toBe(100); }); }); + +describe("graphAdapter — pinned data round-trip", () => { + it("fromGraph copies pinned_input and pinned_output into VcNodeData", () => { + const g = { + nodes: [ + { + id: "n1", type_id: "http.response", + position: { x: 0, y: 0 }, config: {}, + pinned_input: { a: 1 }, + }, + { + id: "t1", type_id: "http.webhook", + position: { x: 0, y: 0 }, config: {}, + pinned_output: [{ b: 1 }], + }, + ], + edges: [], + }; + const { nodes } = fromGraph(g); + expect(nodes[0].data.pinnedInput).toEqual({ a: 1 }); + expect(nodes[0].data.pinnedOutput).toBeUndefined(); + expect(nodes[1].data.pinnedOutput).toEqual([{ b: 1 }]); + expect(nodes[1].data.pinnedInput).toBeUndefined(); + }); + + it("toGraph copies pinnedInput and pinnedOutput back to graph JSON", () => { + const g = toGraph( + [{ + id: "n1", type: "vc", + position: { x: 0, y: 0 }, + data: { + typeId: "http.response", config: {}, + pinnedInput: { a: 1 }, + }, + }, { + id: "t1", type: "vc", + position: { x: 0, y: 0 }, + data: { + typeId: "http.webhook", config: {}, + pinnedOutput: [{ b: 1 }], + }, + }], + [], + [], + ); + expect(g.nodes[0].pinned_input).toEqual({ a: 1 }); + expect(g.nodes[0].pinned_output).toBeUndefined(); + expect(g.nodes[1].pinned_output).toEqual([{ b: 1 }]); + expect(g.nodes[1].pinned_input).toBeUndefined(); + }); + + it("null pin values are dropped rather than sent as null", () => { + // toGraph should omit the field when it's null/undefined so the wire + // payload stays compact and unchanged for nodes that never used pins. + const g = toGraph( + [{ + id: "n1", type: "vc", + position: { x: 0, y: 0 }, + data: { + typeId: "http.response", config: {}, + pinnedInput: null, pinnedOutput: null, + }, + }], + [], + [], + ); + expect(g.nodes[0].pinned_input).toBeUndefined(); + expect(g.nodes[0].pinned_output).toBeUndefined(); + }); +}); diff --git a/frontend/src/editor/graphAdapter.ts b/frontend/src/editor/graphAdapter.ts index 6012d55..769cf4d 100644 --- a/frontend/src/editor/graphAdapter.ts +++ b/frontend/src/editor/graphAdapter.ts @@ -6,20 +6,27 @@ export interface VcNodeData extends Record { name?: string; config: Record; connection_id?: string | null; + pinnedInput?: unknown; + pinnedOutput?: unknown; } export function fromGraph(g: WorkflowGraph): { nodes: Node[]; edges: Edge[] } { - const nodes: Node[] = g.nodes.map((n) => ({ - id: n.id, - type: "vc", - position: { x: n.position?.x ?? 0, y: n.position?.y ?? 0 }, - data: { + const nodes: Node[] = g.nodes.map((n) => { + const data: VcNodeData = { typeId: n.type_id, name: n.name, config: n.config ?? {}, connection_id: n.connection_id ?? null, - }, - })); + }; + if (n.pinned_input !== undefined && n.pinned_input !== null) data.pinnedInput = n.pinned_input; + if (n.pinned_output !== undefined && n.pinned_output !== null) data.pinnedOutput = n.pinned_output; + return { + id: n.id, + type: "vc", + position: { x: n.position?.x ?? 0, y: n.position?.y ?? 0 }, + data, + }; + }); const edges: Edge[] = g.edges.map((e) => ({ id: `${e.from_node}:${e.from_port}->${e.to_node}:${e.to_port}`, source: e.from_node, @@ -47,6 +54,8 @@ export function toGraph( }; if (n.data.name !== undefined) node.name = n.data.name; if (n.data.connection_id != null) node.connection_id = n.data.connection_id; + if (n.data.pinnedInput !== undefined && n.data.pinnedInput !== null) node.pinned_input = n.data.pinnedInput; + if (n.data.pinnedOutput !== undefined && n.data.pinnedOutput !== null) node.pinned_output = n.data.pinnedOutput; return node; }); const gEdges: GraphEdge[] = edges.map((e) => ({