feat(editor): round-trip pinned_input and pinned_output through graphAdapter

This commit is contained in:
Claude
2026-08-30 14:25:48 +00:00
parent 16a5225ee3
commit a8d2cbdab0
3 changed files with 88 additions and 7 deletions

View File

@@ -47,6 +47,8 @@ export interface GraphNode {
position: { x: number; y: number };
config: Record<string, unknown>;
connection_id?: string | null;
pinned_input?: unknown;
pinned_output?: unknown;
}
export interface GraphEdge {

View File

@@ -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();
});
});

View File

@@ -6,20 +6,27 @@ export interface VcNodeData extends Record<string, unknown> {
name?: string;
config: Record<string, unknown>;
connection_id?: string | null;
pinnedInput?: unknown;
pinnedOutput?: unknown;
}
export function fromGraph(g: WorkflowGraph): { nodes: Node<VcNodeData>[]; edges: Edge[] } {
const nodes: Node<VcNodeData>[] = g.nodes.map((n) => ({
id: n.id,
type: "vc",
position: { x: n.position?.x ?? 0, y: n.position?.y ?? 0 },
data: {
const nodes: Node<VcNodeData>[] = 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) => ({