fix: sync canvas save revisions

This commit is contained in:
134 2026-06-23 22:43:22 +08:00
parent c82223f8f4
commit 64ba6a6f29
2 changed files with 162 additions and 1 deletions

View File

@ -47,6 +47,8 @@ interface ApiResponse<T = any> {
data?: T;
error?: string;
status?: number;
code?: string;
canvasUpdatedAt?: string;
pagination?: {
page: number;
limit: number;
@ -54,6 +56,22 @@ interface ApiResponse<T = any> {
};
}
function normalizeRevisionText(value: unknown) {
return String(value || "").trim();
}
function syncCanvasStoreRevisionFromUpdatedAt(updatedAt: unknown): void {
if (typeof window === "undefined") return;
const revision = normalizeRevisionText(updatedAt);
if (!revision) return;
useCanvasStore.getState().setCanvasRevision(revision, revision);
}
function syncCanvasStoreRevisionFromProject(project?: Partial<CanvasProject> | null): void {
if (!project?.canvasData) return;
syncCanvasStoreRevisionFromUpdatedAt(project.updatedAt);
}
export interface NormalizedCropRect {
x: number;
y: number;
@ -147,6 +165,9 @@ export const canvasService = {
try {
const response = await fetch(`/api/canvas/${id}`);
const data = await response.json();
if (data?.success) {
syncCanvasStoreRevisionFromProject(data.data);
}
return data;
} catch (error) {
return { success: false, error: '获取画布失败' };
@ -165,6 +186,9 @@ export const canvasService = {
body: JSON.stringify({ ...project, workspaceType }),
});
const data = await response.json();
if (data?.success) {
syncCanvasStoreRevisionFromProject(data.data);
}
return data;
} catch (error) {
return { success: false, error: '创建画布失败' };
@ -243,8 +267,16 @@ export const canvasService = {
success: false,
error: String(data?.error || data?.message || '保存画布状态失败'),
status: response.status,
code: typeof data?.code === "string" ? data.code : undefined,
canvasUpdatedAt:
typeof data?.canvasUpdatedAt === "string"
? data.canvasUpdatedAt
: undefined,
};
}
if (data?.success) {
syncCanvasStoreRevisionFromUpdatedAt(data?.data?.updatedAt);
}
return data as ApiResponse;
} catch (error) {
return { success: false, error: '保存画布状态失败' };

View File

@ -514,10 +514,139 @@ test.describe("canvas chat pure helpers", () => {
canvasData,
workspaceType: "canvas",
baseCanvasRevision: "2026-06-22T00:00:12.000Z",
baseCanvasUpdatedAt: "2026-06-22T00:00:10.000Z",
baseCanvasUpdatedAt: "2026-06-22T00:00:11.000Z",
});
});
test("canvas service keeps store revision aligned after project load and save", async () => {
const canvasData = {
nodes: [],
edges: [],
selectedNodeIds: [],
viewport: { x: 0, y: 0, zoom: 1 },
tool: "select" as const,
};
const originalFetch = globalThis.fetch;
const hadWindow = Object.prototype.hasOwnProperty.call(globalThis, "window");
const originalWindow = (globalThis as typeof globalThis & { window?: unknown })
.window;
const responses = [
{
success: true,
data: {
id: "11111111-1111-4111-8111-111111111111",
title: "测试画布",
canvasData,
updatedAt: "2026-06-22T00:01:00.000Z",
},
},
{
success: true,
data: {
updatedAt: "2026-06-22T00:01:05.000Z",
},
},
];
useCanvasStore.getState().setCanvasRevision(null, null);
Object.defineProperty(globalThis, "window", {
configurable: true,
value: {},
writable: true,
});
globalThis.fetch = (async () =>
({
ok: true,
status: 200,
json: async () => responses.shift()!,
}) as Response) as typeof fetch;
try {
await canvasService.getProject("11111111-1111-4111-8111-111111111111");
expect(useCanvasStore.getState().canvasRevision).toBe(
"2026-06-22T00:01:00.000Z"
);
await canvasService.saveCanvasState(
"11111111-1111-4111-8111-111111111111",
canvasData
);
expect(useCanvasStore.getState().canvasRevision).toBe(
"2026-06-22T00:01:05.000Z"
);
expect(useCanvasStore.getState().canvasUpdatedAt).toBe(
"2026-06-22T00:01:05.000Z"
);
} finally {
globalThis.fetch = originalFetch;
if (hadWindow) {
Object.defineProperty(globalThis, "window", {
configurable: true,
value: originalWindow,
writable: true,
});
} else {
Reflect.deleteProperty(globalThis, "window");
}
useCanvasStore.getState().setCanvasRevision(null, null);
}
});
test("canvas service exposes stale save conflict details", async () => {
const originalFetch = globalThis.fetch;
const hadWindow = Object.prototype.hasOwnProperty.call(globalThis, "window");
const originalWindow = (globalThis as typeof globalThis & { window?: unknown })
.window;
Object.defineProperty(globalThis, "window", {
configurable: true,
value: {},
writable: true,
});
globalThis.fetch = (async () =>
({
ok: false,
status: 409,
json: async () => ({
success: false,
error: "画布已更新,请刷新后再保存",
code: "STALE_CANVAS_REVISION",
canvasUpdatedAt: "2026-06-22T00:02:00.000Z",
}),
}) as Response) as typeof fetch;
try {
const result = await canvasService.saveCanvasState(
"11111111-1111-4111-8111-111111111111",
{
nodes: [],
edges: [],
selectedNodeIds: [],
viewport: { x: 0, y: 0, zoom: 1 },
tool: "select",
}
);
expect(result).toMatchObject({
success: false,
status: 409,
code: "STALE_CANVAS_REVISION",
canvasUpdatedAt: "2026-06-22T00:02:00.000Z",
});
} finally {
globalThis.fetch = originalFetch;
if (hadWindow) {
Object.defineProperty(globalThis, "window", {
configurable: true,
value: originalWindow,
writable: true,
});
} else {
Reflect.deleteProperty(globalThis, "window");
}
}
});
const plannerContext: CanvasAgentContext = {
version: 1,
source: "canvas",