diff --git a/frontend/src/components/maps/LocationMapBottomSheet.jsx b/frontend/src/components/maps/LocationMapBottomSheet.jsx
index 13747a6..0965a85 100644
--- a/frontend/src/components/maps/LocationMapBottomSheet.jsx
+++ b/frontend/src/components/maps/LocationMapBottomSheet.jsx
@@ -121,6 +121,10 @@ export default function LocationMapBottomSheet({
showEditorControls && hiddenAreaCount > 0 && filters.showZones && !hasNoVisibleAreas;
const saveDraftLabel = savingAction === "save" ? "Saving..." : "Save Draft";
const publishLabel = savingAction === "publish" ? "Publishing..." : "Publish";
+ const canPublishDraft = Boolean(mapState?.draft_map || hasUnsavedChanges);
+ const showSaveDraftAction = Boolean(savingAction === "save" || hasUnsavedChanges);
+ const showPublishAction = Boolean(savingAction === "publish" || canPublishDraft);
+ const showPreviewDraftAction = Boolean((mapState?.draft_map || hasUnsavedChanges) && !showSelectedObjectForm);
const mappedItemSummary = assignedItemCount === visibleAssignedItemCount
? String(assignedItemCount)
: `${visibleAssignedItemCount} shown`;
@@ -152,25 +156,29 @@ export default function LocationMapBottomSheet({
showCompleted: true,
}));
};
- const primaryDraftActions = showEditorControls ? (
+ const primaryDraftActions = showEditorControls && (showSaveDraftAction || showPublishAction) ? (
- {!showSelectedObjectForm ? (
+ {showPreviewDraftAction ? (
@@ -236,14 +244,16 @@ export default function LocationMapBottomSheet({
) : null}
- {showSelectedObjectForm && primaryDraftActions ? (
-
- {primaryDraftActions}
-
+ {showSelectedObjectForm ? (
+ primaryDraftActions ? (
+
+ {primaryDraftActions}
+
+ ) : null
) : editorActions}
{showHiddenAreaAction ? (
diff --git a/frontend/tests/location-map-manager.spec.ts b/frontend/tests/location-map-manager.spec.ts
index e26d887..16d7200 100644
--- a/frontend/tests/location-map-manager.spec.ts
+++ b/frontend/tests/location-map-manager.spec.ts
@@ -1229,6 +1229,53 @@ 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 edit mode hides unavailable draft actions until changes exist", async ({ page }) => {
+ await mockMapShell(page);
+
+ const mapState = publishedMapState([
+ {
+ id: 1201,
+ 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);
+
+ await page.route("**/households/1/locations/10/map", async (route) => {
+ 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 Draft" }).click();
+
+ const secondaryActions = page.locator(".location-map-secondary-actions");
+ await expect(page.locator(".location-map-primary-actions")).toHaveCount(0);
+ await expect(secondaryActions.getByRole("button", { name: "Add Area" })).toBeVisible();
+ await expect(secondaryActions.getByRole("button", { name: "Preview Draft" })).toHaveCount(0);
+ await expect(page.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
+ await expect(page.getByRole("button", { name: "Publish", exact: true })).toHaveCount(0);
+
+ await secondaryActions.getByRole("button", { name: "Add Area" }).click();
+
+ await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft");
+ await expect(page.getByRole("button", { name: "Save Draft" })).toBeEnabled();
+ await expect(page.getByRole("button", { name: "Publish", exact: true })).toBeEnabled();
+});
+
test("mode switches close open map layers without resetting filters", async ({ page }) => {
await mockMapShell(page);
@@ -1561,7 +1608,7 @@ test("admin selecting an object does not mark a draft dirty until it changes", a
await expect(page.getByRole("textbox", { name: "Label" })).toBeVisible();
await expect(page.locator(".location-map-status")).toHaveText("Draft");
- await expect(page.getByRole("button", { name: "Save Draft" })).toBeDisabled();
+ await expect(page.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Undo" })).toBeDisabled();
const objectBox = await bakeryObject.boundingBox();
@@ -2098,7 +2145,7 @@ test("admin save progress locks draft controls", async ({ page }) => {
releaseSave();
await expect(page.locator(".location-map-status")).toHaveText("Draft");
- await expect(page.getByRole("button", { name: "Save Draft" })).toBeDisabled();
+ await expect(page.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await expect(page.getByRole("textbox", { name: "Label" })).toBeEnabled();
await expect(page.getByRole("textbox", { name: "Label" })).toHaveValue("Saving Bakery");
await expect(page.getByRole("button", { name: "Map area Saving Bakery" })).toHaveAttribute("aria-pressed", "true");
@@ -2611,7 +2658,7 @@ test("admin publish failure keeps successfully saved pending edits", async ({ pa
])
);
await expect(page.locator(".location-map-status")).toHaveText("Draft");
- await expect(page.getByRole("button", { name: "Save Draft" })).toBeDisabled();
+ await expect(page.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Map area Saved Draft Bakery" })).toBeVisible();
await expect(page.getByRole("button", { name: "Map area Live Bakery" })).toHaveCount(0);
});
@@ -3957,7 +4004,7 @@ test("mobile editor uses bottom sheet controls instead of desktop object toolbar
await expect(historyActions.getByRole("button", { name: "Redo" })).toBeDisabled();
const primaryActions = page.locator(".location-map-primary-actions");
const secondaryActions = page.locator(".location-map-secondary-actions");
- await expect(primaryActions.getByRole("button", { name: "Save Draft" })).toBeVisible();
+ await expect(primaryActions.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await expect(primaryActions.getByRole("button", { name: "Publish" })).toBeVisible();
await expect(secondaryActions.getByRole("button", { name: "Add Area" })).toBeVisible();
await expect(secondaryActions.getByRole("button", { name: "Preview Draft" })).toBeVisible();
@@ -4017,7 +4064,7 @@ test("mobile editor uses bottom sheet controls instead of desktop object toolbar
await expect(page.getByRole("button", { name: "Locked" })).toHaveAttribute("aria-pressed", "false");
const selectedPrimaryActions = page.locator(".location-map-selected-primary-actions");
const selectedSecondaryActions = page.locator(".location-map-selected-secondary-actions");
- await expect(selectedPrimaryActions.getByRole("button", { name: "Save Draft" })).toBeVisible();
+ await expect(selectedPrimaryActions.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await expect(selectedPrimaryActions.getByRole("button", { name: "Publish" })).toBeVisible();
await expect(selectedSecondaryActions.getByRole("button", { name: "Add Area" })).toBeVisible();
await expect(selectedSecondaryActions.getByRole("button", { name: "Preview Draft" })).toHaveCount(0);
@@ -4049,7 +4096,7 @@ test("mobile editor uses bottom sheet controls instead of desktop object toolbar
await closeSelectedButton.click();
await expect(page.getByRole("textbox", { name: "Label" })).toHaveCount(0);
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Edit Areas");
- await expect(page.getByRole("button", { name: "Save Draft" })).toBeDisabled();
+ await expect(page.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await page.getByRole("button", { name: "View", exact: true }).click();
await expect(page.getByRole("textbox", { name: "Label" })).toHaveCount(0);
});