refactor(canvas): extract drop handling hook
This commit is contained in:
201
components/canvas/__tests__/use-canvas-drop.test.tsx
Normal file
201
components/canvas/__tests__/use-canvas-drop.test.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import React, { act, useEffect } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { Id } from "@/convex/_generated/dataModel";
|
||||
import { CANVAS_NODE_DND_MIME } from "@/lib/canvas-connection-policy";
|
||||
import { NODE_DEFAULTS } from "@/lib/canvas-utils";
|
||||
import { useCanvasDrop } from "@/components/canvas/use-canvas-drop";
|
||||
|
||||
vi.mock("@/lib/toast", () => ({
|
||||
toast: {
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/components/canvas/canvas-media-utils", () => ({
|
||||
getImageDimensions: vi.fn(async () => ({ width: 1600, height: 900 })),
|
||||
}));
|
||||
|
||||
const latestHandlersRef: {
|
||||
current: ReturnType<typeof useCanvasDrop> | null;
|
||||
} = { current: null };
|
||||
|
||||
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const asCanvasId = (id: string): Id<"canvases"> => id as Id<"canvases">;
|
||||
|
||||
type HookHarnessProps = {
|
||||
isSyncOnline?: boolean;
|
||||
generateUploadUrl?: ReturnType<typeof vi.fn>;
|
||||
runCreateNodeOnlineOnly?: ReturnType<typeof vi.fn>;
|
||||
notifyOfflineUnsupported?: ReturnType<typeof vi.fn>;
|
||||
syncPendingMoveForClientRequest?: ReturnType<typeof vi.fn>;
|
||||
screenToFlowPosition?: (position: { x: number; y: number }) => { x: number; y: number };
|
||||
};
|
||||
|
||||
function HookHarness({
|
||||
isSyncOnline = true,
|
||||
generateUploadUrl = vi.fn(async () => "https://upload.test"),
|
||||
runCreateNodeOnlineOnly = vi.fn(async () => "node-1"),
|
||||
notifyOfflineUnsupported = vi.fn(),
|
||||
syncPendingMoveForClientRequest = vi.fn(async () => undefined),
|
||||
screenToFlowPosition = (position) => position,
|
||||
}: HookHarnessProps) {
|
||||
const handlers = useCanvasDrop({
|
||||
canvasId: asCanvasId("canvas-1"),
|
||||
isSyncOnline,
|
||||
t: ((key: string) => key) as (key: string) => string,
|
||||
screenToFlowPosition,
|
||||
generateUploadUrl,
|
||||
runCreateNodeOnlineOnly,
|
||||
notifyOfflineUnsupported,
|
||||
syncPendingMoveForClientRequest,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
latestHandlersRef.current = handlers;
|
||||
}, [handlers]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
describe("useCanvasDrop", () => {
|
||||
let container: HTMLDivElement | null = null;
|
||||
let root: Root | null = null;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => ({
|
||||
ok: true,
|
||||
json: async () => ({ storageId: "storage-1" }),
|
||||
})));
|
||||
vi.stubGlobal("crypto", {
|
||||
randomUUID: vi.fn(() => "req-1"),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
latestHandlersRef.current = null;
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
if (root) {
|
||||
await act(async () => {
|
||||
root?.unmount();
|
||||
});
|
||||
}
|
||||
container?.remove();
|
||||
root = null;
|
||||
container = null;
|
||||
});
|
||||
|
||||
it("creates a node from a raw sidebar node type drop", async () => {
|
||||
const runCreateNodeOnlineOnly = vi.fn(async () => "node-1");
|
||||
const syncPendingMoveForClientRequest = vi.fn(async () => undefined);
|
||||
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<HookHarness
|
||||
runCreateNodeOnlineOnly={runCreateNodeOnlineOnly}
|
||||
syncPendingMoveForClientRequest={syncPendingMoveForClientRequest}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await latestHandlersRef.current?.onDrop({
|
||||
preventDefault: vi.fn(),
|
||||
clientX: 120,
|
||||
clientY: 340,
|
||||
dataTransfer: {
|
||||
getData: vi.fn((type: string) =>
|
||||
type === CANVAS_NODE_DND_MIME ? "image" : "",
|
||||
),
|
||||
files: [],
|
||||
},
|
||||
} as unknown as React.DragEvent);
|
||||
});
|
||||
|
||||
expect(runCreateNodeOnlineOnly).toHaveBeenCalledWith({
|
||||
canvasId: "canvas-1",
|
||||
type: "image",
|
||||
positionX: 120,
|
||||
positionY: 340,
|
||||
width: NODE_DEFAULTS.image.width,
|
||||
height: NODE_DEFAULTS.image.height,
|
||||
data: {
|
||||
...NODE_DEFAULTS.image.data,
|
||||
canvasId: "canvas-1",
|
||||
},
|
||||
clientRequestId: "req-1",
|
||||
});
|
||||
expect(syncPendingMoveForClientRequest).toHaveBeenCalledWith("req-1", "node-1");
|
||||
});
|
||||
|
||||
it("creates an image node from a dropped image file", async () => {
|
||||
const generateUploadUrl = vi.fn(async () => "https://upload.test");
|
||||
const runCreateNodeOnlineOnly = vi.fn(async () => "node-image");
|
||||
const syncPendingMoveForClientRequest = vi.fn(async () => undefined);
|
||||
const file = new File(["image-bytes"], "photo.png", { type: "image/png" });
|
||||
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<HookHarness
|
||||
generateUploadUrl={generateUploadUrl}
|
||||
runCreateNodeOnlineOnly={runCreateNodeOnlineOnly}
|
||||
syncPendingMoveForClientRequest={syncPendingMoveForClientRequest}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await latestHandlersRef.current?.onDrop({
|
||||
preventDefault: vi.fn(),
|
||||
clientX: 240,
|
||||
clientY: 180,
|
||||
dataTransfer: {
|
||||
getData: vi.fn(() => ""),
|
||||
files: [file],
|
||||
},
|
||||
} as unknown as React.DragEvent);
|
||||
});
|
||||
|
||||
expect(generateUploadUrl).toHaveBeenCalledTimes(1);
|
||||
expect(fetch).toHaveBeenCalledWith("https://upload.test", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "image/png" },
|
||||
body: file,
|
||||
});
|
||||
expect(runCreateNodeOnlineOnly).toHaveBeenCalledWith({
|
||||
canvasId: "canvas-1",
|
||||
type: "image",
|
||||
positionX: 240,
|
||||
positionY: 180,
|
||||
width: NODE_DEFAULTS.image.width,
|
||||
height: NODE_DEFAULTS.image.height,
|
||||
data: {
|
||||
storageId: "storage-1",
|
||||
filename: "photo.png",
|
||||
mimeType: "image/png",
|
||||
width: 1600,
|
||||
height: 900,
|
||||
canvasId: "canvas-1",
|
||||
},
|
||||
clientRequestId: "req-1",
|
||||
});
|
||||
expect(syncPendingMoveForClientRequest).toHaveBeenCalledWith(
|
||||
"req-1",
|
||||
"node-image",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -24,9 +24,7 @@ import {
|
||||
} from "@xyflow/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import { toast } from "@/lib/toast";
|
||||
import {
|
||||
CANVAS_NODE_DND_MIME,
|
||||
type CanvasConnectionValidationReason,
|
||||
} from "@/lib/canvas-connection-policy";
|
||||
import { showCanvasConnectionRejectedToast } from "@/lib/toast-messages";
|
||||
@@ -35,12 +33,9 @@ import { api } from "@/convex/_generated/api";
|
||||
import type { Id } from "@/convex/_generated/dataModel";
|
||||
import {
|
||||
isAdjustmentPresetNodeType,
|
||||
isCanvasNodeType,
|
||||
type CanvasNodeType,
|
||||
} from "@/lib/canvas-node-types";
|
||||
|
||||
import { nodeTypes } from "./node-types";
|
||||
import { NODE_DEFAULTS } from "@/lib/canvas-utils";
|
||||
import CanvasToolbar, {
|
||||
type CanvasNavTool,
|
||||
} from "@/components/canvas/canvas-toolbar";
|
||||
@@ -68,9 +63,9 @@ import {
|
||||
} from "./canvas-helpers";
|
||||
import { useGenerationFailureWarnings } from "./canvas-generation-failures";
|
||||
import { useCanvasDeleteHandlers } from "./canvas-delete-handlers";
|
||||
import { getImageDimensions } from "./canvas-media-utils";
|
||||
import { useCanvasNodeInteractions } from "./use-canvas-node-interactions";
|
||||
import { useCanvasConnections } from "./use-canvas-connections";
|
||||
import { useCanvasDrop } from "./use-canvas-drop";
|
||||
import { useCanvasScissors } from "./canvas-scissors";
|
||||
import { CanvasSyncProvider } from "./canvas-sync-context";
|
||||
import { useCanvasData } from "./use-canvas-data";
|
||||
@@ -371,158 +366,16 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
console.error("[ReactFlow error]", { canvasId, id, error });
|
||||
}, [canvasId]);
|
||||
|
||||
// ─── Future hook seam: drop flows ─────────────────────────────
|
||||
const onDragOver = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
const hasFiles = event.dataTransfer.types.includes("Files");
|
||||
event.dataTransfer.dropEffect = hasFiles ? "copy" : "move";
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback(
|
||||
async (event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const rawData = event.dataTransfer.getData(
|
||||
CANVAS_NODE_DND_MIME,
|
||||
);
|
||||
if (!rawData) {
|
||||
const hasFiles = event.dataTransfer.files && event.dataTransfer.files.length > 0;
|
||||
if (hasFiles) {
|
||||
if (!isSyncOnline) {
|
||||
notifyOfflineUnsupported("Upload per Drag-and-drop");
|
||||
return;
|
||||
}
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (file.type.startsWith("image/")) {
|
||||
try {
|
||||
let dimensions: { width: number; height: number } | undefined;
|
||||
try {
|
||||
dimensions = await getImageDimensions(file);
|
||||
} catch {
|
||||
dimensions = undefined;
|
||||
}
|
||||
|
||||
const uploadUrl = await generateUploadUrl();
|
||||
const result = await fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": file.type },
|
||||
body: file,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error("Upload failed");
|
||||
}
|
||||
|
||||
const { storageId } = (await result.json()) as { storageId: string };
|
||||
|
||||
const position = screenToFlowPosition({ x: event.clientX, y: event.clientY });
|
||||
const clientRequestId = crypto.randomUUID();
|
||||
|
||||
void runCreateNodeOnlineOnly({
|
||||
canvasId,
|
||||
type: "image",
|
||||
positionX: position.x,
|
||||
positionY: position.y,
|
||||
width: NODE_DEFAULTS.image.width,
|
||||
height: NODE_DEFAULTS.image.height,
|
||||
data: {
|
||||
storageId,
|
||||
filename: file.name,
|
||||
mimeType: file.type,
|
||||
...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),
|
||||
canvasId,
|
||||
},
|
||||
clientRequestId,
|
||||
}).then((realId) => {
|
||||
void syncPendingMoveForClientRequest(
|
||||
clientRequestId,
|
||||
realId,
|
||||
).catch((error: unknown) => {
|
||||
console.error(
|
||||
"[Canvas] drop createNode syncPendingMove failed",
|
||||
error,
|
||||
);
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to upload dropped file:", err);
|
||||
toast.error(t('canvas.uploadFailed'), err instanceof Error ? err.message : undefined);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Support both plain type string (sidebar) and JSON payload (browser panels)
|
||||
let nodeType: CanvasNodeType | null = null;
|
||||
let payloadData: Record<string, unknown> | undefined;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(rawData);
|
||||
if (
|
||||
typeof parsed === "object" &&
|
||||
parsed !== null &&
|
||||
typeof (parsed as { type?: unknown }).type === "string" &&
|
||||
isCanvasNodeType((parsed as { type: string }).type)
|
||||
) {
|
||||
nodeType = (parsed as { type: CanvasNodeType }).type;
|
||||
payloadData = parsed.data;
|
||||
}
|
||||
} catch {
|
||||
if (isCanvasNodeType(rawData)) {
|
||||
nodeType = rawData;
|
||||
}
|
||||
}
|
||||
|
||||
if (!nodeType) {
|
||||
toast.warning("Node-Typ nicht verfuegbar", "Unbekannter Node konnte nicht erstellt werden.");
|
||||
return;
|
||||
}
|
||||
|
||||
const position = screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
|
||||
const defaults = NODE_DEFAULTS[nodeType] ?? {
|
||||
width: 200,
|
||||
height: 100,
|
||||
data: {},
|
||||
};
|
||||
|
||||
const clientRequestId = crypto.randomUUID();
|
||||
void runCreateNodeOnlineOnly({
|
||||
canvasId,
|
||||
type: nodeType,
|
||||
positionX: position.x,
|
||||
positionY: position.y,
|
||||
width: defaults.width,
|
||||
height: defaults.height,
|
||||
data: { ...defaults.data, ...payloadData, canvasId },
|
||||
clientRequestId,
|
||||
}).then((realId) => {
|
||||
void syncPendingMoveForClientRequest(clientRequestId, realId).catch(
|
||||
(error: unknown) => {
|
||||
console.error(
|
||||
"[Canvas] createNode syncPendingMove failed",
|
||||
error,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
[
|
||||
screenToFlowPosition,
|
||||
t,
|
||||
canvasId,
|
||||
generateUploadUrl,
|
||||
isSyncOnline,
|
||||
runCreateNodeOnlineOnly,
|
||||
notifyOfflineUnsupported,
|
||||
syncPendingMoveForClientRequest,
|
||||
],
|
||||
);
|
||||
const { onDragOver, onDrop } = useCanvasDrop({
|
||||
canvasId,
|
||||
isSyncOnline,
|
||||
t,
|
||||
screenToFlowPosition,
|
||||
generateUploadUrl,
|
||||
runCreateNodeOnlineOnly,
|
||||
notifyOfflineUnsupported,
|
||||
syncPendingMoveForClientRequest,
|
||||
});
|
||||
|
||||
const canvasSyncContextValue = useMemo(
|
||||
() => ({
|
||||
|
||||
212
components/canvas/use-canvas-drop.ts
Normal file
212
components/canvas/use-canvas-drop.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import type { Id } from "@/convex/_generated/dataModel";
|
||||
import {
|
||||
CANVAS_NODE_DND_MIME,
|
||||
} from "@/lib/canvas-connection-policy";
|
||||
import { NODE_DEFAULTS } from "@/lib/canvas-utils";
|
||||
import {
|
||||
isCanvasNodeType,
|
||||
type CanvasNodeType,
|
||||
} from "@/lib/canvas-node-types";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
import { getImageDimensions } from "./canvas-media-utils";
|
||||
|
||||
type UseCanvasDropParams = {
|
||||
canvasId: Id<"canvases">;
|
||||
isSyncOnline: boolean;
|
||||
t: (key: string) => string;
|
||||
screenToFlowPosition: (position: { x: number; y: number }) => { x: number; y: number };
|
||||
generateUploadUrl: () => Promise<string>;
|
||||
runCreateNodeOnlineOnly: (args: {
|
||||
canvasId: Id<"canvases">;
|
||||
type: CanvasNodeType;
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
data: Record<string, unknown>;
|
||||
clientRequestId?: string;
|
||||
}) => Promise<Id<"nodes">>;
|
||||
notifyOfflineUnsupported: (featureLabel: string) => void;
|
||||
syncPendingMoveForClientRequest: (
|
||||
clientRequestId: string,
|
||||
realId?: Id<"nodes">,
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
function parseCanvasDropPayload(rawData: string): {
|
||||
nodeType: CanvasNodeType;
|
||||
payloadData?: Record<string, unknown>;
|
||||
} | null {
|
||||
try {
|
||||
const parsed = JSON.parse(rawData);
|
||||
if (
|
||||
typeof parsed === "object" &&
|
||||
parsed !== null &&
|
||||
typeof (parsed as { type?: unknown }).type === "string" &&
|
||||
isCanvasNodeType((parsed as { type: string }).type)
|
||||
) {
|
||||
return {
|
||||
nodeType: (parsed as { type: CanvasNodeType }).type,
|
||||
payloadData: (parsed as { data?: Record<string, unknown> }).data,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
if (isCanvasNodeType(rawData)) {
|
||||
return { nodeType: rawData };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useCanvasDrop({
|
||||
canvasId,
|
||||
isSyncOnline,
|
||||
t,
|
||||
screenToFlowPosition,
|
||||
generateUploadUrl,
|
||||
runCreateNodeOnlineOnly,
|
||||
notifyOfflineUnsupported,
|
||||
syncPendingMoveForClientRequest,
|
||||
}: UseCanvasDropParams) {
|
||||
const onDragOver = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
const hasFiles = event.dataTransfer.types.includes("Files");
|
||||
event.dataTransfer.dropEffect = hasFiles ? "copy" : "move";
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback(
|
||||
async (event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const rawData = event.dataTransfer.getData(CANVAS_NODE_DND_MIME);
|
||||
if (!rawData) {
|
||||
const hasFiles = event.dataTransfer.files && event.dataTransfer.files.length > 0;
|
||||
if (hasFiles) {
|
||||
if (!isSyncOnline) {
|
||||
notifyOfflineUnsupported("Upload per Drag-and-drop");
|
||||
return;
|
||||
}
|
||||
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (file.type.startsWith("image/")) {
|
||||
try {
|
||||
let dimensions: { width: number; height: number } | undefined;
|
||||
try {
|
||||
dimensions = await getImageDimensions(file);
|
||||
} catch {
|
||||
dimensions = undefined;
|
||||
}
|
||||
|
||||
const uploadUrl = await generateUploadUrl();
|
||||
const result = await fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": file.type },
|
||||
body: file,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error("Upload failed");
|
||||
}
|
||||
|
||||
const { storageId } = (await result.json()) as { storageId: string };
|
||||
const position = screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
const clientRequestId = crypto.randomUUID();
|
||||
|
||||
void runCreateNodeOnlineOnly({
|
||||
canvasId,
|
||||
type: "image",
|
||||
positionX: position.x,
|
||||
positionY: position.y,
|
||||
width: NODE_DEFAULTS.image.width,
|
||||
height: NODE_DEFAULTS.image.height,
|
||||
data: {
|
||||
storageId,
|
||||
filename: file.name,
|
||||
mimeType: file.type,
|
||||
...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),
|
||||
canvasId,
|
||||
},
|
||||
clientRequestId,
|
||||
}).then((realId) => {
|
||||
void syncPendingMoveForClientRequest(clientRequestId, realId).catch(
|
||||
(error: unknown) => {
|
||||
console.error("[Canvas] drop createNode syncPendingMove failed", error);
|
||||
},
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to upload dropped file:", error);
|
||||
toast.error(
|
||||
t("canvas.uploadFailed"),
|
||||
error instanceof Error ? error.message : undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedPayload = parseCanvasDropPayload(rawData);
|
||||
if (!parsedPayload) {
|
||||
toast.warning(
|
||||
"Node-Typ nicht verfuegbar",
|
||||
"Unbekannter Node konnte nicht erstellt werden.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const position = screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
const defaults = NODE_DEFAULTS[parsedPayload.nodeType] ?? {
|
||||
width: 200,
|
||||
height: 100,
|
||||
data: {},
|
||||
};
|
||||
const clientRequestId = crypto.randomUUID();
|
||||
|
||||
void runCreateNodeOnlineOnly({
|
||||
canvasId,
|
||||
type: parsedPayload.nodeType,
|
||||
positionX: position.x,
|
||||
positionY: position.y,
|
||||
width: defaults.width,
|
||||
height: defaults.height,
|
||||
data: { ...defaults.data, ...parsedPayload.payloadData, canvasId },
|
||||
clientRequestId,
|
||||
}).then((realId) => {
|
||||
void syncPendingMoveForClientRequest(clientRequestId, realId).catch(
|
||||
(error: unknown) => {
|
||||
console.error("[Canvas] createNode syncPendingMove failed", error);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
[
|
||||
canvasId,
|
||||
generateUploadUrl,
|
||||
isSyncOnline,
|
||||
notifyOfflineUnsupported,
|
||||
runCreateNodeOnlineOnly,
|
||||
screenToFlowPosition,
|
||||
syncPendingMoveForClientRequest,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
onDragOver,
|
||||
onDrop,
|
||||
};
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export default defineConfig({
|
||||
"tests/**/*.test.ts",
|
||||
"components/canvas/__tests__/canvas-flow-reconciliation-helpers.test.ts",
|
||||
"components/canvas/__tests__/use-canvas-flow-reconciliation.test.ts",
|
||||
"components/canvas/__tests__/use-canvas-drop.test.tsx",
|
||||
"components/canvas/__tests__/use-canvas-node-interactions.test.tsx",
|
||||
"components/canvas/__tests__/use-canvas-sync-engine.test.ts",
|
||||
"components/canvas/__tests__/use-canvas-sync-engine-hook.test.tsx",
|
||||
|
||||
Reference in New Issue
Block a user