fix: ignore stale map draft action responses
This commit is contained in:
parent
27a50d599c
commit
9772d7e043
@ -1,4 +1,4 @@
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import {
|
||||
createBlankLocationMap,
|
||||
createLocationMapFromZones,
|
||||
@ -24,10 +24,20 @@ export default function useLocationMapDraftActions({
|
||||
syncMapDraftFromState,
|
||||
toast,
|
||||
}) {
|
||||
const latestScopeRef = useRef({ activeHouseholdId, locationId });
|
||||
const selectedObjectIndex = selectedObjectKey
|
||||
? objects.findIndex((object) => getObjectKey(object) === selectedObjectKey)
|
||||
: -1;
|
||||
|
||||
useEffect(() => {
|
||||
latestScopeRef.current = { activeHouseholdId, locationId };
|
||||
}, [activeHouseholdId, locationId]);
|
||||
|
||||
const isCurrentScope = useCallback(() => (
|
||||
String(latestScopeRef.current.activeHouseholdId ?? "") === String(activeHouseholdId ?? "") &&
|
||||
String(latestScopeRef.current.locationId ?? "") === String(locationId ?? "")
|
||||
), [activeHouseholdId, locationId]);
|
||||
|
||||
const applyDraftResponse = useCallback((response, nextMode = "edit", nextPreviewDraft = true, options = {}) => {
|
||||
setMapState(response.data);
|
||||
setMode(nextMode);
|
||||
@ -40,30 +50,40 @@ export default function useLocationMapDraftActions({
|
||||
beginSaving("create-blank");
|
||||
try {
|
||||
const response = await createBlankLocationMap(activeHouseholdId, locationId, mapDraft);
|
||||
if (!isCurrentScope()) return;
|
||||
applyDraftResponse(response);
|
||||
setEditorTool("edit");
|
||||
toast.success("Created map", "Blank draft map created");
|
||||
} catch (error) {
|
||||
if (!isCurrentScope()) return;
|
||||
toast.error("Create map failed", getApiErrorMessage(error, "Failed to create map"));
|
||||
} finally {
|
||||
endSaving();
|
||||
}
|
||||
}, [activeHouseholdId, applyDraftResponse, beginSaving, endSaving, locationId, mapDraft, setEditorTool, toast]);
|
||||
}, [
|
||||
activeHouseholdId, applyDraftResponse, beginSaving, endSaving, isCurrentScope,
|
||||
locationId, mapDraft, setEditorTool, toast,
|
||||
]);
|
||||
|
||||
const handleCreateFromZones = useCallback(async () => {
|
||||
if (!activeHouseholdId || !locationId) return;
|
||||
beginSaving("create-zones");
|
||||
try {
|
||||
const response = await createLocationMapFromZones(activeHouseholdId, locationId, mapDraft);
|
||||
if (!isCurrentScope()) return;
|
||||
applyDraftResponse(response);
|
||||
setEditorTool("edit");
|
||||
toast.success("Created starter map", "Zones were added as editable rectangles");
|
||||
} catch (error) {
|
||||
if (!isCurrentScope()) return;
|
||||
toast.error("Create map failed", getApiErrorMessage(error, "Failed to create map from zones"));
|
||||
} finally {
|
||||
endSaving();
|
||||
}
|
||||
}, [activeHouseholdId, applyDraftResponse, beginSaving, endSaving, locationId, mapDraft, setEditorTool, toast]);
|
||||
}, [
|
||||
activeHouseholdId, applyDraftResponse, beginSaving, endSaving, isCurrentScope,
|
||||
locationId, mapDraft, setEditorTool, toast,
|
||||
]);
|
||||
|
||||
const handleSaveDraft = useCallback(async () => {
|
||||
if (!activeHouseholdId || !locationId) return;
|
||||
@ -73,15 +93,17 @@ export default function useLocationMapDraftActions({
|
||||
map: mapDraft,
|
||||
objects: prepareObjectsForSave(objects),
|
||||
});
|
||||
if (!isCurrentScope()) return;
|
||||
applyDraftResponse(response, "edit", true, { preserveSelectedIndex: selectedObjectIndex });
|
||||
toast.success("Saved draft", "Map draft saved");
|
||||
} catch (error) {
|
||||
if (!isCurrentScope()) return;
|
||||
toast.error("Save draft failed", getApiErrorMessage(error, "Failed to save map draft"));
|
||||
} finally {
|
||||
endSaving();
|
||||
}
|
||||
}, [
|
||||
activeHouseholdId, applyDraftResponse, beginSaving, endSaving, locationId,
|
||||
activeHouseholdId, applyDraftResponse, beginSaving, endSaving, isCurrentScope, locationId,
|
||||
mapDraft, objects, selectedObjectIndex, toast,
|
||||
]);
|
||||
|
||||
@ -95,11 +117,13 @@ export default function useLocationMapDraftActions({
|
||||
map: mapDraft,
|
||||
objects: prepareObjectsForSave(objects),
|
||||
});
|
||||
if (!isCurrentScope()) return;
|
||||
savedPendingDraft = true;
|
||||
applyDraftResponse(saveResponse, "edit", true, { preserveSelectedIndex: selectedObjectIndex });
|
||||
}
|
||||
|
||||
const response = await publishLocationMapDraft(activeHouseholdId, locationId);
|
||||
if (!isCurrentScope()) return;
|
||||
applyDraftResponse(response, "view", false);
|
||||
setEditorTool("pan");
|
||||
toast.success(
|
||||
@ -109,13 +133,14 @@ export default function useLocationMapDraftActions({
|
||||
: "Map is now visible to household members"
|
||||
);
|
||||
} catch (error) {
|
||||
if (!isCurrentScope()) return;
|
||||
const message = getApiErrorMessage(error, "Failed to publish map");
|
||||
toast.error("Publish failed", savedPendingDraft ? `${message}. Draft changes were saved.` : message);
|
||||
} finally {
|
||||
endSaving();
|
||||
}
|
||||
}, [
|
||||
activeHouseholdId, applyDraftResponse, beginSaving, endSaving, hasUnsavedChanges,
|
||||
activeHouseholdId, applyDraftResponse, beginSaving, endSaving, hasUnsavedChanges, isCurrentScope,
|
||||
locationId, mapDraft, objects, selectedObjectIndex, setEditorTool, toast,
|
||||
]);
|
||||
|
||||
|
||||
@ -591,6 +591,127 @@ test("ignores stale map responses after switching locations", async ({ page }) =
|
||||
await expect(page.locator(".location-map-title span")).toHaveText("Ontario");
|
||||
});
|
||||
|
||||
test("ignores stale map save responses after switching locations", async ({ page }) => {
|
||||
await mockMapShell(page, adminHousehold, [storeLocation, secondStoreLocation]);
|
||||
|
||||
const firstLocationState = draftMapState([
|
||||
{
|
||||
id: 483,
|
||||
location_map_id: 900,
|
||||
zone_id: 501,
|
||||
zone_name: "Bakery",
|
||||
type: "zone",
|
||||
label: "Eastvale Bakery",
|
||||
x: 40,
|
||||
y: 40,
|
||||
width: 260,
|
||||
height: 160,
|
||||
rotation: 0,
|
||||
locked: false,
|
||||
visible: true,
|
||||
sort_order: 1,
|
||||
},
|
||||
], true);
|
||||
const nextLocationBaseState = publishedMapState([
|
||||
{
|
||||
id: 484,
|
||||
location_map_id: 902,
|
||||
zone_id: 502,
|
||||
zone_name: "Produce",
|
||||
type: "zone",
|
||||
label: "Ontario Produce",
|
||||
x: 80,
|
||||
y: 80,
|
||||
width: 280,
|
||||
height: 160,
|
||||
rotation: 0,
|
||||
locked: false,
|
||||
visible: true,
|
||||
sort_order: 1,
|
||||
},
|
||||
], true);
|
||||
const nextPublishedMap = {
|
||||
...nextLocationBaseState.published_map,
|
||||
id: 902,
|
||||
store_location_id: secondStoreLocation.id,
|
||||
};
|
||||
const nextLocationState = {
|
||||
...nextLocationBaseState,
|
||||
location: secondStoreLocation,
|
||||
map: nextPublishedMap,
|
||||
published_map: nextPublishedMap,
|
||||
};
|
||||
|
||||
let releaseSaveResponse: () => void = () => {};
|
||||
let resolveSaveStarted: () => void = () => {};
|
||||
let resolveSaveResponded: () => void = () => {};
|
||||
const saveStarted = new Promise<void>((resolve) => {
|
||||
resolveSaveStarted = resolve;
|
||||
});
|
||||
const saveGate = new Promise<void>((resolve) => {
|
||||
releaseSaveResponse = resolve;
|
||||
});
|
||||
const saveResponded = new Promise<void>((resolve) => {
|
||||
resolveSaveResponded = resolve;
|
||||
});
|
||||
|
||||
await page.route("**/households/1/locations/10/map", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(firstLocationState),
|
||||
});
|
||||
});
|
||||
await page.route("**/households/1/locations/10/map/draft", async (route) => {
|
||||
resolveSaveStarted();
|
||||
await saveGate;
|
||||
const payload = await route.request().postDataJSON();
|
||||
const savedObjects = (payload.objects as Array<Record<string, unknown>>).map((object, index) => ({
|
||||
id: 1480 + index,
|
||||
location_map_id: 900,
|
||||
zone_name: object.zone_id === 501 ? "Bakery" : null,
|
||||
...object,
|
||||
label: "Saved Eastvale Bakery",
|
||||
}));
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(draftMapState(savedObjects, true)),
|
||||
});
|
||||
resolveSaveResponded();
|
||||
});
|
||||
await page.route("**/households/1/locations/11/map", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(nextLocationState),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/stores/100/locations/10/map");
|
||||
await page.getByRole("button", { name: "Continue Editing" }).click();
|
||||
await page.getByRole("button", { name: "Map area Eastvale Bakery" }).click();
|
||||
await page.getByRole("textbox", { name: "Label" }).fill("Saving Eastvale Bakery");
|
||||
await page.getByRole("button", { name: "Save Draft" }).click();
|
||||
await saveStarted;
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.history.pushState({}, "", "/stores/100/locations/11/map");
|
||||
window.dispatchEvent(new PopStateEvent("popstate"));
|
||||
});
|
||||
|
||||
await expect(page.getByRole("button", { name: "Map area Ontario Produce" })).toBeVisible();
|
||||
await expect(page.locator(".location-map-title span")).toHaveText("Ontario");
|
||||
|
||||
releaseSaveResponse();
|
||||
await saveResponded;
|
||||
|
||||
await expect(page.getByRole("button", { name: "Map area Ontario Produce" })).toBeVisible();
|
||||
await expect(page.locator(".location-map-title span")).toHaveText("Ontario");
|
||||
await expect(page.getByRole("button", { name: "Map area Saved Eastvale Bakery" })).toHaveCount(0);
|
||||
await expect(page.getByText("Saved draft")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("resets map layer filters after switching locations", async ({ page }) => {
|
||||
await mockMapShell(page, adminHousehold, [storeLocation, secondStoreLocation]);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user