Add store location map manager #20

Open
nalalangan wants to merge 206 commits from feature/location-map-manager into feature/store-selector-modal
3 changed files with 106 additions and 27 deletions
Showing only changes of commit 11bace3c1b - Show all commits

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect } from "react";
import { useCallback, useEffect, useRef } from "react";
import useMobileMapAutoFit from "./useMobileMapAutoFit";
import { getObjectKey } from "../lib/locationMapUtils";
@ -27,14 +27,21 @@ function createHistorySnapshot(objects, selectedObjectKey) {
};
}
function isTextEditingTarget(target) {
return Boolean(target?.closest?.("input, textarea, select, [contenteditable]"));
}
export default function useLocationMapViewControls({
filters,
future = [],
hasUnsavedChanges,
history = [],
mapSize,
mapState,
mode,
objects,
previewDraft,
saving,
selectedObjectKey,
setFuture,
setHasUnsavedChanges,
@ -49,6 +56,13 @@ export default function useLocationMapViewControls({
syncMapDraftFromState,
zoom,
}) {
const shortcutControlsRef = useRef({
handleRedo: () => {},
handleUndo: () => {},
mode: "view",
saving: false,
});
useEffect(() => {
if (!selectedObjectKey) return;
@ -68,41 +82,86 @@ export default function useLocationMapViewControls({
});
const handleUndo = useCallback(() => {
setHistory((previous) => {
if (previous.length === 0) return previous;
const priorSnapshot = getHistorySnapshot(previous[previous.length - 1], selectedObjectKey);
setFuture((nextFuture) => [
createHistorySnapshot(objects, selectedObjectKey),
...nextFuture.slice(0, 19),
]);
setObjects(priorSnapshot.objects);
setSelectedObjectKey(priorSnapshot.selectedObjectKey);
setHasUnsavedChanges(previous.length > 1);
return previous.slice(0, -1);
});
if (history.length === 0) return;
const priorSnapshot = getHistorySnapshot(history[history.length - 1], selectedObjectKey);
setFuture([
createHistorySnapshot(objects, selectedObjectKey),
...future.slice(0, 19),
]);
setObjects(priorSnapshot.objects);
setSelectedObjectKey(priorSnapshot.selectedObjectKey);
setHasUnsavedChanges(history.length > 1);
setHistory(history.slice(0, -1));
}, [
objects, selectedObjectKey, setFuture, setHasUnsavedChanges, setHistory,
future, history, objects, selectedObjectKey, setFuture, setHasUnsavedChanges, setHistory,
setObjects, setSelectedObjectKey,
]);
const handleRedo = useCallback(() => {
setFuture((previous) => {
if (previous.length === 0) return previous;
const nextSnapshot = getHistorySnapshot(previous[0], selectedObjectKey);
setHistory((nextHistory) => [
...nextHistory.slice(-19),
createHistorySnapshot(objects, selectedObjectKey),
]);
setObjects(nextSnapshot.objects);
setSelectedObjectKey(nextSnapshot.selectedObjectKey);
setHasUnsavedChanges(true);
return previous.slice(1);
});
if (future.length === 0) return;
const nextSnapshot = getHistorySnapshot(future[0], selectedObjectKey);
setHistory([
...history.slice(-19),
createHistorySnapshot(objects, selectedObjectKey),
]);
setObjects(nextSnapshot.objects);
setSelectedObjectKey(nextSnapshot.selectedObjectKey);
setHasUnsavedChanges(true);
setFuture(future.slice(1));
}, [
objects, selectedObjectKey, setFuture, setHasUnsavedChanges, setHistory,
future, history, objects, selectedObjectKey, setFuture, setHasUnsavedChanges, setHistory,
setObjects, setSelectedObjectKey,
]);
shortcutControlsRef.current = {
handleRedo,
handleUndo,
mode,
saving,
};
useEffect(() => {
if (typeof window === "undefined") {
return undefined;
}
const handleKeyDown = (event) => {
if (
event.defaultPrevented ||
event.altKey ||
!(event.ctrlKey || event.metaKey) ||
isTextEditingTarget(event.target)
) {
return;
}
const controls = shortcutControlsRef.current;
if (controls.mode !== "edit" || controls.saving) {
return;
}
const key = event.key.toLowerCase();
const wantsUndo = key === "z" && !event.shiftKey;
const wantsRedo = key === "y" || (key === "z" && event.shiftKey);
if (wantsUndo) {
event.preventDefault();
controls.handleUndo();
return;
}
if (wantsRedo) {
event.preventDefault();
controls.handleRedo();
}
};
window.addEventListener("keydown", handleKeyDown, true);
return () => window.removeEventListener("keydown", handleKeyDown, true);
}, []);
const handlePreviewDraft = useCallback(() => {
setMode("view");
setLayersOpen(false);

View File

@ -185,12 +185,15 @@ export default function LocationMapManager() {
handleViewMode,
} = useLocationMapViewControls({
filters,
future,
hasUnsavedChanges,
history,
mapSize,
mapState,
mode,
objects,
previewDraft,
saving,
selectedObjectKey,
setFuture,
setHasUnsavedChanges,

View File

@ -1806,6 +1806,23 @@ test("admin undoing the only draft change clears unsaved state", async ({ page }
await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft");
await expect(page.getByRole("button", { name: "Save Draft" })).toBeEnabled();
await expect(page.getByLabel("Linked zone")).toHaveValue("");
await page.getByRole("button", { name: "Undo" }).focus();
await page.keyboard.down("Control");
await page.keyboard.press("KeyZ");
await page.keyboard.up("Control");
await expect(page.locator(".location-map-status")).toHaveText("Draft");
await expect(page.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await expect(page.getByLabel("Linked zone")).toHaveValue("501");
await expect(page.getByRole("button", { name: "Redo" })).toBeEnabled();
await page.getByRole("button", { name: "Redo" }).focus();
await page.keyboard.down("Control");
await page.keyboard.press("KeyY");
await page.keyboard.up("Control");
await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft");
await expect(page.getByRole("button", { name: "Save Draft" })).toBeEnabled();
await expect(page.getByLabel("Linked zone")).toHaveValue("");
});
test("admin can recover hidden map areas from the empty canvas", async ({ page }) => {