fix: support map undo shortcuts
This commit is contained in:
parent
ff68413e62
commit
11bace3c1b
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import useMobileMapAutoFit from "./useMobileMapAutoFit";
|
import useMobileMapAutoFit from "./useMobileMapAutoFit";
|
||||||
import { getObjectKey } from "../lib/locationMapUtils";
|
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({
|
export default function useLocationMapViewControls({
|
||||||
filters,
|
filters,
|
||||||
|
future = [],
|
||||||
hasUnsavedChanges,
|
hasUnsavedChanges,
|
||||||
|
history = [],
|
||||||
mapSize,
|
mapSize,
|
||||||
mapState,
|
mapState,
|
||||||
mode,
|
mode,
|
||||||
objects,
|
objects,
|
||||||
previewDraft,
|
previewDraft,
|
||||||
|
saving,
|
||||||
selectedObjectKey,
|
selectedObjectKey,
|
||||||
setFuture,
|
setFuture,
|
||||||
setHasUnsavedChanges,
|
setHasUnsavedChanges,
|
||||||
@ -49,6 +56,13 @@ export default function useLocationMapViewControls({
|
|||||||
syncMapDraftFromState,
|
syncMapDraftFromState,
|
||||||
zoom,
|
zoom,
|
||||||
}) {
|
}) {
|
||||||
|
const shortcutControlsRef = useRef({
|
||||||
|
handleRedo: () => {},
|
||||||
|
handleUndo: () => {},
|
||||||
|
mode: "view",
|
||||||
|
saving: false,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedObjectKey) return;
|
if (!selectedObjectKey) return;
|
||||||
|
|
||||||
@ -68,41 +82,86 @@ export default function useLocationMapViewControls({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleUndo = useCallback(() => {
|
const handleUndo = useCallback(() => {
|
||||||
setHistory((previous) => {
|
if (history.length === 0) return;
|
||||||
if (previous.length === 0) return previous;
|
|
||||||
const priorSnapshot = getHistorySnapshot(previous[previous.length - 1], selectedObjectKey);
|
const priorSnapshot = getHistorySnapshot(history[history.length - 1], selectedObjectKey);
|
||||||
setFuture((nextFuture) => [
|
setFuture([
|
||||||
createHistorySnapshot(objects, selectedObjectKey),
|
createHistorySnapshot(objects, selectedObjectKey),
|
||||||
...nextFuture.slice(0, 19),
|
...future.slice(0, 19),
|
||||||
]);
|
]);
|
||||||
setObjects(priorSnapshot.objects);
|
setObjects(priorSnapshot.objects);
|
||||||
setSelectedObjectKey(priorSnapshot.selectedObjectKey);
|
setSelectedObjectKey(priorSnapshot.selectedObjectKey);
|
||||||
setHasUnsavedChanges(previous.length > 1);
|
setHasUnsavedChanges(history.length > 1);
|
||||||
return previous.slice(0, -1);
|
setHistory(history.slice(0, -1));
|
||||||
});
|
|
||||||
}, [
|
}, [
|
||||||
objects, selectedObjectKey, setFuture, setHasUnsavedChanges, setHistory,
|
future, history, objects, selectedObjectKey, setFuture, setHasUnsavedChanges, setHistory,
|
||||||
setObjects, setSelectedObjectKey,
|
setObjects, setSelectedObjectKey,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleRedo = useCallback(() => {
|
const handleRedo = useCallback(() => {
|
||||||
setFuture((previous) => {
|
if (future.length === 0) return;
|
||||||
if (previous.length === 0) return previous;
|
|
||||||
const nextSnapshot = getHistorySnapshot(previous[0], selectedObjectKey);
|
const nextSnapshot = getHistorySnapshot(future[0], selectedObjectKey);
|
||||||
setHistory((nextHistory) => [
|
setHistory([
|
||||||
...nextHistory.slice(-19),
|
...history.slice(-19),
|
||||||
createHistorySnapshot(objects, selectedObjectKey),
|
createHistorySnapshot(objects, selectedObjectKey),
|
||||||
]);
|
]);
|
||||||
setObjects(nextSnapshot.objects);
|
setObjects(nextSnapshot.objects);
|
||||||
setSelectedObjectKey(nextSnapshot.selectedObjectKey);
|
setSelectedObjectKey(nextSnapshot.selectedObjectKey);
|
||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
return previous.slice(1);
|
setFuture(future.slice(1));
|
||||||
});
|
|
||||||
}, [
|
}, [
|
||||||
objects, selectedObjectKey, setFuture, setHasUnsavedChanges, setHistory,
|
future, history, objects, selectedObjectKey, setFuture, setHasUnsavedChanges, setHistory,
|
||||||
setObjects, setSelectedObjectKey,
|
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(() => {
|
const handlePreviewDraft = useCallback(() => {
|
||||||
setMode("view");
|
setMode("view");
|
||||||
setLayersOpen(false);
|
setLayersOpen(false);
|
||||||
|
|||||||
@ -185,12 +185,15 @@ export default function LocationMapManager() {
|
|||||||
handleViewMode,
|
handleViewMode,
|
||||||
} = useLocationMapViewControls({
|
} = useLocationMapViewControls({
|
||||||
filters,
|
filters,
|
||||||
|
future,
|
||||||
hasUnsavedChanges,
|
hasUnsavedChanges,
|
||||||
|
history,
|
||||||
mapSize,
|
mapSize,
|
||||||
mapState,
|
mapState,
|
||||||
mode,
|
mode,
|
||||||
objects,
|
objects,
|
||||||
previewDraft,
|
previewDraft,
|
||||||
|
saving,
|
||||||
selectedObjectKey,
|
selectedObjectKey,
|
||||||
setFuture,
|
setFuture,
|
||||||
setHasUnsavedChanges,
|
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.locator(".location-map-status")).toHaveText("Unsaved Draft");
|
||||||
await expect(page.getByRole("button", { name: "Save Draft" })).toBeEnabled();
|
await expect(page.getByRole("button", { name: "Save Draft" })).toBeEnabled();
|
||||||
await expect(page.getByLabel("Linked zone")).toHaveValue("");
|
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 }) => {
|
test("admin can recover hidden map areas from the empty canvas", async ({ page }) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user