diff --git a/frontend/src/components/maps/LocationMapBottomSheet.jsx b/frontend/src/components/maps/LocationMapBottomSheet.jsx index ac6090f..222e8e3 100644 --- a/frontend/src/components/maps/LocationMapBottomSheet.jsx +++ b/frontend/src/components/maps/LocationMapBottomSheet.jsx @@ -77,7 +77,14 @@ export default function LocationMapBottomSheet({ - + diff --git a/frontend/src/pages/LocationMapManager.jsx b/frontend/src/pages/LocationMapManager.jsx index b8b3211..b37fd08 100644 --- a/frontend/src/pages/LocationMapManager.jsx +++ b/frontend/src/pages/LocationMapManager.jsx @@ -249,13 +249,25 @@ export default function LocationMapManager() { if (!activeHousehold?.id || !locationId) return; setSaving(true); try { + if (hasUnsavedChanges) { + await saveLocationMapDraft(activeHousehold.id, locationId, { + map: mapDraft, + objects: prepareObjectsForSave(objects), + }); + } + const response = await publishLocationMapDraft(activeHousehold.id, locationId); setMapState(response.data); setMode("view"); setEditorTool("pan"); setPreviewDraft(false); syncMapDraftFromState(response.data, "view", false); - toast.success("Published map", "Map is now visible to household members"); + toast.success( + "Published map", + hasUnsavedChanges + ? "Latest edits were saved and published" + : "Map is now visible to household members" + ); } catch (error) { toast.error("Publish failed", getApiErrorMessage(error, "Failed to publish map")); } finally { diff --git a/frontend/tests/location-map-manager.spec.ts b/frontend/tests/location-map-manager.spec.ts index e7cdd44..5002d86 100644 --- a/frontend/tests/location-map-manager.spec.ts +++ b/frontend/tests/location-map-manager.spec.ts @@ -356,7 +356,7 @@ test("admin creates a map from zones, saves a draft, and publishes it", async ({ await expect(page.locator(".location-map-object", { hasText: "Bread Wall" })).toBeVisible(); await expect(page.locator(".location-map-object", { hasText: "Temporary Freezer" })).toHaveCount(0); - await page.getByRole("button", { name: "Publish" }).click(); + await page.getByRole("button", { name: "Publish", exact: true }).click(); await expect(page.locator(".location-map-status")).toHaveText("Published"); await expect(page.getByRole("button", { name: "Edit Map" })).toBeVisible(); await expect(page.getByText("loose batteries")).toHaveCount(0); @@ -433,6 +433,89 @@ test("admin status follows the visible map when a saved draft exists", async ({ await expect(page.locator(".location-map-object", { hasText: "Live Bakery" })).toBeVisible(); }); +test("admin publish saves pending map edits before publishing", async ({ page }) => { + await mockMapShell(page); + + let mapState = publishedMapState([ + { + id: 1401, + 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: 1450 + 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; + mapState = publishedMapState(mapState.draft_objects, true); + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(mapState), + }); + }); + + 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("Quick Publish 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: "Quick Publish Bakery", + zone_id: 501, + }), + ]) + ); + await expect(page.locator(".location-map-status")).toHaveText("Published"); + await expect(page.getByRole("button", { name: "Map area Quick Publish 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);