Compare commits

...

3 Commits

Author SHA1 Message Date
Nico
a2db8cd655 fix: compact map state cards 2026-06-04 17:59:24 -07:00
Nico
5f1eb30080 fix: stabilize map history controls 2026-06-04 17:47:29 -07:00
Nico
11979af190 fix: keep selected map areas on top 2026-06-04 17:37:19 -07:00
5 changed files with 104 additions and 32 deletions

View File

@ -55,6 +55,21 @@ export default function LocationMapCanvas({
const objectDragHistoryCapturedRef = useRef(false);
const suppressPanClickRef = useRef(false);
const [isPanning, setIsPanning] = useState(false);
const orderedObjects = useMemo(() => {
if (!selectedObjectKey) return objects;
const selectedObjects = [];
const remainingObjects = [];
objects.forEach((object) => {
if (getObjectKey(object) === selectedObjectKey) {
selectedObjects.push(object);
return;
}
remainingObjects.push(object);
});
return [...remainingObjects, ...selectedObjects];
}, [objects, selectedObjectKey]);
const zoneItemSummary = useMemo(() => {
const assignedCounts = new Map();
const visibleItemsByZone = new Map();
@ -324,13 +339,7 @@ export default function LocationMapCanvas({
{mode === "edit" ? (
<rect className="location-map-grid" width={mapSize.width} height={mapSize.height} />
) : null}
{(selectedObjectKey
? [
...objects.filter((object) => getObjectKey(object) !== selectedObjectKey),
...objects.filter((object) => getObjectKey(object) === selectedObjectKey),
]
: objects
).map(renderMapObject)}
{orderedObjects.map(renderMapObject)}
</svg>
{emptyCanvasMessage ? (
<div

View File

@ -11,13 +11,9 @@ export function LocationMapMessageState({ message, location, status = "Loading",
<main className="location-map-workspace location-map-message-workspace">
<section className="location-map-setup location-map-message-card" aria-live="polite">
<h2>{status}</h2>
<div className="location-map-setup-rows" role="group" aria-label="Map message status">
<div className="location-map-setup-rows" role="group" aria-label="Map message details">
<div className="location-map-setup-row">
<span>Status</span>
<strong>{status}</strong>
</div>
<div className="location-map-setup-row">
<span>Detail</span>
<span>Message</span>
<strong>{message}</strong>
</div>
</div>
@ -34,13 +30,9 @@ export function LocationMapLoadErrorState({ location, loadError, onBack, onRetry
<main className="location-map-workspace">
<section className="location-map-setup location-map-load-error" role="alert" aria-live="polite">
<h2>Map Unavailable</h2>
<div className="location-map-setup-rows" role="group" aria-label="Map load status">
<div className="location-map-setup-rows" role="group" aria-label="Map load details">
<div className="location-map-setup-row">
<span>Status</span>
<strong>Load failed</strong>
</div>
<div className="location-map-setup-row">
<span>Detail</span>
<span>Error</span>
<strong>{loadError}</strong>
</div>
</div>

View File

@ -104,6 +104,7 @@ export default function LocationMapToolbar({
<div
className={`location-map-history-buttons ${canUseHistoryControls ? "" : "is-inactive"}`}
aria-label="Edit history"
aria-hidden={!canUseHistoryControls}
>
<button
type="button"

View File

@ -184,6 +184,11 @@
gap: 0.3rem;
}
.location-map-history-buttons.is-inactive {
visibility: hidden;
pointer-events: none;
}
.location-map-history-buttons button {
min-width: 40px;
padding-inline: 0.55rem;

View File

@ -465,9 +465,10 @@ test("map message prompts for a household with compact status rows", async ({ pa
await page.goto("/stores/100/locations/10/map");
await expect(page.getByRole("heading", { name: "Select Household" })).toBeVisible();
const messageStatus = page.getByRole("group", { name: "Map message status" });
await expect(messageStatus.locator(".location-map-setup-row", { hasText: "Status" })).toContainText("Select Household");
await expect(messageStatus.locator(".location-map-setup-row", { hasText: "Detail" })).toContainText("Select a household to manage maps.");
const messageDetails = page.getByRole("group", { name: "Map message details" });
await expect(messageDetails.locator(".location-map-setup-row")).toHaveCount(1);
await expect(messageDetails.locator(".location-map-setup-row", { hasText: "Message" })).toContainText("Select a household to manage maps.");
await expect(messageDetails.locator(".location-map-setup-row", { hasText: "Status" })).toHaveCount(0);
await expect(page.locator(".location-map-message-card p")).toHaveCount(0);
await expect(page.getByLabel("Map controls")).toHaveCount(0);
});
@ -517,9 +518,10 @@ test("load failure shows retryable error instead of no-map setup", async ({ page
const errorCard = page.locator(".location-map-load-error");
await expect(errorCard.getByRole("heading", { name: "Map Unavailable" })).toBeVisible();
const loadStatus = errorCard.getByRole("group", { name: "Map load status" });
await expect(loadStatus.locator(".location-map-setup-row", { hasText: "Status" })).toContainText("Load failed");
await expect(loadStatus.locator(".location-map-setup-row", { hasText: "Detail" })).toContainText("Map service unavailable");
const loadDetails = errorCard.getByRole("group", { name: "Map load details" });
await expect(loadDetails.locator(".location-map-setup-row")).toHaveCount(1);
await expect(loadDetails.locator(".location-map-setup-row", { hasText: "Error" })).toContainText("Map service unavailable");
await expect(loadDetails.locator(".location-map-setup-row", { hasText: "Status" })).toHaveCount(0);
await expect(errorCard.locator("p")).toHaveCount(0);
await expect(page.getByText("Load Error")).toBeVisible();
await expect(page.getByRole("heading", { name: "No Map" })).toHaveCount(0);
@ -585,9 +587,10 @@ test("loading map keeps location context visible", async ({ page }) => {
await expect(page.getByText("Eastvale")).toBeVisible();
await expect(page.getByRole("button", { name: "Back to grocery list" })).toBeVisible();
await expect(page.locator(".location-map-status")).toHaveText("Loading");
const messageStatus = page.getByRole("group", { name: "Map message status" });
await expect(messageStatus.locator(".location-map-setup-row", { hasText: "Status" })).toContainText("Loading");
await expect(messageStatus.locator(".location-map-setup-row", { hasText: "Detail" })).toContainText("Loading map...");
const messageDetails = page.getByRole("group", { name: "Map message details" });
await expect(messageDetails.locator(".location-map-setup-row")).toHaveCount(1);
await expect(messageDetails.locator(".location-map-setup-row", { hasText: "Message" })).toContainText("Loading map...");
await expect(messageDetails.locator(".location-map-setup-row", { hasText: "Status" })).toHaveCount(0);
await expect(page.locator(".location-map-message-card p")).toHaveCount(0);
releaseMapResponse();
@ -1391,15 +1394,15 @@ test("mobile keeps draft preview status compact in the topbar", async ({ page })
expect(modeBox.width).toBeLessThanOrEqual(126);
expect(reservedHistoryBox.width).toBeGreaterThanOrEqual(82);
expect(reservedHistoryBox.x).toBeGreaterThan(modeBox.x + modeBox.width - 1);
await expect(historyActions.getByRole("button", { name: "Undo" })).toBeVisible();
await expect(historyActions.getByRole("button", { name: "Redo" })).toBeVisible();
await expect(historyActions.getByRole("button", { name: "Undo" })).toBeDisabled();
await expect(historyActions.getByRole("button", { name: "Redo" })).toBeDisabled();
await expect(historyActions).toHaveCSS("visibility", "hidden");
await expect(historyActions.locator('button[aria-label="Undo"]')).toBeDisabled();
await expect(historyActions.locator('button[aria-label="Redo"]')).toBeDisabled();
expect(zoomBox.y).toBeLessThan(modeBox.y + modeBox.height);
expect(zoomBox.x).toBeGreaterThan(reservedHistoryBox.x + reservedHistoryBox.width - 1);
expect(zoomBox.x + zoomBox.width).toBeLessThanOrEqual(toolbarBox.x + toolbarBox.width + 1);
await page.getByRole("button", { name: "Edit Draft" }).click();
await expect(historyActions).toHaveCSS("visibility", "visible");
await expect(historyActions.getByRole("button", { name: "Undo" })).toBeVisible();
await expect(historyActions.getByRole("button", { name: "Redo" })).toBeVisible();
const editToolbarBox = await toolbar.boundingBox();
@ -1778,6 +1781,68 @@ test("admin locked map areas hide resize affordances", async ({ page }) => {
await expect(page.locator(".location-map-resize-handle")).toBeVisible();
});
test("admin selected overlapping map area renders above other areas", async ({ page }) => {
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1311,
location_map_id: 900,
zone_id: 501,
zone_name: "Bakery",
type: "zone",
label: "Bakery",
x: 40,
y: 40,
width: 260,
height: 180,
rotation: 0,
locked: false,
visible: true,
sort_order: 1,
},
{
id: 1312,
location_map_id: 900,
zone_id: 502,
zone_name: "Frozen Foods",
type: "zone",
label: "Frozen Foods",
x: 120,
y: 90,
width: 260,
height: 180,
rotation: 0,
locked: false,
visible: true,
sort_order: 2,
},
], 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();
const bakeryArea = page.getByRole("button", { name: "Map area Bakery" });
await bakeryArea.focus();
await page.keyboard.press("Enter");
await expect(bakeryArea).toHaveAttribute("aria-pressed", "true");
await expect.poll(async () =>
page.locator(".location-map-object").evaluateAll((nodes) =>
nodes.map((node) => node.getAttribute("data-object-key"))
)
).toEqual(["1312", "1311"]);
await expect(page.locator('[data-object-key="1311"] .location-map-resize-handle')).toBeVisible();
});
test("admin cleared map area labels keep linked zone action names", async ({ page }) => {
await mockMapShell(page);