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, } }