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 90 additions and 2 deletions
Showing only changes of commit b739be29ba - Show all commits

View File

@ -1,3 +1,4 @@
import { useRef, useState } from "react";
import {
clientPointToMap,
getObjectKey,
@ -22,6 +23,11 @@ export default function LocationMapCanvas({
updateObjects,
}) {
const canEditObjects = mode === "edit" && editorTool === "edit";
const canDragPan = mode === "view" || editorTool === "pan";
const scrollRef = useRef(null);
const panDragRef = useRef(null);
const suppressPanClickRef = useRef(false);
const [isPanning, setIsPanning] = useState(false);
const startDrag = (event, object, type) => {
if (!canEditObjects || object.locked || !svgRef.current) return;
@ -71,17 +77,67 @@ export default function LocationMapCanvas({
const handleObjectClick = (event, object) => {
event.stopPropagation();
if (suppressPanClickRef.current) {
suppressPanClickRef.current = false;
return;
}
if (mode === "view" || canEditObjects) {
setSelectedObjectKey(getObjectKey(object));
}
};
const clearSelection = () => {
if (suppressPanClickRef.current) {
suppressPanClickRef.current = false;
return;
}
if (!dragState) {
setSelectedObjectKey(null);
}
};
const startPan = (event) => {
if (!canDragPan || !scrollRef.current || event.button !== 0) return;
const objectElement = event.target.closest?.(".location-map-object");
event.currentTarget.setPointerCapture?.(event.pointerId);
panDragRef.current = {
startX: event.clientX,
startY: event.clientY,
scrollLeft: scrollRef.current.scrollLeft,
scrollTop: scrollRef.current.scrollTop,
objectKey: objectElement?.getAttribute("data-object-key") || null,
moved: false,
};
setIsPanning(true);
};
const movePan = (event) => {
if (!panDragRef.current || !scrollRef.current) return;
const deltaX = event.clientX - panDragRef.current.startX;
const deltaY = event.clientY - panDragRef.current.startY;
if (Math.abs(deltaX) > 4 || Math.abs(deltaY) > 4) {
panDragRef.current.moved = true;
event.preventDefault();
}
scrollRef.current.scrollLeft = panDragRef.current.scrollLeft - deltaX;
scrollRef.current.scrollTop = panDragRef.current.scrollTop - deltaY;
};
const endPan = (event) => {
if (!panDragRef.current) return;
const wasMoved = panDragRef.current.moved;
const objectKey = panDragRef.current.objectKey;
if (!wasMoved && mode === "view" && objectKey) {
setSelectedObjectKey(objectKey);
suppressPanClickRef.current = true;
} else {
suppressPanClickRef.current = wasMoved;
}
panDragRef.current = null;
setIsPanning(false);
event.currentTarget.releasePointerCapture?.(event.pointerId);
};
const fittedLabel = (object, fallback) => {
const rawLabel = object.label || object.zone_name || fallback;
const maxCharacters = Math.max(8, Math.floor((Number(object.width) - 28) / 14));
@ -115,6 +171,7 @@ export default function LocationMapCanvas({
return (
<g
key={key}
data-object-key={key}
className={[
"location-map-object",
isSelected ? "is-selected" : "",
@ -165,9 +222,18 @@ export default function LocationMapCanvas({
"location-map-canvas-shell",
mode === "edit" ? "is-edit-mode" : "is-view-mode",
editorTool === "edit" ? "is-object-tool" : "is-pan-tool",
isPanning ? "is-panning" : "",
].filter(Boolean).join(" ")}
>
<div className="location-map-scroll">
<div
ref={scrollRef}
className="location-map-scroll"
onPointerDown={startPan}
onPointerMove={movePan}
onPointerUp={endPan}
onPointerCancel={endPan}
onPointerLeave={endPan}
>
<svg
ref={svgRef}
className={[

View File

@ -203,6 +203,15 @@
touch-action: pan-x pan-y;
}
.location-map-canvas-shell.is-pan-tool .location-map-scroll {
cursor: grab;
}
.location-map-canvas-shell.is-panning .location-map-scroll {
cursor: grabbing;
user-select: none;
}
.location-map-svg {
min-width: 320px;
min-height: 260px;

View File

@ -238,6 +238,19 @@ test("admin creates a map from zones, saves a draft, and publishes it", async ({
await expect(page.getByRole("button", { name: "Edit Objects" })).toBeVisible();
await expect(page.locator(".location-map-pin")).toHaveCount(0);
await page.getByRole("button", { name: "Pan Mode" }).click();
const bakeryPanObject = page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first();
const panBox = await bakeryPanObject.boundingBox();
expect(panBox).not.toBeNull();
if (!panBox) throw new Error("Bakery map object was not measurable");
await page.mouse.move(panBox.x + panBox.width / 2, panBox.y + panBox.height / 2);
await page.mouse.down();
await page.mouse.move(panBox.x + panBox.width / 2 - 80, panBox.y + panBox.height / 2 - 20, {
steps: 8,
});
await page.mouse.up();
await page.getByRole("button", { name: "Edit Objects" }).click();
const bakeryObject = page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first();
await bakeryObject.click();
await page.getByRole("textbox", { name: "Label" }).fill("Bread Wall");
@ -246,7 +259,7 @@ test("admin creates a map from zones, saves a draft, and publishes it", async ({
expect(savedPayload).not.toBeNull();
expect(savedPayload?.objects).toEqual(
expect.arrayContaining([
expect.objectContaining({ label: "Bread Wall", zone_id: 501 }),
expect.objectContaining({ label: "Bread Wall", zone_id: 501, x: 28, y: 28 }),
])
);