fix: warn before leaving unsaved map drafts

This commit is contained in:
Nico 2026-06-03 17:18:04 -07:00
parent fd22cc1fe8
commit f8b3a684b8
2 changed files with 77 additions and 3 deletions

View File

@ -1,5 +1,5 @@
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { useBeforeUnload, useLocation, useNavigate, useParams } from "react-router-dom";
import {
createBlankLocationMap,
createLocationMapFromZones,
@ -79,12 +79,22 @@ export default function LocationMapManager() {
? itemsForZone(mapState?.items || [], selectedObject.zone_id, filters, username)
: [];
const visibleUnmappedItems = unmappedItems(mapState?.items || [], filters, username);
const shouldGuardLeave = canManage && hasUnsavedChanges;
const mapSize = {
width: mapDraft.width || activeMap?.width || DEFAULT_MAP_SIZE.width,
height: mapDraft.height || activeMap?.height || DEFAULT_MAP_SIZE.height,
};
useBeforeUnload(
useCallback((event) => {
if (!shouldGuardLeave) return;
event.preventDefault();
event.returnValue = "";
}, [shouldGuardLeave]),
{ capture: true }
);
const syncMapDraftFromState = useCallback((nextState, nextMode, nextPreviewDraft) => {
const nextMap = mapForMode(nextState, nextMode, nextPreviewDraft);
const nextObjects = objectsForMode(nextState, nextMode, nextPreviewDraft);
@ -435,12 +445,12 @@ export default function LocationMapManager() {
}, [navigate, returnPath]);
const handleBack = useCallback(() => {
if (hasUnsavedChanges && canManage) {
if (shouldGuardLeave) {
setPendingLeave(true);
return;
}
performBackNavigation();
}, [canManage, hasUnsavedChanges, performBackNavigation]);
}, [performBackNavigation, shouldGuardLeave]);
if (!hasLoaded || householdLoading || loading) {
return (

View File

@ -669,6 +669,70 @@ test("admin save progress locks draft controls", async ({ page }) => {
await expect(page.getByRole("button", { name: "Save Draft" })).toBeDisabled();
});
test("admin hard navigation warns when draft edits are unsaved", async ({ page }) => {
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1371,
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);
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");
const cleanBeforeUnload = await page.evaluate(() => {
const event = new Event("beforeunload", { cancelable: true });
const dispatchResult = window.dispatchEvent(event);
return {
defaultPrevented: event.defaultPrevented,
dispatchResult,
};
});
expect(cleanBeforeUnload).toEqual({
defaultPrevented: false,
dispatchResult: true,
});
await page.getByRole("button", { name: "Continue Editing" }).click();
await page.getByRole("button", { name: "Edit Objects" }).click();
await page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first().click();
await page.getByRole("textbox", { name: "Label" }).fill("Hard Navigation Bakery");
const dirtyBeforeUnload = await page.evaluate(() => {
const event = new Event("beforeunload", { cancelable: true });
const dispatchResult = window.dispatchEvent(event);
return {
defaultPrevented: event.defaultPrevented,
dispatchResult,
};
});
expect(dirtyBeforeUnload).toEqual({
defaultPrevented: true,
dispatchResult: false,
});
});
test("admin publish saves pending map edits before publishing", async ({ page }) => {
await mockMapShell(page);