From bff830dd4bbb8b98ab5b0e164c2ed8701045d3dd Mon Sep 17 00:00:00 2001 From: Nico Date: Wed, 3 Jun 2026 17:29:57 -0700 Subject: [PATCH] fix: bound map object numeric edits --- .../maps/LocationMapBottomSheet.jsx | 19 +++++++ frontend/src/pages/LocationMapManager.jsx | 1 + frontend/tests/location-map-manager.spec.ts | 55 +++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/frontend/src/components/maps/LocationMapBottomSheet.jsx b/frontend/src/components/maps/LocationMapBottomSheet.jsx index f9cddfc..b6b8a6c 100644 --- a/frontend/src/components/maps/LocationMapBottomSheet.jsx +++ b/frontend/src/components/maps/LocationMapBottomSheet.jsx @@ -32,6 +32,7 @@ export default function LocationMapBottomSheet({ savingAction, hasUnsavedChanges, mapState, + mapSize, onAddObject, onUndo, onRedo, @@ -75,6 +76,12 @@ export default function LocationMapBottomSheet({ const changedLayerCount = DISPLAY_CONTROLS.filter(([key]) => filters[key] !== DEFAULT_MAP_FILTERS[key]).length; const unmappedItemPreview = visibleUnmappedItems.slice(0, 8); const hiddenUnmappedItemCount = visibleUnmappedItems.length - unmappedItemPreview.length; + const selectedWidth = Number(selectedObject?.width) || 40; + const selectedHeight = Number(selectedObject?.height) || 40; + const maxX = Math.max(0, Math.round((mapSize?.width || selectedWidth) - selectedWidth)); + const maxY = Math.max(0, Math.round((mapSize?.height || selectedHeight) - selectedHeight)); + const maxWidth = Math.max(40, Math.round(mapSize?.width || selectedWidth)); + const maxHeight = Math.max(40, Math.round(mapSize?.height || selectedHeight)); const saveDraftLabel = savingAction === "save" ? "Saving..." : "Save Draft"; const publishLabel = savingAction === "publish" ? "Publishing..." : "Publish"; const showSelectedZoneItems = () => { @@ -234,6 +241,9 @@ export default function LocationMapBottomSheet({ X onObjectField("x", Number(event.target.value))} @@ -243,6 +253,9 @@ export default function LocationMapBottomSheet({ Y onObjectField("y", Number(event.target.value))} @@ -252,6 +265,9 @@ export default function LocationMapBottomSheet({ W onObjectField("width", Number(event.target.value))} @@ -261,6 +277,9 @@ export default function LocationMapBottomSheet({ H onObjectField("height", Number(event.target.value))} diff --git a/frontend/src/pages/LocationMapManager.jsx b/frontend/src/pages/LocationMapManager.jsx index 330a1a4..e351a0b 100644 --- a/frontend/src/pages/LocationMapManager.jsx +++ b/frontend/src/pages/LocationMapManager.jsx @@ -543,6 +543,7 @@ export default function LocationMapManager() { savingAction={savingAction} hasUnsavedChanges={hasUnsavedChanges} mapState={mapState} + mapSize={mapSize} onAddObject={handleAddObject} onUndo={handleUndo} onRedo={handleRedo} diff --git a/frontend/tests/location-map-manager.spec.ts b/frontend/tests/location-map-manager.spec.ts index 5d355e8..5e954fa 100644 --- a/frontend/tests/location-map-manager.spec.ts +++ b/frontend/tests/location-map-manager.spec.ts @@ -596,6 +596,61 @@ test("admin selecting an object does not mark a draft dirty until it changes", a await expect(page.getByRole("button", { name: "Undo" })).toBeEnabled(); }); +test("admin object numeric fields expose map bounds and clamp edits", async ({ page }) => { + await mockMapShell(page); + + const mapState = draftMapState([ + { + id: 1352, + 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(); + await page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first().click(); + + const xInput = page.getByRole("spinbutton", { name: "X" }); + const yInput = page.getByRole("spinbutton", { name: "Y" }); + const widthInput = page.getByRole("spinbutton", { name: "W" }); + const heightInput = page.getByRole("spinbutton", { name: "H" }); + + await expect(xInput).toHaveAttribute("min", "0"); + await expect(xInput).toHaveAttribute("max", "740"); + await expect(yInput).toHaveAttribute("max", "540"); + await expect(widthInput).toHaveAttribute("min", "40"); + await expect(widthInput).toHaveAttribute("max", "1000"); + await expect(heightInput).toHaveAttribute("max", "700"); + + await xInput.fill("9999"); + await expect(xInput).toHaveValue("740"); + + await widthInput.fill("5000"); + await expect(widthInput).toHaveValue("1000"); + await expect(xInput).toHaveValue("0"); +}); + test("admin save progress locks draft controls", async ({ page }) => { await mockMapShell(page);