fix: recover hidden map areas

This commit is contained in:
Nico 2026-06-03 21:36:22 -07:00
parent 5b8b7ee2b8
commit daa283b160
4 changed files with 161 additions and 0 deletions

View File

@ -30,6 +30,7 @@ export default function LocationMapBottomSheet({
selectedObject,
selectedZoneItems,
visibleUnmappedItems,
hiddenAreaCount,
history,
future,
saving,
@ -51,6 +52,7 @@ export default function LocationMapBottomSheet({
onClearSelection,
onDuplicateObject,
onDeleteObject,
onShowHiddenAreas,
}) {
const selectedTitle = getMapObjectDisplayLabel(selectedObject);
const selectedAssignedItems = selectedObject?.zone_id
@ -82,6 +84,7 @@ export default function LocationMapBottomSheet({
const hiddenUnmappedItemCount = visibleUnmappedItems.length - unmappedItemPreview.length;
const selectedWidth = Number(selectedObject?.width) || 40;
const selectedHeight = Number(selectedObject?.height) || 40;
const hiddenAreaLabel = `Show Hidden Areas (${hiddenAreaCount})`;
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));
@ -217,6 +220,14 @@ export default function LocationMapBottomSheet({
</div>
) : null}
{showEditorControls && hiddenAreaCount > 0 ? (
<div className="location-map-hidden-areas">
<button type="button" onClick={onShowHiddenAreas} disabled={saving}>
{hiddenAreaLabel}
</button>
</div>
) : null}
{mode === "edit" && editorTool === "edit" && selectedObject ? (
<div className="location-map-object-form">
<label>
@ -254,6 +265,26 @@ export default function LocationMapBottomSheet({
))}
</select>
</label>
<div className="location-map-object-flags">
<label>
<input
type="checkbox"
checked={selectedObject.visible !== false}
disabled={saving}
onChange={(event) => onObjectField("visible", event.target.checked)}
/>
Visible
</label>
<label>
<input
type="checkbox"
checked={Boolean(selectedObject.locked)}
disabled={saving}
onChange={(event) => onObjectField("locked", event.target.checked)}
/>
Locked
</label>
</div>
<div className="location-map-object-grid">
<label>
X

View File

@ -81,6 +81,7 @@ export default function LocationMapManager() {
? itemsForZone(mapState?.items || [], selectedObject.zone_id, filters, username)
: [];
const visibleUnmappedItems = unmappedItems(mapState?.items || [], filters, username);
const hiddenAreaCount = objects.filter((object) => object.visible === false).length;
const shouldGuardLeave = canManage && hasUnsavedChanges;
const mapSize = {
@ -368,6 +369,17 @@ export default function LocationMapManager() {
setPendingDeleteObject(null);
};
const handleShowHiddenAreas = () => {
if (!canManage || hiddenAreaCount === 0) return;
remember();
updateObjects((currentObjects) =>
currentObjects.map((object) =>
object.visible === false ? { ...object, visible: true } : object
)
);
toast.success("Showed hidden areas", "Save the draft to keep this change.");
};
const handleObjectField = (field, value) => {
if (!selectedObject) return;
remember();
@ -567,6 +579,7 @@ export default function LocationMapManager() {
selectedObject={selectedObject}
selectedZoneItems={selectedZoneItems}
visibleUnmappedItems={visibleUnmappedItems}
hiddenAreaCount={hiddenAreaCount}
history={history}
future={future}
saving={saving}
@ -588,6 +601,7 @@ export default function LocationMapManager() {
onClearSelection={() => setSelectedObjectKey(null)}
onDuplicateObject={handleDuplicateObject}
onDeleteObject={requestDeleteObject}
onShowHiddenAreas={handleShowHiddenAreas}
/>
</>
)}

View File

@ -175,6 +175,7 @@
.location-map-tool-buttons button:disabled,
.location-map-zoom-controls button:disabled,
.location-map-editor-actions button:disabled,
.location-map-hidden-areas button:disabled,
.location-map-mobile-tool-switch button:disabled,
.location-map-setup-actions button:disabled {
opacity: 0.55;
@ -540,6 +541,22 @@
background: rgba(20, 184, 166, 0.24);
}
.location-map-hidden-areas {
margin-bottom: 0.75rem;
}
.location-map-hidden-areas button {
min-height: 40px;
width: 100%;
padding: 0.5rem 0.7rem;
border: 1px dashed rgba(96, 165, 250, 0.5);
border-radius: 8px;
background: rgba(59, 130, 246, 0.14);
color: #dbeafe;
font-weight: 900;
cursor: pointer;
}
.location-map-object-form {
display: grid;
gap: 0.6rem;
@ -563,6 +580,31 @@
color: #f8fafc;
}
.location-map-object-flags {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.45rem;
}
.location-map-object-flags label {
min-height: 40px;
display: flex;
align-items: center;
gap: 0.45rem;
padding: 0.35rem 0.5rem;
border: 1px solid var(--color-border-light);
border-radius: 8px;
background: rgba(15, 23, 34, 0.58);
color: #f8fafc;
}
.location-map-object-flags input {
min-height: 0;
width: 18px;
height: 18px;
padding: 0;
}
.location-map-object-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));

View File

@ -596,6 +596,80 @@ 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 can recover hidden map areas from the edit sheet", async ({ page }) => {
await mockMapShell(page);
let savedPayload: Record<string, unknown> | null = null;
let mapState = draftMapState([
{
id: 1354,
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.route("**/households/1/locations/10/map/draft", async (route) => {
savedPayload = await route.request().postDataJSON();
const savedObjects = (savedPayload.objects as Array<Record<string, unknown>>).map((object, index) => ({
id: 1370 + index,
location_map_id: 900,
zone_name: object.zone_id === 501 ? "Bakery" : null,
...object,
}));
mapState = draftMapState(savedObjects, true);
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.getByRole("button", { name: "Map area Bakery" }).click();
await expect(page.getByLabel("Visible")).toBeChecked();
await expect(page.getByLabel("Locked")).not.toBeChecked();
await page.getByLabel("Visible").click();
await expect(page.getByRole("button", { name: "Map area Bakery" })).toHaveCount(0);
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Edit Objects");
await expect(page.getByText("No visible map areas")).toBeVisible();
await expect(page.getByRole("button", { name: "Show Hidden Areas (1)" })).toBeVisible();
await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft");
await page.getByRole("button", { name: "Show Hidden Areas (1)" }).click();
await expect(page.getByRole("button", { name: "Map area Bakery" })).toBeVisible();
await expect(page.getByRole("button", { name: "Show Hidden Areas (1)" })).toHaveCount(0);
await page.getByRole("button", { name: "Save Draft" }).click();
await expect(page.locator(".location-map-status")).toHaveText("Draft");
expect(savedPayload).not.toBeNull();
expect((savedPayload?.objects as Array<Record<string, unknown>>)[0]).toEqual(
expect.objectContaining({ visible: true, zone_id: 501 })
);
});
test("admin cleared map area labels keep linked zone action names", async ({ page }) => {
await mockMapShell(page);