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);