From 415a7223cdfb9d8896c92e9498f5c7fa2b19df3f Mon Sep 17 00:00:00 2001 From: Nico Date: Wed, 3 Jun 2026 20:34:13 -0700 Subject: [PATCH] fix: keep saved map drafts after publish failure --- frontend/src/pages/LocationMapManager.jsx | 14 +++- frontend/tests/location-map-manager.spec.ts | 83 +++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/LocationMapManager.jsx b/frontend/src/pages/LocationMapManager.jsx index e2bc22b..778139b 100644 --- a/frontend/src/pages/LocationMapManager.jsx +++ b/frontend/src/pages/LocationMapManager.jsx @@ -270,12 +270,18 @@ export default function LocationMapManager() { const handlePublish = async () => { if (!activeHousehold?.id || !locationId) return; beginSaving("publish"); + let savedPendingDraft = false; try { if (hasUnsavedChanges) { - await saveLocationMapDraft(activeHousehold.id, locationId, { + const saveResponse = await saveLocationMapDraft(activeHousehold.id, locationId, { map: mapDraft, objects: prepareObjectsForSave(objects), }); + savedPendingDraft = true; + setMapState(saveResponse.data); + setMode("edit"); + setPreviewDraft(true); + syncMapDraftFromState(saveResponse.data, "edit", true); } const response = await publishLocationMapDraft(activeHousehold.id, locationId); @@ -291,7 +297,11 @@ export default function LocationMapManager() { : "Map is now visible to household members" ); } catch (error) { - toast.error("Publish failed", getApiErrorMessage(error, "Failed to publish map")); + const message = getApiErrorMessage(error, "Failed to publish map"); + toast.error( + "Publish failed", + savedPendingDraft ? `${message}. Draft changes were saved.` : message + ); } finally { endSaving(); } diff --git a/frontend/tests/location-map-manager.spec.ts b/frontend/tests/location-map-manager.spec.ts index 5c3c0aa..fcea9d0 100644 --- a/frontend/tests/location-map-manager.spec.ts +++ b/frontend/tests/location-map-manager.spec.ts @@ -967,6 +967,89 @@ test("admin publish saves pending map edits before publishing", async ({ page }) await expect(page.getByRole("button", { name: "Map area Live Bakery" })).toHaveCount(0); }); +test("admin publish failure keeps successfully saved pending edits", async ({ page }) => { + await mockMapShell(page); + + let mapState = publishedMapState([ + { + id: 1461, + location_map_id: 901, + zone_id: 501, + zone_name: "Bakery", + type: "zone", + label: "Live Bakery", + x: 40, + y: 40, + width: 260, + height: 160, + rotation: 0, + locked: false, + visible: true, + sort_order: 1, + }, + ], true); + let savedPayload: Record | null = null; + let publishCalled = false; + + await page.route("**/households/1/locations/10/map", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(mapState), + }); + }); + + await page.route("**/households/1/locations/10/map/draft", async (route) => { + savedPayload = await route.request().postDataJSON(); + const savedObjects = (savedPayload.objects as Array>).map((object, index) => ({ + id: 1465 + index, + location_map_id: 900, + zone_name: object.zone_id === 501 ? "Bakery" : null, + ...object, + })); + mapState = draftAndPublishedMapState(savedObjects, mapState.published_objects, true); + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(mapState), + }); + }); + + await page.route("**/households/1/locations/10/map/publish", async (route) => { + publishCalled = true; + await route.fulfill({ + status: 503, + contentType: "application/json", + body: JSON.stringify({ error: { message: "Publish service unavailable" } }), + }); + }); + + await page.goto("/stores/100/locations/10/map"); + + await page.getByRole("button", { name: "Edit Map" }).click(); + await page.getByRole("button", { name: "Edit Objects" }).click(); + await page.locator(".location-map-object", { hasText: "Live Bakery" }).locator("rect").first().click(); + await page.getByRole("textbox", { name: "Label" }).fill("Saved Draft Bakery"); + await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft"); + + await page.getByRole("button", { name: "Publish", exact: true }).click(); + + await expect.poll(() => publishCalled).toBe(true); + expect(savedPayload).not.toBeNull(); + expect(savedPayload?.objects).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + label: "Saved Draft Bakery", + zone_id: 501, + }), + ]) + ); + await expect(page.locator(".location-map-status")).toHaveText("Draft"); + await expect(page.getByRole("button", { name: "Save Draft" })).toBeDisabled(); + await expect(page.getByRole("button", { name: "Map area Saved Draft Bakery" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Map area Live Bakery" })).toHaveCount(0); +}); + test("viewer wraps long zone labels and keeps item counts visible", async ({ page }) => { await mockMapShell(page);