refactor(canvas): unify node handles with shared wrapper
This commit is contained in:
@@ -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;
|
||||
}) => (
|
||||
<div
|
||||
data-canvas-handle="true"
|
||||
data-handle-id={id ?? ""}
|
||||
data-handle-type={type}
|
||||
data-node-id={nodeId}
|
||||
data-node-type={nodeType ?? ""}
|
||||
data-top={typeof style?.top === "string" ? style.top : ""}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("../nodes/base-node-wrapper", () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
@@ -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%"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}) => (
|
||||
<div
|
||||
data-canvas-handle="true"
|
||||
data-handle-id={id ?? ""}
|
||||
data-handle-type={type}
|
||||
data-node-id={nodeId}
|
||||
data-node-type={nodeType ?? ""}
|
||||
data-top={typeof style?.top === "string" ? style.top : ""}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<AgentNodeTyp
|
||||
statusMessage={nodeData._statusMessage}
|
||||
className="min-w-[300px] border-amber-500/30"
|
||||
>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="agent"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id="agent-in"
|
||||
className="!h-3 !w-3 !bg-amber-500 !border-2 !border-background"
|
||||
/>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="agent"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="!h-3 !w-3 !bg-amber-500 !border-2 !border-background"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import BaseNodeWrapper from "./base-node-wrapper";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
type AgentOutputNodeData = {
|
||||
isSkeleton?: boolean;
|
||||
@@ -186,7 +187,7 @@ function partitionSections(
|
||||
};
|
||||
}
|
||||
|
||||
export default function AgentOutputNode({ data, selected }: NodeProps<AgentOutputNodeType>) {
|
||||
export default function AgentOutputNode({ id, data, selected }: NodeProps<AgentOutputNodeType>) {
|
||||
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<AgentOutpu
|
||||
statusMessage={nodeData._statusMessage}
|
||||
className={`min-w-[300px] border-amber-500/30 ${isSkeleton ? "opacity-80" : ""}`}
|
||||
>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="agent-output"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id="agent-output-in"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Handle, Position, useReactFlow, type NodeProps, type Node } from "@xyflow/react";
|
||||
import { Position, useReactFlow, type NodeProps, type Node } from "@xyflow/react";
|
||||
import { useAction } from "convex/react";
|
||||
import { api } from "@/convex/_generated/api";
|
||||
import type { Id } from "@/convex/_generated/dataModel";
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
type AiImageNodeData = {
|
||||
storageId?: string;
|
||||
@@ -194,7 +195,9 @@ export default function AiImageNode({
|
||||
]}
|
||||
className="flex h-full w-full min-h-0 min-w-0 flex-col"
|
||||
>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="ai-image"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id="prompt-in"
|
||||
@@ -331,7 +334,9 @@ export default function AiImageNode({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="ai-image"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="image-out"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useAction } from "convex/react";
|
||||
import type { FunctionReference } from "convex/server";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { AlertCircle, Download, Loader2, RefreshCw, Video } from "lucide-react";
|
||||
import { Handle, Position, useReactFlow, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { Position, useReactFlow, type Node, type NodeProps } from "@xyflow/react";
|
||||
|
||||
import { api } from "@/convex/_generated/api";
|
||||
import type { Id } from "@/convex/_generated/dataModel";
|
||||
@@ -14,6 +14,7 @@ import { classifyError } from "@/lib/ai-errors";
|
||||
import { getVideoModel, type VideoModelDurationSeconds } from "@/lib/ai-video-models";
|
||||
import { toast } from "@/lib/toast";
|
||||
import BaseNodeWrapper from "./base-node-wrapper";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
type AiVideoNodeData = {
|
||||
prompt?: string;
|
||||
@@ -160,7 +161,9 @@ export default function AiVideoNode({ id, data, selected }: NodeProps<AiVideoNod
|
||||
statusMessage={nodeData._statusMessage}
|
||||
className="flex h-full w-full min-h-0 min-w-0 flex-col"
|
||||
>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="ai-video"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id="video-in"
|
||||
@@ -240,7 +243,9 @@ export default function AiVideoNode({ id, data, selected }: NodeProps<AiVideoNod
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="ai-video"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="video-out"
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useState,
|
||||
type MouseEvent,
|
||||
} from "react";
|
||||
import { Handle, Position, useStore, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { Position, useStore, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { ExternalLink, ImageIcon } from "lucide-react";
|
||||
import BaseNodeWrapper from "./base-node-wrapper";
|
||||
import {
|
||||
@@ -21,6 +21,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { resolveMediaAspectRatio } from "@/lib/canvas-utils";
|
||||
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
type AssetNodeData = {
|
||||
assetId?: number;
|
||||
@@ -152,7 +153,9 @@ export default function AssetNode({ id, data, selected, width, height }: NodePro
|
||||
status={data._status}
|
||||
statusMessage={data._statusMessage}
|
||||
>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="asset"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="h-3! w-3! border-2! border-background! bg-primary!"
|
||||
@@ -273,7 +276,9 @@ export default function AssetNode({ id, data, selected, width, height }: NodePro
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="asset"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="h-3! w-3! border-2! border-background! bg-primary!"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Palette } from "lucide-react";
|
||||
|
||||
@@ -29,6 +29,7 @@ import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
||||
import { COLOR_PRESETS } from "@/lib/image-pipeline/presets";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { toast } from "@/lib/toast";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
type ColorAdjustNodeData = ColorAdjustData & {
|
||||
_status?: string;
|
||||
@@ -191,7 +192,9 @@ export default function ColorAdjustNode({ id, data, selected, width }: NodeProps
|
||||
statusMessage={data._statusMessage}
|
||||
className="min-w-[300px] border-cyan-500/30"
|
||||
>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="color-adjust"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="!h-3 !w-3 !border-2 !border-background !bg-cyan-500"
|
||||
@@ -268,7 +271,9 @@ export default function ColorAdjustNode({ id, data, selected, width }: NodeProps
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="color-adjust"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="!h-3 !w-3 !border-2 !border-background !bg-cyan-500"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { Handle, Position, type NodeProps } from "@xyflow/react";
|
||||
import { Position, type NodeProps } from "@xyflow/react";
|
||||
import { ImageIcon } from "lucide-react";
|
||||
import BaseNodeWrapper from "./base-node-wrapper";
|
||||
import CompareSurface from "./compare-surface";
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
resolveMixerPreviewFromGraph,
|
||||
type MixerPreviewState,
|
||||
} from "@/lib/canvas-mixer-preview";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
interface CompareNodeData {
|
||||
leftUrl?: string;
|
||||
@@ -242,21 +243,27 @@ export default function CompareNode({ id, data, selected, width }: NodeProps) {
|
||||
|
||||
return (
|
||||
<BaseNodeWrapper nodeType="compare" selected={selected} className="p-0">
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="compare"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id="left"
|
||||
style={{ top: "35%" }}
|
||||
className="!h-3 !w-3 !border-2 !border-background !bg-blue-500"
|
||||
/>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="compare"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id="right"
|
||||
style={{ top: "55%" }}
|
||||
className="!h-3 !w-3 !border-2 !border-background !bg-emerald-500"
|
||||
/>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="compare"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="compare-out"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useRef, type PointerEvent as ReactPointerEvent } from "react";
|
||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { Crop } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
||||
import type { Id } from "@/convex/_generated/dataModel";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
type CropNodeViewData = CropNodeData & {
|
||||
_status?: string;
|
||||
@@ -400,7 +401,9 @@ export default function CropNode({ id, data, selected, width }: NodeProps<CropNo
|
||||
statusMessage={data._statusMessage}
|
||||
className="min-w-[320px] border-violet-500/30"
|
||||
>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="crop"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="!h-3 !w-3 !border-2 !border-background !bg-violet-500"
|
||||
@@ -735,7 +738,9 @@ export default function CropNode({ id, data, selected, width }: NodeProps<CropNo
|
||||
{error ? <p className="text-[11px] text-destructive">{error}</p> : null}
|
||||
</div>
|
||||
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="crop"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="!h-3 !w-3 !border-2 !border-background !bg-violet-500"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { TrendingUp } from "lucide-react";
|
||||
|
||||
@@ -29,6 +29,7 @@ import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
||||
import { CURVE_PRESETS } from "@/lib/image-pipeline/presets";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { toast } from "@/lib/toast";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
type CurvesNodeData = CurvesData & {
|
||||
_status?: string;
|
||||
@@ -163,7 +164,9 @@ export default function CurvesNode({ id, data, selected, width }: NodeProps<Curv
|
||||
statusMessage={data._statusMessage}
|
||||
className="min-w-[300px] border-emerald-500/30"
|
||||
>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="curves"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="!h-3 !w-3 !border-2 !border-background !bg-emerald-500"
|
||||
@@ -237,7 +240,9 @@ export default function CurvesNode({ id, data, selected, width }: NodeProps<Curv
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="curves"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="!h-3 !w-3 !border-2 !border-background !bg-emerald-500"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Focus } from "lucide-react";
|
||||
|
||||
@@ -29,6 +29,7 @@ import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
||||
import { DETAIL_PRESETS } from "@/lib/image-pipeline/presets";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { toast } from "@/lib/toast";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
type DetailAdjustNodeData = DetailAdjustData & {
|
||||
_status?: string;
|
||||
@@ -202,7 +203,9 @@ export default function DetailAdjustNode({ id, data, selected, width }: NodeProp
|
||||
statusMessage={data._statusMessage}
|
||||
className="min-w-[300px] border-indigo-500/30"
|
||||
>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="detail-adjust"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="!h-3 !w-3 !border-2 !border-background !bg-indigo-500"
|
||||
@@ -286,7 +289,9 @@ export default function DetailAdjustNode({ id, data, selected, width }: NodeProp
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="detail-adjust"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="!h-3 !w-3 !border-2 !border-background !bg-indigo-500"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { Handle, Position, type NodeProps } from "@xyflow/react";
|
||||
import { Position, type NodeProps } from "@xyflow/react";
|
||||
import { useAction } from "convex/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Download, Loader2 } from "lucide-react";
|
||||
@@ -11,6 +11,7 @@ import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
||||
import BaseNodeWrapper from "./base-node-wrapper";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
interface FrameNodeData {
|
||||
label?: string;
|
||||
@@ -125,13 +126,17 @@ export default function FrameNode({ id, data, selected, width, height }: NodePro
|
||||
|
||||
<div className="nodrag h-full w-full" />
|
||||
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="frame"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id="frame-in"
|
||||
className="!h-3 !w-3 !border-2 !border-background !bg-orange-500"
|
||||
/>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="frame"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="frame-out"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { Handle, Position, type NodeProps, type Node } from "@xyflow/react";
|
||||
import { Position, type NodeProps, type Node } from "@xyflow/react";
|
||||
import type { Id } from "@/convex/_generated/dataModel";
|
||||
import BaseNodeWrapper from "./base-node-wrapper";
|
||||
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
type GroupNodeData = {
|
||||
label?: string;
|
||||
@@ -47,7 +48,9 @@ export default function GroupNode({ id, data, selected }: NodeProps<GroupNode>)
|
||||
selected={selected}
|
||||
className="min-w-[200px] min-h-[150px] p-3 border-dashed"
|
||||
>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="group"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="!h-3 !w-3 !bg-muted-foreground !border-2 !border-background"
|
||||
@@ -71,7 +74,9 @@ export default function GroupNode({ id, data, selected }: NodeProps<GroupNode>)
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="group"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="!h-3 !w-3 !bg-muted-foreground !border-2 !border-background"
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
type ChangeEvent,
|
||||
type DragEvent,
|
||||
} from "react";
|
||||
import { Handle, Position, type NodeProps, type Node } from "@xyflow/react";
|
||||
import { Position, type NodeProps, type Node } from "@xyflow/react";
|
||||
import { Maximize2, X } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { api } from "@/convex/_generated/api";
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
getImageDimensions,
|
||||
} from "@/components/canvas/canvas-media-utils";
|
||||
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
const ALLOWED_IMAGE_TYPES = new Set([
|
||||
"image/png",
|
||||
@@ -508,7 +509,9 @@ export default function ImageNode({
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="image"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="h-3! w-3! bg-primary! border-2! border-background!"
|
||||
@@ -609,7 +612,9 @@ export default function ImageNode({
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="image"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="h-3! w-3! bg-primary! border-2! border-background!"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Sun } from "lucide-react";
|
||||
|
||||
@@ -29,6 +29,7 @@ import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
||||
import { LIGHT_PRESETS } from "@/lib/image-pipeline/presets";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { toast } from "@/lib/toast";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
type LightAdjustNodeData = LightAdjustData & {
|
||||
_status?: string;
|
||||
@@ -213,7 +214,9 @@ export default function LightAdjustNode({ id, data, selected, width }: NodeProps
|
||||
statusMessage={data._statusMessage}
|
||||
className="min-w-[300px] border-amber-500/30"
|
||||
>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="light-adjust"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="!h-3 !w-3 !border-2 !border-background !bg-amber-500"
|
||||
@@ -292,7 +295,9 @@ export default function LightAdjustNode({ id, data, selected, width }: NodeProps
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="light-adjust"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="!h-3 !w-3 !border-2 !border-background !bg-amber-500"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState, type ChangeEvent, type FormEvent } from "react";
|
||||
import { Handle, Position, type NodeProps } from "@xyflow/react";
|
||||
import { Position, type NodeProps } from "@xyflow/react";
|
||||
|
||||
import BaseNodeWrapper from "./base-node-wrapper";
|
||||
import { useCanvasGraph } from "@/components/canvas/canvas-graph-context";
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type MixerBlendMode,
|
||||
} from "@/lib/canvas-mixer-preview";
|
||||
import type { Id } from "@/convex/_generated/dataModel";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
const BLEND_MODE_OPTIONS: MixerBlendMode[] = ["normal", "multiply", "screen", "overlay"];
|
||||
|
||||
@@ -56,21 +57,27 @@ export default function MixerNode({ id, data, selected }: NodeProps) {
|
||||
|
||||
return (
|
||||
<BaseNodeWrapper nodeType="mixer" selected={selected} className="p-0">
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="mixer"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id="base"
|
||||
style={{ top: "35%" }}
|
||||
className="!h-3 !w-3 !border-2 !border-background !bg-sky-500"
|
||||
/>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="mixer"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id="overlay"
|
||||
style={{ top: "58%" }}
|
||||
className="!h-3 !w-3 !border-2 !border-background !bg-pink-500"
|
||||
/>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="mixer"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="mixer-out"
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { Handle, Position, type NodeProps, type Node } from "@xyflow/react";
|
||||
import { Position, type NodeProps, type Node } from "@xyflow/react";
|
||||
import type { Id } from "@/convex/_generated/dataModel";
|
||||
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
||||
import BaseNodeWrapper from "./base-node-wrapper";
|
||||
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
type NoteNodeData = {
|
||||
content?: string;
|
||||
@@ -53,7 +54,9 @@ export default function NoteNode({ id, data, selected }: NodeProps<NoteNode>) {
|
||||
|
||||
return (
|
||||
<BaseNodeWrapper nodeType="note" selected={selected} className="p-3">
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="note"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="!h-3 !w-3 !bg-primary !border-2 !border-background"
|
||||
@@ -85,7 +88,9 @@ export default function NoteNode({ id, data, selected }: NodeProps<NoteNode>) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="note"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="!h-3 !w-3 !bg-primary !border-2 !border-background"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Handle,
|
||||
Position,
|
||||
useReactFlow,
|
||||
useStore,
|
||||
@@ -45,6 +44,7 @@ import { useRouter } from "next/navigation";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { classifyError } from "@/lib/ai-errors";
|
||||
import { normalizePublicTier } from "@/lib/tier-credits";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
type PromptNodeData = {
|
||||
prompt?: string;
|
||||
@@ -353,7 +353,9 @@ export default function PromptNode({
|
||||
statusMessage={nodeData._statusMessage}
|
||||
className="min-w-[240px] border-violet-500/30"
|
||||
>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="prompt"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id="image-in"
|
||||
@@ -489,7 +491,9 @@ export default function PromptNode({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="prompt"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="prompt-out"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { AlertCircle, ArrowDown, CheckCircle2, CloudUpload, Loader2, Maximize2, X } from "lucide-react";
|
||||
import { useMutation } from "convex/react";
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
||||
import type { Id } from "@/convex/_generated/dataModel";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
type RenderResolutionOption = "original" | "2x" | "custom";
|
||||
type RenderFormatOption = "png" | "jpeg" | "webp";
|
||||
@@ -978,7 +979,9 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
|
||||
]}
|
||||
className="flex h-full min-w-[280px] flex-col overflow-hidden border-sky-500/30"
|
||||
>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="render"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="!h-3 !w-3 !border-2 !border-background !bg-sky-500"
|
||||
@@ -1273,7 +1276,9 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="render"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="!h-3 !w-3 !border-2 !border-background !bg-sky-500"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import {
|
||||
Handle,
|
||||
Position,
|
||||
useReactFlow,
|
||||
type NodeProps,
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
type TextNodeData = {
|
||||
content?: string;
|
||||
@@ -155,7 +155,9 @@ export default function TextNode({ id, data, selected }: NodeProps<TextNode>) {
|
||||
]}
|
||||
className="relative"
|
||||
>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="text"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="!h-3 !w-3 !bg-primary !border-2 !border-background"
|
||||
@@ -190,7 +192,9 @@ export default function TextNode({ id, data, selected }: NodeProps<TextNode>) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="text"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="!h-3 !w-3 !bg-primary !border-2 !border-background"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Handle, Position, useStore, type NodeProps } from "@xyflow/react";
|
||||
import { Position, useStore, type NodeProps } from "@xyflow/react";
|
||||
import { useAction } from "convex/react";
|
||||
import { Play } from "lucide-react";
|
||||
import BaseNodeWrapper from "./base-node-wrapper";
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { api } from "@/convex/_generated/api";
|
||||
import type { Id } from "@/convex/_generated/dataModel";
|
||||
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
type VideoNodeData = {
|
||||
canvasId?: string;
|
||||
@@ -150,7 +151,9 @@ export default function VideoNode({
|
||||
|
||||
return (
|
||||
<BaseNodeWrapper nodeType="video" selected={selected}>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="video"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="h-3! w-3! border-2! border-background! bg-primary!"
|
||||
@@ -245,7 +248,9 @@ export default function VideoNode({
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="video"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="h-3! w-3! border-2! border-background! bg-primary!"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Handle, Position, useReactFlow, useStore, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { Position, useReactFlow, useStore, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { useAction } from "convex/react";
|
||||
import type { FunctionReference } from "convex/server";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import CanvasHandle from "@/components/canvas/canvas-handle";
|
||||
|
||||
type VideoPromptNodeData = {
|
||||
prompt?: string;
|
||||
@@ -300,7 +301,9 @@ export default function VideoPromptNode({
|
||||
statusMessage={nodeData._statusMessage}
|
||||
className="min-w-[260px] border-violet-500/30"
|
||||
>
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="video-prompt"
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id="video-prompt-in"
|
||||
@@ -407,7 +410,9 @@ export default function VideoPromptNode({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<Handle
|
||||
<CanvasHandle
|
||||
nodeId={id}
|
||||
nodeType="video-prompt"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="video-prompt-out"
|
||||
|
||||
Reference in New Issue
Block a user