feat: delete project sync

This commit is contained in:
Stanley Cheung 2026-02-11 16:44:03 +08:00
parent 4c67ec6209
commit 658b9b7758
3 changed files with 185 additions and 6 deletions

View File

@ -86,8 +86,137 @@ test("login loads backend projects list on /projects", async ({ context, page })
await page.getByRole("button", { name: "Sign in" }).click();
await expect(page).toHaveURL(/\/projects/);
await page.waitForResponse((response) =>
response.url().includes("/api/editor/projects"),
);
await expect(page.getByText("Backend Project Alpha")).toBeVisible();
});
test("deleting project from /projects sends cloud DELETE request", async ({
context,
page,
}) => {
await page.addInitScript(() => {
window.localStorage.setItem("vibecut_workspace_id", "ws-1");
});
await context.addCookies([
{
name: "refresh_token",
value: "e2e-refresh-token",
domain: "127.0.0.1",
path: "/",
httpOnly: false,
secure: false,
sameSite: "Lax",
},
]);
await page.route("**/api/auth/user/refresh", async (route) => {
await route.fulfill({
status: 401,
contentType: "application/json",
body: JSON.stringify({
status: "error",
message: "Invalid refresh token",
statusCode: 401,
}),
});
});
await page.route("**/api/auth/user/login", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
token: "backend-jwt-token",
user: {
id: "u-1",
username: "john",
email: "john@example.com",
},
}),
});
});
const deleteRequestPromise = page.waitForRequest(
(request) =>
request.method() === "DELETE" &&
request.url().includes("/api/editor/projects/proj-remote-1"),
);
await page.route("**/api/editor/projects/*", async (route) => {
const workspaceHeader = route.request().headers()["x-workspace-id"];
if (!workspaceHeader) {
await route.fulfill({
status: 400,
contentType: "application/json",
body: JSON.stringify({
message: "x-workspace-id header is required",
error: "Bad Request",
statusCode: 400,
}),
});
return;
}
if (route.request().method() === "DELETE") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ ok: true }),
});
return;
}
await route.fulfill({ status: 405 });
});
await page.route("**/api/editor/projects**", async (route) => {
const workspaceHeader = route.request().headers()["x-workspace-id"];
if (!workspaceHeader) {
await route.fulfill({
status: 400,
contentType: "application/json",
body: JSON.stringify({
message: "x-workspace-id header is required",
error: "Bad Request",
statusCode: 400,
}),
});
return;
}
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
items: [
{
id: "proj-remote-1",
workspaceId: "ws-1",
ownerId: 1,
name: "Backend Project Alpha",
version: 4,
updatedAt: "2026-02-09T12:00:00.000Z",
createdAt: "2026-02-08T12:00:00.000Z",
},
],
total: 1,
}),
});
});
await page.goto("/auth/login");
await page.getByLabel("Username").fill("john");
await page.getByLabel("Password").fill("12345678");
await page.getByRole("button", { name: "Sign in" }).click();
await expect(page).toHaveURL(/\/projects/);
await expect(page.getByText("Backend Project Alpha")).toBeVisible();
await page.getByRole("button", { name: "Project menu" }).first().click();
await page.getByRole("menuitem", { name: "Delete" }).click();
await page.getByPlaceholder("DELETE").fill("DELETE");
await page.getByRole("button", { name: "Delete project" }).click();
await deleteRequestPromise;
});

View File

@ -307,8 +307,33 @@ export class ProjectManager {
if (uniqueIds.length === 0) return;
try {
const token = authSession.getToken();
const deletedProjectIds =
token === null
? uniqueIds
: (
await Promise.all(
uniqueIds.map(async (id) => {
try {
await editorCloudApi.deleteProject({ projectId: id, token });
return id;
} catch (error) {
console.error(`Failed to delete cloud project ${id}:`, error);
return null;
}
}),
)
).filter((id): id is string => id !== null);
if (deletedProjectIds.length === 0) {
toast.error("Failed to delete projects", {
description: "Cloud delete failed. Please try again.",
});
return;
}
await Promise.all(
uniqueIds.map((id) =>
deletedProjectIds.map((id) =>
Promise.all([
storageService.deleteProjectMedia({ projectId: id }),
storageService.deleteProject({ id }),
@ -316,10 +341,13 @@ export class ProjectManager {
),
);
const idSet = new Set(uniqueIds);
const idSet = new Set(deletedProjectIds);
this.savedProjects = this.savedProjects.filter(
(project) => !idSet.has(project.id),
);
deletedProjectIds.forEach((id) => {
this.remoteVersionByProjectId.delete(id);
});
const shouldClearActive =
this.active && idSet.has(this.active.metadata.id);
@ -331,8 +359,18 @@ export class ProjectManager {
}
this.notify();
if (deletedProjectIds.length < uniqueIds.length) {
toast.error("Some projects were not deleted", {
description:
"Cloud sync failed for some selected projects. Please try again.",
});
}
} catch (error) {
console.error("Failed to delete projects:", error);
toast.error("Failed to delete projects", {
description: error instanceof Error ? error.message : "Please try again",
});
}
}

View File

@ -133,7 +133,7 @@ const requestWithAuthRetry = async <T>({
body,
}: {
path: string;
method: "GET" | "PUT" | "POST";
method: "GET" | "PUT" | "POST" | "DELETE";
token: string;
body?: unknown;
}): Promise<T> => {
@ -277,6 +277,18 @@ export const editorCloudApi = {
token,
body: payload,
}),
deleteProject: ({
projectId,
token,
}: {
projectId: string;
token: string;
}) =>
requestWithAuthRetry<void>({
path: `/editor/projects/${projectId}`,
method: "DELETE",
token,
}),
createFileUpload: ({
token,
payload,