feat(canvas): add persistent node favorites with toolbar star and glow

This commit is contained in:
2026-04-09 14:12:43 +02:00
parent e4d39a21fd
commit b08e448be0
18 changed files with 625 additions and 76 deletions

View File

@@ -1,4 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import type { Id } from "@/convex/_generated/dataModel";
import { createCanvasSyncEngineController } from "@/components/canvas/use-canvas-sync-engine";
@@ -75,6 +77,67 @@ describe("useCanvasSyncEngine", () => {
expect(controller.pendingDataAfterCreateRef.current.has("req-2")).toBe(false);
});
it("keeps favorite fields in pinned and deferred optimistic data updates", async () => {
const enqueueSyncMutation = vi.fn(async () => undefined);
const controller = createCanvasSyncEngineController({
canvasId: asCanvasId("canvas-1"),
isSyncOnline: true,
getEnqueueSyncMutation: () => enqueueSyncMutation,
getRunBatchRemoveNodes: () => vi.fn(async () => undefined),
getRunSplitEdgeAtExistingNode: () => vi.fn(async () => undefined),
});
const favoritePayload = {
storageId: "storage-next",
filename: "hero.png",
isFavorite: true,
};
await controller.queueNodeDataUpdate({
nodeId: asNodeId("optimistic_req-favorite"),
data: favoritePayload,
});
expect(
controller.pendingLocalNodeDataUntilConvexMatchesRef.current.get(
"optimistic_req-favorite",
),
).toEqual(favoritePayload);
await controller.syncPendingMoveForClientRequest(
"req-favorite",
asNodeId("node-favorite"),
);
expect(enqueueSyncMutation).toHaveBeenCalledWith("updateData", {
nodeId: asNodeId("node-favorite"),
data: favoritePayload,
});
expect(
controller.pendingLocalNodeDataUntilConvexMatchesRef.current.get("node-favorite"),
).toEqual(favoritePayload);
});
it("uses favorite-preserving payloads in media replacement write paths", () => {
const imageNodeSource = readFileSync(
resolve(process.cwd(), "components/canvas/nodes/image-node.tsx"),
"utf8",
);
const assetBrowserSource = readFileSync(
resolve(process.cwd(), "components/canvas/asset-browser-panel.tsx"),
"utf8",
);
const videoBrowserSource = readFileSync(
resolve(process.cwd(), "components/canvas/video-browser-panel.tsx"),
"utf8",
);
expect(imageNodeSource).toContain("preserveNodeFavorite(");
expect(assetBrowserSource).toContain("preserveNodeFavorite(");
expect(videoBrowserSource).toContain("preserveNodeFavorite(");
});
it("pins local node data immediately when queueing an update", async () => {
const enqueueSyncMutation = vi.fn(async () => undefined);