Add store location map manager #20
@ -1,3 +1,5 @@
|
||||
import { clampMapZoom } from "../../lib/locationMapUtils";
|
||||
|
||||
export default function LocationMapToolbar({
|
||||
mode,
|
||||
editorTool,
|
||||
@ -66,7 +68,7 @@ export default function LocationMapToolbar({
|
||||
<div className="location-map-zoom-controls" aria-label="Zoom controls">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setZoom((value) => Math.max(0.45, value - 0.15))}
|
||||
onClick={() => setZoom((value) => Number(clampMapZoom(value - 0.15).toFixed(2)))}
|
||||
disabled={!hasAnyMap}
|
||||
aria-label="Zoom out"
|
||||
>
|
||||
@ -75,7 +77,7 @@ export default function LocationMapToolbar({
|
||||
<span>{Math.round(zoom * 100)}%</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setZoom((value) => Math.min(1.6, value + 0.15))}
|
||||
onClick={() => setZoom((value) => Number(clampMapZoom(value + 0.15).toFixed(2)))}
|
||||
disabled={!hasAnyMap}
|
||||
aria-label="Zoom in"
|
||||
>
|
||||
|
||||
@ -5,6 +5,9 @@ export const DEFAULT_MAP_SIZE = {
|
||||
height: 700,
|
||||
};
|
||||
|
||||
export const MIN_MAP_ZOOM = 0.3;
|
||||
export const MAX_MAP_ZOOM = 1.6;
|
||||
|
||||
export const DEFAULT_MAP_FILTERS = {
|
||||
showZones: true,
|
||||
showLabels: true,
|
||||
@ -178,6 +181,10 @@ export function clientPointToMap(event, svgElement, mapSize) {
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
export function clampMapZoom(value) {
|
||||
return Math.max(MIN_MAP_ZOOM, Math.min(MAX_MAP_ZOOM, value));
|
||||
}
|
||||
|
||||
export function clampObjectToMap(object, mapSize) {
|
||||
const width = Math.max(40, Math.min(object.width, mapSize.width));
|
||||
const height = Math.max(40, Math.min(object.height, mapSize.height));
|
||||
|
||||
@ -21,6 +21,7 @@ import getApiErrorMessage from "../lib/getApiErrorMessage";
|
||||
import {
|
||||
DEFAULT_MAP_FILTERS,
|
||||
DEFAULT_MAP_SIZE,
|
||||
clampMapZoom,
|
||||
clampObjectToMap,
|
||||
createClientObject,
|
||||
getMapStatus,
|
||||
@ -427,12 +428,14 @@ export default function LocationMapManager() {
|
||||
};
|
||||
|
||||
const handleFitMap = () => {
|
||||
const availableWidth = typeof window === "undefined"
|
||||
? DEFAULT_MAP_SIZE.width
|
||||
: window.innerWidth >= 840
|
||||
? window.innerWidth - 420
|
||||
: window.innerWidth - 24;
|
||||
const nextZoom = Math.max(0.45, Math.min(1.6, (availableWidth - 24) / mapSize.width));
|
||||
const scrollElement = svgRef.current?.closest(".location-map-scroll");
|
||||
const fallbackWidth = typeof window === "undefined" ? DEFAULT_MAP_SIZE.width : window.innerWidth;
|
||||
const fallbackHeight = typeof window === "undefined" ? DEFAULT_MAP_SIZE.height : window.innerHeight;
|
||||
const availableWidth = Math.max(280, (scrollElement?.clientWidth || fallbackWidth) - 24);
|
||||
const availableHeight = Math.max(220, (scrollElement?.clientHeight || fallbackHeight) - 24);
|
||||
const nextZoom = clampMapZoom(
|
||||
Math.min(availableWidth / mapSize.width, availableHeight / mapSize.height)
|
||||
);
|
||||
setZoom(Number(nextZoom.toFixed(2)));
|
||||
};
|
||||
|
||||
|
||||
@ -1082,6 +1082,55 @@ test("viewer handles empty and many-item zone panels without default pins", asyn
|
||||
await expect(page.locator(".location-map-pin")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("mobile fit zoom fits the map into the visible canvas", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await mockMapShell(page);
|
||||
|
||||
const mapState = publishedMapState([
|
||||
{
|
||||
id: 1581,
|
||||
location_map_id: 901,
|
||||
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: "Fit" }).click();
|
||||
|
||||
const zoomText = page.locator(".location-map-zoom-controls span");
|
||||
await expect.poll(async () => {
|
||||
const value = await zoomText.textContent();
|
||||
return Number(value?.replace("%", "") || 0);
|
||||
}).toBeLessThan(45);
|
||||
|
||||
const scrollBox = await page.locator(".location-map-scroll").boundingBox();
|
||||
const svgBox = await page.locator(".location-map-svg").boundingBox();
|
||||
expect(scrollBox).not.toBeNull();
|
||||
expect(svgBox).not.toBeNull();
|
||||
if (!scrollBox || !svgBox) throw new Error("Mobile fitted map was not measurable");
|
||||
expect(svgBox.width).toBeLessThanOrEqual(scrollBox.width + 1);
|
||||
expect(svgBox.height).toBeLessThanOrEqual(scrollBox.height + 1);
|
||||
});
|
||||
|
||||
test("mobile editor uses bottom sheet controls instead of desktop object toolbar", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await mockMapShell(page);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user