diff --git a/frontend/src/pages/LocationMapManager.jsx b/frontend/src/pages/LocationMapManager.jsx index 0535f32..ef8c4fc 100644 --- a/frontend/src/pages/LocationMapManager.jsx +++ b/frontend/src/pages/LocationMapManager.jsx @@ -77,6 +77,8 @@ export default function LocationMapManager() { const svgRef = useRef(null); const toastRef = useRef(toast); const loadRequestRef = useRef(0); + const guardedClickTargetRef = useRef(null); + const allowGuardedClickRef = useRef(false); const location = mapState?.location || stores.find((store) => String(store.id) === String(locationId)); const canManage = Boolean(mapState?.can_manage ?? ["owner", "admin"].includes(activeHousehold?.role)); @@ -144,10 +146,13 @@ export default function LocationMapManager() { useEffect(() => { if (!shouldGuardLeave) { setPendingLeaveTarget(null); + guardedClickTargetRef.current = null; return undefined; } const handleDocumentClick = (event) => { + if (allowGuardedClickRef.current) return; + if ( event.defaultPrevented || event.button !== 0 || @@ -160,6 +165,15 @@ export default function LocationMapManager() { } const target = event.target instanceof Element ? event.target : null; + const button = target?.closest("button"); + if (button?.classList.contains("user-dropdown-logout")) { + event.preventDefault(); + event.stopPropagation(); + guardedClickTargetRef.current = button; + setPendingLeaveTarget({ type: "click" }); + return; + } + const anchor = target?.closest("a[href]"); if (!anchor || (anchor.target && anchor.target !== "_self") || anchor.hasAttribute("download")) { return; @@ -610,6 +624,18 @@ export default function LocationMapManager() { const confirmPendingLeave = useCallback(() => { const target = pendingLeaveTarget; setPendingLeaveTarget(null); + if (target?.type === "click") { + const guardedClickTarget = guardedClickTargetRef.current; + guardedClickTargetRef.current = null; + if (guardedClickTarget) { + allowGuardedClickRef.current = true; + guardedClickTarget.click(); + window.setTimeout(() => { + allowGuardedClickRef.current = false; + }, 0); + return; + } + } if (target?.type === "path" && target.path) { navigate(target.path); return; @@ -774,7 +800,10 @@ export default function LocationMapManager() { title="Leave with unsaved changes?" description="Your draft edits are only on this screen right now. Save the draft before leaving if you want to keep them." confirmLabel="Leave Map" - onClose={() => setPendingLeaveTarget(null)} + onClose={() => { + guardedClickTargetRef.current = null; + setPendingLeaveTarget(null); + }} onConfirm={confirmPendingLeave} /> diff --git a/frontend/tests/location-map-manager.spec.ts b/frontend/tests/location-map-manager.spec.ts index 3177fce..fdb6792 100644 --- a/frontend/tests/location-map-manager.spec.ts +++ b/frontend/tests/location-map-manager.spec.ts @@ -1502,6 +1502,69 @@ test("admin app navigation warns when draft edits are unsaved", async ({ page }) await expect(page).toHaveURL(/\/$/); }); +test("admin logout warns when draft edits are unsaved", async ({ page }) => { + await mockMapShell(page); + + const mapState = draftMapState([ + { + id: 1373, + location_map_id: 900, + zone_id: 501, + zone_name: "Bakery", + type: "zone", + label: "Bakery", + x: 40, + y: 40, + width: 260, + height: 160, + rotation: 0, + locked: false, + visible: true, + sort_order: 1, + }, + ], true); + + let logoutCalls = 0; + 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("**/auth/logout", async (route) => { + logoutCalls += 1; + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ message: "Logged out" }), + }); + }); + + await page.goto("/stores/100/locations/10/map"); + + await page.getByRole("button", { name: "Continue Editing" }).click(); + await page.getByRole("button", { name: "Edit Areas" }).click(); + await page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first().click(); + await page.getByRole("textbox", { name: "Label" }).fill("Unsaved Logout Bakery"); + + await page.getByRole("button", { name: "User menu" }).click(); + await page.getByRole("button", { name: "Logout", exact: true }).click(); + + await expect(page.getByRole("heading", { name: "Leave with unsaved changes?" })).toBeVisible(); + expect(logoutCalls).toBe(0); + await page.getByRole("button", { name: "Cancel" }).click(); + await expect(page).toHaveURL(/\/stores\/100\/locations\/10\/map$/); + await expect(page.getByRole("textbox", { name: "Label" })).toHaveValue("Unsaved Logout Bakery"); + expect(logoutCalls).toBe(0); + + await page.getByRole("button", { name: "Logout", exact: true }).click(); + await confirmSlide(page); + + await expect(page).toHaveURL(/\/login$/); + expect(logoutCalls).toBe(1); +}); + test("admin publish saves pending map edits before publishing", async ({ page }) => { await mockMapShell(page);