fix: sync canvas save revisions
This commit is contained in:
parent
c82223f8f4
commit
64ba6a6f29
@ -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: '保存画布状态失败' };
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user