diff --git a/web/src/plan/PlanPage.test.tsx b/web/src/plan/PlanPage.test.tsx
index c26a70d..44be664 100644
--- a/web/src/plan/PlanPage.test.tsx
+++ b/web/src/plan/PlanPage.test.tsx
@@ -1,6 +1,6 @@
import { QueryClientProvider } from '@tanstack/react-query'
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
-import { MemoryRouter, Route, Routes, useLocation } from 'react-router'
+import { Link, MemoryRouter, Route, Routes, useLocation } from 'react-router'
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest'
import { ApiProvider, createApiClient, createQueryClient, createVerifiedSession } from '../api/index.ts'
import { createNetworkStatus } from '../pwa/networkStatus.ts'
@@ -177,10 +177,16 @@ async function renderPage() {
)
}
-// Opened stands in for the screen a link opened, saying where it is.
+// Opened stands in for the screen a link opened, saying where it is, with
+// a link back to the Plan.
function Opened() {
const { pathname, search } = useLocation()
- return
Opened {pathname + search}
+ return (
+ <>
+
Opened {pathname + search}
+ Back to the Plan
+ >
+ )
}
// supplyRun is the Supply run of the week of Oct 5 with needed of its 32
@@ -542,6 +548,35 @@ describe('PlanPage', () => {
])
})
+ it('no longer says the Supply run could not be made once the shopping list is made, after leaving the Plan and coming back', async () => {
+ stubServer({
+ [`GET ${THIS_WEEK}`]: () => jsonResponse(200, planBody('draft')),
+ [`POST ${PUSH}`]: () => jsonResponse(200, syncedNow(SUPPLY_RUN_FAILED)),
+ 'POST /api/v1/shopping/2026-10-05/generate': () => jsonResponse(200, { ...supplyRun(28), skipped: [] }),
+ })
+ await renderPage()
+
+ fireEvent.click(await screen.findByRole('button', { name: 'Looks good — push to Calendar' }))
+ const alert = await screen.findByRole('alert')
+ expect(alert).toHaveTextContent("Pushed to Calendar, but this week's Supply run couldn't be made.")
+
+ // A Supply run never made is still said after leaving the Plan and
+ // coming back.
+ fireEvent.click(screen.getByRole('link', { name: 'Shopping list' }))
+ fireEvent.click(await screen.findByRole('link', { name: 'Back to the Plan' }))
+ expect(await screen.findByRole('alert')).toHaveTextContent("Pushed to Calendar, but this week's Supply run couldn't be made.")
+
+ fireEvent.click(within(screen.getByRole('alert')).getByRole('button', { name: 'Make the shopping list' }))
+ expect(await screen.findByRole('status')).toHaveTextContent("Made this week's Supply run from the plan.")
+
+ // Once made, it is not said again to be unmade.
+ fireEvent.click(screen.getByRole('link', { name: 'Shopping list · 28' }))
+ fireEvent.click(await screen.findByRole('link', { name: 'Back to the Plan' }))
+ expect(await screen.findByRole('link', { name: 'Shopping list · 28' })).toBeInTheDocument()
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument()
+ expect(screen.queryByRole('button', { name: 'Make the shopping list' })).not.toBeInTheDocument()
+ })
+
it('pushes nothing again until the changed plan is read and shown', async () => {
let read: () => Response | Promise = () => jsonResponse(200, planBody('draft'))
let push = () => errorResponse(409, 'plan_changed')
diff --git a/web/src/plan/PlanPage.tsx b/web/src/plan/PlanPage.tsx
index 0bbc291..baf6257 100644
--- a/web/src/plan/PlanPage.tsx
+++ b/web/src/plan/PlanPage.tsx
@@ -352,6 +352,7 @@ function WeekBoard({
setJobId(null)
swap.dismiss()
push.reset()
+ generate.reset()
mutate(
{ weekStart, confirm },
{
@@ -371,8 +372,17 @@ function WeekBoard({
}
const retryPush = cached ? () => void plan.refetch() : pushToCalendar
- // A push's Supply run made again on the Plan stands for the push's.
- const supplyRun: PushedSupplyRun | null = push.supplyRun && (generate.isSuccess ? { status: 'generated', error: null } : push.supplyRun)
+ // makeShoppingList makes the week's Supply run again after a push could
+ // not make it. Once made, the push is settled, even if the Plan was left
+ // meanwhile: coming back does not say again that it couldn't be made. A
+ // failure is the mutation's state, which generate.error reports.
+ function makeShoppingList() {
+ const { settle } = push
+ generate.mutateAsync().then(settle, () => undefined)
+ }
+ // A push's Supply run made again on the Plan stands for the push's, which
+ // is settled.
+ const supplyRun: PushedSupplyRun | null = generate.isSuccess ? { status: 'generated', error: null } : push.supplyRun
const failure = push.error !== null ? (
) : supplyRun?.status === 'failed' ? (
@@ -382,7 +392,7 @@ function WeekBoard({
it again.
) : (
- generate.mutate()} retryLabel="Make the shopping list">
+
Pushed to Calendar, but this week's Supply run couldn't be made.
{generate.isPending
? ' Making the shopping list…'
@@ -426,6 +436,7 @@ function WeekBoard({
function swapIn(chosen: Swap) {
setSwappingDate(null)
push.reset()
+ generate.reset()
swap.mutate(chosen)
}
// dropIn swaps in a dinner dragged onto a day. A drag is not picked up,
diff --git a/web/src/plan/api.ts b/web/src/plan/api.ts
index 5a5302f..6359449 100644
--- a/web/src/plan/api.ts
+++ b/web/src/plan/api.ts
@@ -430,6 +430,7 @@ export type PushToCalendar = {
supplyRun: PushedSupplyRun | null
mutate: () => void
reset: () => void
+ settle: () => void
}
// usePushToCalendar pushes the plan of the week starting on weekStart to the
@@ -450,8 +451,10 @@ export type PushToCalendar = {
//
// The week's pushes live in the mutation cache, not in the component, as its
// swaps do, so leaving the week and coming back keeps one pending. reset
-// leaves settled pushes behind; it is refused while one is pending. Settled
-// pushes of a week no longer shown are left behind too
+// leaves settled pushes behind; it is refused while one is pending. settle
+// leaves behind the push shown, once its failed Supply run has been made
+// another way, so it is not said again to be unmade; a push sent since is
+// kept. Settled pushes of a week no longer shown are left behind too
// (keepWeekMutations).
export function usePushToCalendar(weekStart: string): PushToCalendar {
const { apiFetch } = useApi()
@@ -463,7 +466,12 @@ export function usePushToCalendar(weekStart: string): PushToCalendar {
// The week's pushes, in the order sent.
const pushes = useMutationState({
filters: { mutationKey },
- select: (mutation) => ({ status: mutation.state.status, error: mutation.state.error, data: mutation.state.data as PushedPlan | undefined }),
+ select: (mutation) => ({
+ mutationId: mutation.mutationId,
+ status: mutation.state.status,
+ error: mutation.state.error,
+ data: mutation.state.data as PushedPlan | undefined,
+ }),
})
const latest = pushes.at(-1)
@@ -504,11 +512,17 @@ export function usePushToCalendar(weekStart: string): PushToCalendar {
for (const mutation of week) mutations.remove(mutation)
}
+ function settle() {
+ const shown = mutations.findAll({ mutationKey }).find((mutation) => mutation.mutationId === latest?.mutationId)
+ if (shown !== undefined) mutations.remove(shown)
+ }
+
return {
pending: pushes.some((push) => push.status === 'pending'),
error: latest?.status === 'error' ? latest.error! : null,
supplyRun: latest?.status === 'success' ? latest.data!.supply_run : null,
mutate,
reset,
+ settle,
}
}