diff --git a/web/src/plan/PlanPage.test.tsx b/web/src/plan/PlanPage.test.tsx index fc76865..bdfdd47 100644 --- a/web/src/plan/PlanPage.test.tsx +++ b/web/src/plan/PlanPage.test.tsx @@ -774,6 +774,27 @@ describe('PlanPage', () => { expect(await screen.findByText('Opened /plan/shopping?week=2026-10-05')).toBeInTheDocument() }) + it("shows a failed read of the week's shopping list with Retry, the link still opening the Supply run", async () => { + let failing = true + const fetchMock = stubServer({ + [`GET ${THIS_WEEK}`]: () => jsonResponse(200, planBody('draft')), + 'GET /api/v1/shopping/2026-10-05': () => (failing ? errorResponse(500, 'internal') : jsonResponse(200, supplyRun(19))), + }) + await renderPage() + + const alert = await screen.findByRole('alert') + expect(alert).toHaveTextContent("Couldn't load this week's shopping list. internal message") + expect(screen.getByRole('link', { name: 'Shopping list' })).toHaveAttribute('href', '/plan/shopping?week=2026-10-05') + // Retry reads it again. + failing = false + fireEvent.click(within(alert).getByRole('button', { name: 'Retry' })) + const shopping = await screen.findByRole('link', { name: 'Shopping list · 19' }) + expect(screen.queryByRole('alert')).not.toBeInTheDocument() + expect(fetchMock.mock.calls.filter(([input]) => String(input) === '/api/v1/shopping/2026-10-05')).toHaveLength(2) + fireEvent.click(shopping) + expect(await screen.findByText('Opened /plan/shopping?week=2026-10-05')).toBeInTheDocument() + }) + it('links to the Supply run with no count while the week has no shopping list', async () => { const fetchMock = stubServer({ [`GET ${THIS_WEEK}`]: () => jsonResponse(200, planBody('draft')) }) await renderPage() @@ -782,6 +803,8 @@ describe('PlanPage', () => { await waitFor(() => expect(fetchMock.mock.calls.map(([input]) => String(input))).toContain('/api/v1/shopping/2026-10-05')) const shopping = screen.getByRole('link', { name: 'Shopping list' }) expect(shopping).toHaveAttribute('href', '/plan/shopping?week=2026-10-05') + // No list is not a failure. + expect(screen.queryByRole('alert')).not.toBeInTheDocument() fireEvent.click(shopping) expect(await screen.findByText('Opened /plan/shopping?week=2026-10-05')).toBeInTheDocument() }) @@ -1154,6 +1177,27 @@ describe.each([ expect(status).toHaveTextContent('Synced to Google Calendar 1 hr ago') }) + it("shows a failed read of the week's shopping list with Retry, the action still opening the Supply run", async () => { + let failing = true + stubServer({ + [`GET ${THIS_WEEK}`]: () => jsonResponse(200, planBody('draft')), + 'GET /api/v1/shopping/2026-10-05': () => (failing ? errorResponse(500, 'internal') : jsonResponse(200, supplyRun(28))), + ...lists, + }) + await renderPage() + + const alert = await screen.findByRole('alert') + expect(alert).toHaveTextContent("Couldn't load this week's shopping list. internal message") + expect(screen.getByText('Spaghetti bolognese')).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'Shopping list' })).toHaveAttribute('href', '/plan/shopping?week=2026-10-05') + failing = false + fireEvent.click(within(alert).getByRole('button', { name: 'Retry' })) + const shopping = await screen.findByRole('link', { name: 'Shopping list · 28' }) + expect(screen.queryByRole('alert')).not.toBeInTheDocument() + fireEvent.click(shopping) + expect(await screen.findByText('Opened /plan/shopping?week=2026-10-05')).toBeInTheDocument() + }) + it("opens the week's Supply run from the Shopping list action, saying how many items are still to buy", async () => { stubServer({ [`GET ${THIS_WEEK}`]: () => jsonResponse(200, planBody('draft')), diff --git a/web/src/plan/PlanPage.tsx b/web/src/plan/PlanPage.tsx index 385ad63..cdb4f32 100644 --- a/web/src/plan/PlanPage.tsx +++ b/web/src/plan/PlanPage.tsx @@ -196,15 +196,15 @@ function DayColumn({ day, timezone, swapping, onSwap }: { day: PlanDay; timezone // ShoppingList opens the week's Supply run: an outline action in the plan // manager's header, a grape link under the Draft board on mobile. Once the // week has a Supply run it says how many items are still to buy, "Shopping -// list · 28"; until then, or while that is read, it says no count, and the -// Supply run screen offers to make the list. -function ShoppingList({ weekStart, layout }: { weekStart: string; layout: Breakpoint }) { - const run = useSupplyRun(weekStart) +// list · 28"; until then, while that is read, or when reading it failed, it +// says no count, and the Supply run screen offers to make the list or shows +// the failure. +function ShoppingList({ weekStart, neededCount, layout }: { weekStart: string; neededCount: number | undefined; layout: Breakpoint }) { const className = layout === 'mobile' ? styles.shoppingLink : [buttonStyles.button, buttonStyles.outline, styles.action].join(' ') return ( - {run.data ? `Shopping list · ${run.data.needed_count}` : 'Shopping list'} + {neededCount !== undefined ? `Shopping list · ${neededCount}` : 'Shopping list'} ) } @@ -258,6 +258,7 @@ function WeekBoard({ const [confirming, setConfirming] = useState(false) const swap = useSwapDay(weekStart) const push = usePushToCalendar(weekStart) + const run = useSupplyRun(weekStart) const pushing = push.pending // The date of the day being swapped, while the swap sheet is open. const [swappingDate, setSwappingDate] = useState(null) @@ -375,6 +376,13 @@ function WeekBoard({ ) : ( Couldn't check on the draft. {job.error.message} ) + // A failed read of the week's Supply run is shown, with Retry, after the + // Draft board's Shopping list link or under the plan manager's board; the + // Shopping list action still opens the Supply run. + const shoppingList = + const supplyRunFailure = run.isError && ( + void run.refetch()}>Couldn't load this week's shopping list. {run.error.message} + ) // swapIn sends the swap chosen in the sheet and closes it. function swapIn(chosen: Swap) { @@ -477,7 +485,8 @@ function WeekBoard({ {pushing ? 'Pushing to Calendar…' : 'Looks good — push to Calendar'} - + {shoppingList} + {supplyRunFailure} {sheet} ) @@ -509,7 +518,7 @@ function WeekBoard({
{plan.data ? : }
- + {shoppingList}