feat(media): add Convex media archive with backfill and mixed-media library

This commit is contained in:
2026-04-10 15:15:44 +02:00
parent ddb2412349
commit a1df097f9c
26 changed files with 2664 additions and 122 deletions

View File

@@ -8,6 +8,10 @@ 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 {
emitDashboardSnapshotCacheInvalidationSignal,
invalidateDashboardSnapshotForLastSignedInUser,
} from "@/lib/dashboard-snapshot-cache";
import { toast } from "@/lib/toast";
import { useCanvasDrop } from "@/components/canvas/use-canvas-drop";
import { createCompressedImagePreview } from "@/components/canvas/canvas-media-utils";
@@ -28,6 +32,11 @@ vi.mock("@/components/canvas/canvas-media-utils", () => ({
})),
}));
vi.mock("@/lib/dashboard-snapshot-cache", () => ({
invalidateDashboardSnapshotForLastSignedInUser: vi.fn(),
emitDashboardSnapshotCacheInvalidationSignal: vi.fn(),
}));
const latestHandlersRef: {
current: ReturnType<typeof useCanvasDrop> | null;
} = { current: null };
@@ -245,6 +254,62 @@ describe("useCanvasDrop", () => {
width: 1600,
height: 900,
});
expect(invalidateDashboardSnapshotForLastSignedInUser).toHaveBeenCalledTimes(1);
expect(emitDashboardSnapshotCacheInvalidationSignal).toHaveBeenCalledTimes(1);
});
it("registers dropped image media when node creation fails", async () => {
const registerUploadedImageMedia = vi.fn(async () => ({ ok: true as const }));
const runCreateNodeOnlineOnly = vi.fn(async () => {
throw new Error("create failed");
});
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
registerUploadedImageMedia={registerUploadedImageMedia}
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);
});
await act(async () => {
await Promise.resolve();
});
expect(syncPendingMoveForClientRequest).not.toHaveBeenCalled();
expect(registerUploadedImageMedia).toHaveBeenCalledWith({
canvasId: "canvas-1",
storageId: "storage-1",
filename: "photo.png",
mimeType: "image/png",
width: 1600,
height: 900,
});
expect(registerUploadedImageMedia).not.toHaveBeenCalledWith(
expect.objectContaining({ nodeId: expect.anything() }),
);
expect(invalidateDashboardSnapshotForLastSignedInUser).toHaveBeenCalledTimes(1);
expect(emitDashboardSnapshotCacheInvalidationSignal).toHaveBeenCalledTimes(1);
});
it("creates a node from a JSON payload drop", async () => {