Add store location map manager #20
@ -77,7 +77,14 @@ export default function LocationMapBottomSheet({
|
|||||||
<button type="button" onClick={onAddObject}>Add Area</button>
|
<button type="button" onClick={onAddObject}>Add Area</button>
|
||||||
<button type="button" onClick={onPreviewDraft}>Preview Draft</button>
|
<button type="button" onClick={onPreviewDraft}>Preview Draft</button>
|
||||||
<button type="button" className="primary" onClick={onSaveDraft} disabled={saving || !hasUnsavedChanges}>Save Draft</button>
|
<button type="button" className="primary" onClick={onSaveDraft} disabled={saving || !hasUnsavedChanges}>Save Draft</button>
|
||||||
<button type="button" className="primary" onClick={onPublish} disabled={saving || !mapState?.draft_map}>Publish</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="primary"
|
||||||
|
onClick={onPublish}
|
||||||
|
disabled={saving || (!mapState?.draft_map && !hasUnsavedChanges)}
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</button>
|
||||||
<button type="button" onClick={onUndo} disabled={history.length === 0}>Undo</button>
|
<button type="button" onClick={onUndo} disabled={history.length === 0}>Undo</button>
|
||||||
<button type="button" onClick={onRedo} disabled={future.length === 0}>Redo</button>
|
<button type="button" onClick={onRedo} disabled={future.length === 0}>Redo</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -249,13 +249,25 @@ export default function LocationMapManager() {
|
|||||||
if (!activeHousehold?.id || !locationId) return;
|
if (!activeHousehold?.id || !locationId) return;
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
|
if (hasUnsavedChanges) {
|
||||||
|
await saveLocationMapDraft(activeHousehold.id, locationId, {
|
||||||
|
map: mapDraft,
|
||||||
|
objects: prepareObjectsForSave(objects),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const response = await publishLocationMapDraft(activeHousehold.id, locationId);
|
const response = await publishLocationMapDraft(activeHousehold.id, locationId);
|
||||||
setMapState(response.data);
|
setMapState(response.data);
|
||||||
setMode("view");
|
setMode("view");
|
||||||
setEditorTool("pan");
|
setEditorTool("pan");
|
||||||
setPreviewDraft(false);
|
setPreviewDraft(false);
|
||||||
syncMapDraftFromState(response.data, "view", false);
|
syncMapDraftFromState(response.data, "view", false);
|
||||||
toast.success("Published map", "Map is now visible to household members");
|
toast.success(
|
||||||
|
"Published map",
|
||||||
|
hasUnsavedChanges
|
||||||
|
? "Latest edits were saved and published"
|
||||||
|
: "Map is now visible to household members"
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Publish failed", getApiErrorMessage(error, "Failed to publish map"));
|
toast.error("Publish failed", getApiErrorMessage(error, "Failed to publish map"));
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -356,7 +356,7 @@ test("admin creates a map from zones, saves a draft, and publishes it", async ({
|
|||||||
await expect(page.locator(".location-map-object", { hasText: "Bread Wall" })).toBeVisible();
|
await expect(page.locator(".location-map-object", { hasText: "Bread Wall" })).toBeVisible();
|
||||||
await expect(page.locator(".location-map-object", { hasText: "Temporary Freezer" })).toHaveCount(0);
|
await expect(page.locator(".location-map-object", { hasText: "Temporary Freezer" })).toHaveCount(0);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Publish" }).click();
|
await page.getByRole("button", { name: "Publish", exact: true }).click();
|
||||||
await expect(page.locator(".location-map-status")).toHaveText("Published");
|
await expect(page.locator(".location-map-status")).toHaveText("Published");
|
||||||
await expect(page.getByRole("button", { name: "Edit Map" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Edit Map" })).toBeVisible();
|
||||||
await expect(page.getByText("loose batteries")).toHaveCount(0);
|
await expect(page.getByText("loose batteries")).toHaveCount(0);
|
||||||
@ -433,6 +433,89 @@ test("admin status follows the visible map when a saved draft exists", async ({
|
|||||||
await expect(page.locator(".location-map-object", { hasText: "Live Bakery" })).toBeVisible();
|
await expect(page.locator(".location-map-object", { hasText: "Live Bakery" })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("admin publish saves pending map edits before publishing", async ({ page }) => {
|
||||||
|
await mockMapShell(page);
|
||||||
|
|
||||||
|
let mapState = publishedMapState([
|
||||||
|
{
|
||||||
|
id: 1401,
|
||||||
|
location_map_id: 901,
|
||||||
|
zone_id: 501,
|
||||||
|
zone_name: "Bakery",
|
||||||
|
type: "zone",
|
||||||
|
label: "Live Bakery",
|
||||||
|
x: 40,
|
||||||
|
y: 40,
|
||||||
|
width: 260,
|
||||||
|
height: 160,
|
||||||
|
rotation: 0,
|
||||||
|
locked: false,
|
||||||
|
visible: true,
|
||||||
|
sort_order: 1,
|
||||||
|
},
|
||||||
|
], true);
|
||||||
|
let savedPayload: Record<string, unknown> | null = null;
|
||||||
|
let publishCalled = false;
|
||||||
|
|
||||||
|
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: 1450 + index,
|
||||||
|
location_map_id: 900,
|
||||||
|
zone_name: object.zone_id === 501 ? "Bakery" : null,
|
||||||
|
...object,
|
||||||
|
}));
|
||||||
|
mapState = draftAndPublishedMapState(savedObjects, mapState.published_objects, true);
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(mapState),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/households/1/locations/10/map/publish", async (route) => {
|
||||||
|
publishCalled = true;
|
||||||
|
mapState = publishedMapState(mapState.draft_objects, 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: "Edit Map" }).click();
|
||||||
|
await page.getByRole("button", { name: "Edit Objects" }).click();
|
||||||
|
await page.locator(".location-map-object", { hasText: "Live Bakery" }).locator("rect").first().click();
|
||||||
|
await page.getByRole("textbox", { name: "Label" }).fill("Quick Publish Bakery");
|
||||||
|
await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft");
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Publish", exact: true }).click();
|
||||||
|
|
||||||
|
await expect.poll(() => publishCalled).toBe(true);
|
||||||
|
expect(savedPayload).not.toBeNull();
|
||||||
|
expect(savedPayload?.objects).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
label: "Quick Publish Bakery",
|
||||||
|
zone_id: 501,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
await expect(page.locator(".location-map-status")).toHaveText("Published");
|
||||||
|
await expect(page.getByRole("button", { name: "Map area Quick Publish Bakery" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("button", { name: "Map area Live Bakery" })).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
test("viewer wraps long zone labels and keeps item counts visible", async ({ page }) => {
|
test("viewer wraps long zone labels and keeps item counts visible", async ({ page }) => {
|
||||||
await mockMapShell(page);
|
await mockMapShell(page);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user