fix: ignore stale map buy responses

This commit is contained in:
Nico 2026-06-04 14:42:23 -07:00
parent cfa6e70c13
commit 8f06f3e072
2 changed files with 70 additions and 4 deletions

View File

@ -68,6 +68,7 @@ export default function LocationMapManager() {
const { activeHousehold, loading: householdLoading, hasLoaded } = useContext(HouseholdContext);
const { stores, activeStore, setActiveStore } = useContext(StoreContext);
const [buyModalItem, setBuyModalItem] = useState(null);
const mapBuyScopeRef = useRef({ activeHouseholdId: null, locationId: null });
const {
beginSaving,
@ -172,6 +173,18 @@ export default function LocationMapManager() {
});
}, [setMapState]);
useEffect(() => {
mapBuyScopeRef.current = {
activeHouseholdId: activeHousehold?.id,
locationId,
};
}, [activeHousehold?.id, locationId]);
const isCurrentMapBuyScope = useCallback((activeHouseholdId, scopedLocationId) => (
String(mapBuyScopeRef.current.activeHouseholdId || "") === String(activeHouseholdId || "") &&
String(mapBuyScopeRef.current.locationId || "") === String(scopedLocationId || "")
), []);
const handleMapBuyCancel = useCallback(() => {
setBuyModalItem(null);
}, []);
@ -186,13 +199,16 @@ export default function LocationMapManager() {
return;
}
const activeHouseholdId = activeHousehold.id;
const scopedLocationId = locationId;
const item = buyModalItems.find((candidate) => candidate.id === buyModalItem.id) || buyModalItem;
try {
const currentIndex = buyModalItems.findIndex((candidate) => candidate.id === item.id);
const resolvedIndex = currentIndex >= 0 ? currentIndex : 0;
await markBought(activeHousehold.id, locationId, item.item_name, quantity, true);
await markBought(activeHouseholdId, scopedLocationId, item.item_name, quantity, true);
if (!isCurrentMapBuyScope(activeHouseholdId, scopedLocationId)) return;
let nextBuyModalItems = buyModalItems;
if (quantity >= item.quantity) {
@ -205,9 +221,11 @@ export default function LocationMapManager() {
} else {
let updatedItem;
try {
const response = await getItemByName(activeHousehold.id, locationId, item.item_name);
const response = await getItemByName(activeHouseholdId, scopedLocationId, item.item_name);
if (!isCurrentMapBuyScope(activeHouseholdId, scopedLocationId)) return;
updatedItem = response.data || getLocallyReducedMapItem(item, quantity);
} catch {
if (!isCurrentMapBuyScope(activeHouseholdId, scopedLocationId)) return;
updatedItem = getLocallyReducedMapItem(item, quantity);
toast.info("Updated item locally", "Latest quantity will sync when the map reloads.");
}
@ -221,10 +239,11 @@ export default function LocationMapManager() {
toast.success("Marked item bought", `Marked item ${item.item_name} as bought`);
} catch (error) {
if (!isCurrentMapBuyScope(activeHouseholdId, scopedLocationId)) return;
const message = getApiErrorMessage(error, "Failed to mark item as bought");
toast.error("Mark item bought failed", `Mark item bought failed: ${message}`);
}
}, [activeHousehold?.id, buyModalItem, buyModalItems, locationId, toast, updateMapItems]);
}, [activeHousehold?.id, buyModalItem, buyModalItems, isCurrentMapBuyScope, locationId, toast, updateMapItems]);
const mapSize = useMemo(
() => ({

View File

@ -845,8 +845,30 @@ test("closes stale map buy modal after switching locations", async ({ page }) =>
location: secondStoreLocation,
map: nextPublishedMap,
published_map: nextPublishedMap,
items: [],
items: [
{
id: 1,
item_name: "ontario oranges",
quantity: 1,
bought: false,
zone_id: 502,
zone: "Produce",
added_by_users: ["map-user"],
},
],
};
let resolveBuyRequestStarted: () => void = () => {};
let releaseBuyResponse: () => void = () => {};
let resolveBuyResponseSent: () => void = () => {};
const buyRequestStarted = new Promise<void>((resolve) => {
resolveBuyRequestStarted = resolve;
});
const buyResponseGate = new Promise<void>((resolve) => {
releaseBuyResponse = resolve;
});
const buyResponseSent = new Promise<void>((resolve) => {
resolveBuyResponseSent = resolve;
});
await page.route("**/households/1/locations/10/map", async (route) => {
await route.fulfill({
@ -862,11 +884,28 @@ test("closes stale map buy modal after switching locations", async ({ page }) =>
body: JSON.stringify(nextLocationState),
});
});
await page.route("**/households/1/locations/10/list/item", async (route) => {
if (route.request().method() !== "PATCH") {
await route.fulfill({ status: 404, contentType: "application/json", body: "{}" });
return;
}
resolveBuyRequestStarted();
await buyResponseGate;
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ success: true }),
});
resolveBuyResponseSent();
});
await page.goto("/stores/100/locations/10/map");
await page.getByRole("button", { name: "Map area Eastvale Bakery" }).click();
await page.getByRole("button", { name: "Mark french bread bought" }).click();
await expect(page.locator(".confirm-buy-item-name")).toHaveText("french bread");
await page.getByRole("button", { name: "Mark as Bought" }).click();
await buyRequestStarted;
await page.evaluate(() => {
window.history.pushState({}, "", "/stores/100/locations/11/map");
@ -877,6 +916,14 @@ test("closes stale map buy modal after switching locations", async ({ page }) =>
await expect(page.locator(".location-map-title span")).toHaveText("Ontario");
await expect(page.locator(".confirm-buy-modal")).toHaveCount(0);
await expect(page.getByText("french bread")).toHaveCount(0);
await page.getByRole("button", { name: "Map area Ontario Produce" }).click();
await expect(page.getByRole("button", { name: "Mark ontario oranges bought" })).toBeVisible();
releaseBuyResponse();
await buyResponseSent;
await expect(page.getByRole("button", { name: "Mark ontario oranges bought" })).toBeVisible();
await expect(page.getByText("Marked item bought")).toHaveCount(0);
});
test("closes stale map delete confirmation after switching locations", async ({ page }) => {