Add store location map manager #20
@ -73,7 +73,7 @@ export default function LocationMapManager() {
|
|||||||
const [future, setFuture] = useState([]);
|
const [future, setFuture] = useState([]);
|
||||||
const [dragState, setDragState] = useState(null);
|
const [dragState, setDragState] = useState(null);
|
||||||
const [pendingDeleteObject, setPendingDeleteObject] = useState(null);
|
const [pendingDeleteObject, setPendingDeleteObject] = useState(null);
|
||||||
const [pendingLeave, setPendingLeave] = useState(false);
|
const [pendingLeaveTarget, setPendingLeaveTarget] = useState(null);
|
||||||
const svgRef = useRef(null);
|
const svgRef = useRef(null);
|
||||||
const toastRef = useRef(toast);
|
const toastRef = useRef(toast);
|
||||||
const loadRequestRef = useRef(0);
|
const loadRequestRef = useRef(0);
|
||||||
@ -141,6 +141,45 @@ export default function LocationMapManager() {
|
|||||||
{ capture: true }
|
{ capture: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldGuardLeave) {
|
||||||
|
setPendingLeaveTarget(null);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDocumentClick = (event) => {
|
||||||
|
if (
|
||||||
|
event.defaultPrevented ||
|
||||||
|
event.button !== 0 ||
|
||||||
|
event.altKey ||
|
||||||
|
event.ctrlKey ||
|
||||||
|
event.metaKey ||
|
||||||
|
event.shiftKey
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = event.target instanceof Element ? event.target : null;
|
||||||
|
const anchor = target?.closest("a[href]");
|
||||||
|
if (!anchor || (anchor.target && anchor.target !== "_self") || anchor.hasAttribute("download")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextUrl = new URL(anchor.href, window.location.href);
|
||||||
|
if (nextUrl.origin !== window.location.origin) return;
|
||||||
|
|
||||||
|
const currentPath = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||||
|
const nextPath = `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`;
|
||||||
|
if (nextPath === currentPath) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
setPendingLeaveTarget({ type: "path", path: nextPath });
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("click", handleDocumentClick, true);
|
||||||
|
return () => document.removeEventListener("click", handleDocumentClick, true);
|
||||||
|
}, [shouldGuardLeave]);
|
||||||
|
|
||||||
const syncMapDraftFromState = useCallback((nextState, nextMode, nextPreviewDraft) => {
|
const syncMapDraftFromState = useCallback((nextState, nextMode, nextPreviewDraft) => {
|
||||||
const nextMap = mapForMode(nextState, nextMode, nextPreviewDraft);
|
const nextMap = mapForMode(nextState, nextMode, nextPreviewDraft);
|
||||||
const nextObjects = objectsForMode(nextState, nextMode, nextPreviewDraft);
|
const nextObjects = objectsForMode(nextState, nextMode, nextPreviewDraft);
|
||||||
@ -562,12 +601,22 @@ export default function LocationMapManager() {
|
|||||||
|
|
||||||
const handleBack = useCallback(() => {
|
const handleBack = useCallback(() => {
|
||||||
if (shouldGuardLeave) {
|
if (shouldGuardLeave) {
|
||||||
setPendingLeave(true);
|
setPendingLeaveTarget({ type: "back" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
performBackNavigation();
|
performBackNavigation();
|
||||||
}, [performBackNavigation, shouldGuardLeave]);
|
}, [performBackNavigation, shouldGuardLeave]);
|
||||||
|
|
||||||
|
const confirmPendingLeave = useCallback(() => {
|
||||||
|
const target = pendingLeaveTarget;
|
||||||
|
setPendingLeaveTarget(null);
|
||||||
|
if (target?.type === "path" && target.path) {
|
||||||
|
navigate(target.path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
performBackNavigation();
|
||||||
|
}, [navigate, pendingLeaveTarget, performBackNavigation]);
|
||||||
|
|
||||||
if (!hasLoaded || householdLoading || loading) {
|
if (!hasLoaded || householdLoading || loading) {
|
||||||
return (
|
return (
|
||||||
<div className="location-map-page">
|
<div className="location-map-page">
|
||||||
@ -721,15 +770,12 @@ export default function LocationMapManager() {
|
|||||||
onConfirm={handleDeleteObject}
|
onConfirm={handleDeleteObject}
|
||||||
/>
|
/>
|
||||||
<ConfirmSlideModal
|
<ConfirmSlideModal
|
||||||
isOpen={pendingLeave}
|
isOpen={Boolean(pendingLeaveTarget)}
|
||||||
title="Leave with unsaved changes?"
|
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."
|
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"
|
confirmLabel="Leave Map"
|
||||||
onClose={() => setPendingLeave(false)}
|
onClose={() => setPendingLeaveTarget(null)}
|
||||||
onConfirm={() => {
|
onConfirm={confirmPendingLeave}
|
||||||
setPendingLeave(false);
|
|
||||||
performBackNavigation();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1453,6 +1453,55 @@ test("admin hard navigation warns when draft edits are unsaved", async ({ page }
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("admin app navigation warns when draft edits are unsaved", async ({ page }) => {
|
||||||
|
await mockMapShell(page);
|
||||||
|
|
||||||
|
const mapState = draftMapState([
|
||||||
|
{
|
||||||
|
id: 1372,
|
||||||
|
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");
|
||||||
|
|
||||||
|
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 Navbar Bakery");
|
||||||
|
|
||||||
|
await page.getByRole("link", { name: "Home" }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole("heading", { name: "Leave with unsaved changes?" })).toBeVisible();
|
||||||
|
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 Navbar Bakery");
|
||||||
|
|
||||||
|
await page.getByRole("link", { name: "Home" }).click();
|
||||||
|
await confirmSlide(page);
|
||||||
|
await expect(page).toHaveURL(/\/$/);
|
||||||
|
});
|
||||||
|
|
||||||
test("admin publish saves pending map edits before publishing", async ({ page }) => {
|
test("admin publish saves pending map edits before publishing", async ({ page }) => {
|
||||||
await mockMapShell(page);
|
await mockMapShell(page);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user