fix: sync canvas save revisions
This commit is contained in:
parent
c82223f8f4
commit
64ba6a6f29
@ -47,6 +47,8 @@ interface ApiResponse<T = any> {
|
|||||||
data?: T;
|
data?: T;
|
||||||
error?: string;
|
error?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
|
code?: string;
|
||||||
|
canvasUpdatedAt?: string;
|
||||||
pagination?: {
|
pagination?: {
|
||||||
page: number;
|
page: number;
|
||||||
limit: 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 {
|
export interface NormalizedCropRect {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
@ -147,6 +165,9 @@ export const canvasService = {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/canvas/${id}`);
|
const response = await fetch(`/api/canvas/${id}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
if (data?.success) {
|
||||||
|
syncCanvasStoreRevisionFromProject(data.data);
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: '获取画布失败' };
|
return { success: false, error: '获取画布失败' };
|
||||||
@ -165,6 +186,9 @@ export const canvasService = {
|
|||||||
body: JSON.stringify({ ...project, workspaceType }),
|
body: JSON.stringify({ ...project, workspaceType }),
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
if (data?.success) {
|
||||||
|
syncCanvasStoreRevisionFromProject(data.data);
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: '创建画布失败' };
|
return { success: false, error: '创建画布失败' };
|
||||||
@ -243,8 +267,16 @@ export const canvasService = {
|
|||||||
success: false,
|
success: false,
|
||||||
error: String(data?.error || data?.message || '保存画布状态失败'),
|
error: String(data?.error || data?.message || '保存画布状态失败'),
|
||||||
status: response.status,
|
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;
|
return data as ApiResponse;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: '保存画布状态失败' };
|
return { success: false, error: '保存画布状态失败' };
|
||||||
|
|||||||
@ -514,10 +514,139 @@ test.describe("canvas chat pure helpers", () => {
|
|||||||
canvasData,
|
canvasData,
|
||||||
workspaceType: "canvas",
|
workspaceType: "canvas",
|
||||||
baseCanvasRevision: "2026-06-22T00:00:12.000Z",
|
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 = {
|
const plannerContext: CanvasAgentContext = {
|
||||||
version: 1,
|
version: 1,
|
||||||
source: "canvas",
|
source: "canvas",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user