Add store location map manager #20
@ -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);
|
||||
|
||||
@ -185,12 +185,15 @@ export default function LocationMapManager() {
|
||||
handleViewMode,
|
||||
} = useLocationMapViewControls({
|
||||
filters,
|
||||
future,
|
||||
hasUnsavedChanges,
|
||||
history,
|
||||
mapSize,
|
||||
mapState,
|
||||
mode,
|
||||
objects,
|
||||
previewDraft,
|
||||
saving,
|
||||
selectedObjectKey,
|
||||
setFuture,
|
||||
setHasUnsavedChanges,
|
||||
|
||||
@ -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 }) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user