fix: avoid dirty map state on selection

This commit is contained in:
Nico 2026-06-03 13:49:33 -07:00
parent 04b538011e
commit 1eaddd8a07
2 changed files with 71 additions and 1 deletions

View File

@ -5,6 +5,8 @@ import {
itemsForZone,
} from "../../lib/locationMapUtils";
const DRAG_CHANGE_THRESHOLD = 2;
export default function LocationMapCanvas({
mode,
editorTool,
@ -26,6 +28,7 @@ export default function LocationMapCanvas({
const canDragPan = mode === "view" || editorTool === "pan";
const scrollRef = useRef(null);
const panDragRef = useRef(null);
const objectDragHistoryCapturedRef = useRef(false);
const suppressPanClickRef = useRef(false);
const [isPanning, setIsPanning] = useState(false);
@ -33,7 +36,7 @@ export default function LocationMapCanvas({
if (!canEditObjects || object.locked || !svgRef.current) return;
event.stopPropagation();
event.preventDefault();
remember();
objectDragHistoryCapturedRef.current = false;
const point = clientPointToMap(event, svgRef.current, mapSize);
event.currentTarget.setPointerCapture?.(event.pointerId);
setSelectedObjectKey(getObjectKey(object));
@ -51,6 +54,15 @@ export default function LocationMapCanvas({
const point = clientPointToMap(event, svgRef.current, mapSize);
const deltaX = point.x - dragState.startPoint.x;
const deltaY = point.y - dragState.startPoint.y;
const hasMeaningfulChange =
Math.abs(deltaX) >= DRAG_CHANGE_THRESHOLD ||
Math.abs(deltaY) >= DRAG_CHANGE_THRESHOLD;
if (!hasMeaningfulChange) return;
if (!objectDragHistoryCapturedRef.current) {
remember();
objectDragHistoryCapturedRef.current = true;
}
updateObjects((currentObjects) =>
currentObjects.map((object) => {
@ -72,6 +84,7 @@ export default function LocationMapCanvas({
};
const handlePointerUp = () => {
objectDragHistoryCapturedRef.current = false;
setDragState(null);
};

View File

@ -433,6 +433,63 @@ test("admin status follows the visible map when a saved draft exists", async ({
await expect(page.locator(".location-map-object", { hasText: "Live Bakery" })).toBeVisible();
});
test("admin selecting an object does not mark a draft dirty until it changes", async ({ page }) => {
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1351,
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 Objects" }).click();
const bakeryObject = page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first();
await bakeryObject.click();
await expect(page.getByRole("textbox", { name: "Label" })).toBeVisible();
await expect(page.locator(".location-map-status")).toHaveText("Draft");
await expect(page.getByRole("button", { name: "Save Draft" })).toBeDisabled();
await expect(page.getByRole("button", { name: "Undo" })).toBeDisabled();
const objectBox = await bakeryObject.boundingBox();
expect(objectBox).not.toBeNull();
if (!objectBox) throw new Error("Bakery map object was not measurable");
await page.mouse.move(objectBox.x + objectBox.width / 2, objectBox.y + objectBox.height / 2);
await page.mouse.down();
await page.mouse.move(objectBox.x + objectBox.width / 2 + 50, objectBox.y + objectBox.height / 2 + 30, {
steps: 8,
});
await page.mouse.up();
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: "Undo" })).toBeEnabled();
});
test("admin publish saves pending map edits before publishing", async ({ page }) => {
await mockMapShell(page);