From db71b2485a9c7bf255ebe16d06ca13bfc3cb6673 Mon Sep 17 00:00:00 2001 From: Matthias Meister Date: Sat, 11 Apr 2026 08:56:45 +0200 Subject: [PATCH] refactor(canvas): unify node handles with shared wrapper --- .../canvas/__tests__/compare-node.test.tsx | 56 +++++++++++++++++++ .../canvas/__tests__/mixer-node.test.tsx | 43 +++++++++++++- components/canvas/nodes/agent-node.tsx | 11 +++- components/canvas/nodes/agent-output-node.tsx | 9 ++- components/canvas/nodes/ai-image-node.tsx | 11 +++- components/canvas/nodes/ai-video-node.tsx | 11 +++- components/canvas/nodes/asset-node.tsx | 11 +++- components/canvas/nodes/color-adjust-node.tsx | 11 +++- components/canvas/nodes/compare-node.tsx | 15 +++-- components/canvas/nodes/crop-node.tsx | 11 +++- components/canvas/nodes/curves-node.tsx | 11 +++- .../canvas/nodes/detail-adjust-node.tsx | 11 +++- components/canvas/nodes/frame-node.tsx | 11 +++- components/canvas/nodes/group-node.tsx | 11 +++- components/canvas/nodes/image-node.tsx | 11 +++- components/canvas/nodes/light-adjust-node.tsx | 11 +++- components/canvas/nodes/mixer-node.tsx | 15 +++-- components/canvas/nodes/note-node.tsx | 11 +++- components/canvas/nodes/prompt-node.tsx | 10 +++- components/canvas/nodes/render-node.tsx | 11 +++- components/canvas/nodes/text-node.tsx | 10 +++- components/canvas/nodes/video-node.tsx | 11 +++- components/canvas/nodes/video-prompt-node.tsx | 11 +++- 23 files changed, 266 insertions(+), 68 deletions(-) diff --git a/components/canvas/__tests__/compare-node.test.tsx b/components/canvas/__tests__/compare-node.test.tsx index 9e9f14b..aa60d8c 100644 --- a/components/canvas/__tests__/compare-node.test.tsx +++ b/components/canvas/__tests__/compare-node.test.tsx @@ -28,6 +28,31 @@ vi.mock("@xyflow/react", () => ({ useStore: (selector: (state: StoreState) => unknown) => selector(storeState), })); +vi.mock("@/components/canvas/canvas-handle", () => ({ + default: ({ + id, + type, + nodeId, + nodeType, + style, + }: { + id?: string; + type: "source" | "target"; + nodeId: string; + nodeType?: string; + style?: React.CSSProperties; + }) => ( +
+ ), +})); + vi.mock("../nodes/base-node-wrapper", () => ({ default: ({ children }: { children: React.ReactNode }) =>
{children}
, })); @@ -261,4 +286,35 @@ describe("CompareNode render preview inputs", () => { }, }); }); + + it("renders compare handles through CanvasHandle with preserved ids and positions", () => { + const markup = renderCompareNode({ + id: "compare-1", + data: {}, + selected: false, + dragging: false, + zIndex: 0, + isConnectable: true, + type: "compare", + xPos: 0, + yPos: 0, + width: 500, + height: 380, + sourcePosition: undefined, + targetPosition: undefined, + positionAbsoluteX: 0, + positionAbsoluteY: 0, + }); + + expect(markup).toContain('data-canvas-handle="true"'); + expect(markup).toContain('data-node-id="compare-1"'); + expect(markup).toContain('data-node-type="compare"'); + expect(markup).toContain('data-handle-id="left"'); + expect(markup).toContain('data-handle-id="right"'); + expect(markup).toContain('data-handle-id="compare-out"'); + expect(markup).toContain('data-handle-type="target"'); + expect(markup).toContain('data-handle-type="source"'); + expect(markup).toContain('data-top="35%"'); + expect(markup).toContain('data-top="55%"'); + }); }); diff --git a/components/canvas/__tests__/mixer-node.test.tsx b/components/canvas/__tests__/mixer-node.test.tsx index d4c8e75..f0152d1 100644 --- a/components/canvas/__tests__/mixer-node.test.tsx +++ b/components/canvas/__tests__/mixer-node.test.tsx @@ -17,6 +17,31 @@ vi.mock("@xyflow/react", () => ({ Position: { Left: "left", Right: "right" }, })); +vi.mock("@/components/canvas/canvas-handle", () => ({ + default: ({ + id, + type, + nodeId, + nodeType, + style, + }: { + id?: string; + type: "source" | "target"; + nodeId: string; + nodeType?: string; + style?: React.CSSProperties; + }) => ( +
+ ), +})); + vi.mock("@/components/canvas/canvas-sync-context", () => ({ useCanvasSync: () => ({ queueNodeDataUpdate: mocks.queueNodeDataUpdate, @@ -222,8 +247,20 @@ describe("MixerNode", () => { it("renders expected mixer handles", async () => { await renderNode(); - expect(container?.querySelector('[data-handle-id="base"][data-handle-type="target"]')).toBeTruthy(); - expect(container?.querySelector('[data-handle-id="overlay"][data-handle-type="target"]')).toBeTruthy(); - expect(container?.querySelector('[data-handle-id="mixer-out"][data-handle-type="source"]')).toBeTruthy(); + expect( + container?.querySelector( + '[data-canvas-handle="true"][data-node-id="mixer-1"][data-node-type="mixer"][data-handle-id="base"][data-handle-type="target"][data-top="35%"]', + ), + ).toBeTruthy(); + expect( + container?.querySelector( + '[data-canvas-handle="true"][data-node-id="mixer-1"][data-node-type="mixer"][data-handle-id="overlay"][data-handle-type="target"][data-top="58%"]', + ), + ).toBeTruthy(); + expect( + container?.querySelector( + '[data-canvas-handle="true"][data-node-id="mixer-1"][data-node-type="mixer"][data-handle-id="mixer-out"][data-handle-type="source"]', + ), + ).toBeTruthy(); }); }); diff --git a/components/canvas/nodes/agent-node.tsx b/components/canvas/nodes/agent-node.tsx index ed0228f..bfe8e41 100644 --- a/components/canvas/nodes/agent-node.tsx +++ b/components/canvas/nodes/agent-node.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from "react"; import { Bot } from "lucide-react"; -import { Handle, Position, type Node, type NodeProps } from "@xyflow/react"; +import { Position, type Node, type NodeProps } from "@xyflow/react"; import { useAction } from "convex/react"; import type { FunctionReference } from "convex/server"; import { useTranslations } from "next-intl"; @@ -33,6 +33,7 @@ import { SelectValue, } from "@/components/ui/select"; import BaseNodeWrapper from "./base-node-wrapper"; +import CanvasHandle from "@/components/canvas/canvas-handle"; type AgentNodeData = { templateId?: string; @@ -466,13 +467,17 @@ export default function AgentNode({ id, data, selected }: NodeProps - - ) { +export default function AgentOutputNode({ id, data, selected }: NodeProps) { const t = useTranslations("agentOutputNode"); const nodeData = data as AgentOutputNodeData; const isSkeleton = nodeData.isSkeleton === true; @@ -240,7 +241,9 @@ export default function AgentOutputNode({ data, selected }: NodeProps - - )} - - - - ) : null} - -
- - - - - {error}

: null}
- - - - - - - ) selected={selected} className="min-w-[200px] min-h-[150px] p-3 border-dashed" > - ) )} - - - - - - - - ) { return ( - ) { )} - - - - - ) { ]} className="relative" > - ) { )} - - ) : null} - - -