From 3a4991cc8e74e6d905c711c128147a64e2d7f807 Mon Sep 17 00:00:00 2001 From: dev_agent Date: Sun, 11 Oct 2026 16:34:02 -0400 Subject: [PATCH] Supply archive: open an unfrozen ?week= visit in the editable Supply run (Maestro 29b629aa) Review remediation for task 29b629aa-1ad9-48a3-be75-0380b645d4fe. Finding 1 (medium): /plan/shopping/archive?week= forced the list read-only whatever its status, so a saved link to a Supply run not yet frozen showed it tagged Archived without checkboxes. Read-only, a run not yet frozen now opens the editable Supply run screen instead (replace), and only a frozen run is tagged Archived. The desktop test that pinned the old read-only behaviour now expects the editable routing, and a new test covers a past week not yet frozen. Finding 2 (low): the card link uses the exported supplyRunScreen() instead of a duplicate supplyRun() helper. Co-Authored-By: Claude Opus 5.5 --- web/src/shopping/SupplyArchivePage.test.tsx | 24 ++++++++++------- web/src/shopping/SupplyArchivePage.tsx | 13 +++------ web/src/shopping/SupplyRunPage.tsx | 30 +++++++++++---------- 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/web/src/shopping/SupplyArchivePage.test.tsx b/web/src/shopping/SupplyArchivePage.test.tsx index ebff490..1eaad07 100644 --- a/web/src/shopping/SupplyArchivePage.test.tsx +++ b/web/src/shopping/SupplyArchivePage.test.tsx @@ -409,20 +409,26 @@ describe('SupplyArchivePage frozen post', () => { expect(screen.queryByText(/Still open/)).not.toBeInTheDocument() }) - it('on desktop opens the current week’s post read-only, and with no post open shows this week’s Supply run to change', async () => { + it('opens a saved link to a past week’s list not yet frozen in the editable Supply run, not read-only', async () => { + stubServer({ 'GET /api/v1/shopping/2026-09-28': () => jsonResponse(200, { ...FROZEN, status: 'current' }) }) + await renderPage('/plan/shopping/archive?week=2026-09-28', ) + + expect(await screen.findByRole('checkbox', { name: 'Pizza dough' })).toBeEnabled() + expect(screen.queryByText('Archived')).not.toBeInTheDocument() + expect(screen.getByText("Uncheck anything you already have — it won't be counted.")).toBeInTheDocument() + }) + + it('on desktop opens a saved link to the current week’s list, not yet frozen, in the editable Supply run, as with no post open', async () => { mockViewport(1440) - const fetchMock = stubServer({ + stubServer({ [`GET ${ARCHIVE('week')}`]: archive('week'), 'GET /api/v1/shopping/2026-10-05': () => jsonResponse(200, CURRENT), }) - const view = await renderPage('/plan/shopping/archive?week=2026-10-05') + const view = await renderPage('/plan/shopping/archive?week=2026-10-05', ) - expect(await screen.findByText('Archived')).toBeInTheDocument() - expect(screen.getByText('Corn tortillas')).toBeInTheDocument() - expect(screen.queryAllByRole('checkbox')).toEqual([]) - expect(screen.queryByText('Send to Google Chat')).not.toBeInTheDocument() - fireEvent.click(screen.getByText('Corn tortillas')) - expect(fetchMock.mock.calls.filter(([, init]) => init?.method !== undefined && init.method !== 'GET')).toEqual([]) + expect(await screen.findByRole('checkbox', { name: 'Corn tortillas' })).toBeEnabled() + expect(screen.queryByText('Archived')).not.toBeInTheDocument() + expect(screen.queryByRole('complementary', { name: 'Supply archive' })).not.toBeInTheDocument() view.unmount() await renderPage() diff --git a/web/src/shopping/SupplyArchivePage.tsx b/web/src/shopping/SupplyArchivePage.tsx index d3e4a80..e645c95 100644 --- a/web/src/shopping/SupplyArchivePage.tsx +++ b/web/src/shopping/SupplyArchivePage.tsx @@ -9,7 +9,7 @@ import { Icon } from '../ui/Icon.tsx' import { IconButton } from '../ui/IconButton.tsx' import { Segmented } from '../ui/Segmented.tsx' import { Skeleton } from '../ui/Skeleton.tsx' -import { useSupplyArchive, WEEK_PARAM, type ArchiveGroup, type ArchivePeriod, type ArchivePost } from './api.ts' +import { supplyRunScreen, useSupplyArchive, WEEK_PARAM, type ArchiveGroup, type ArchivePeriod, type ArchivePost } from './api.ts' import styles from './SupplyArchivePage.module.css' import { SUPPLY_ARCHIVE, SUPPLY_RUN, SupplyRunPage } from './SupplyRunPage.tsx' @@ -32,12 +32,6 @@ export function archivePost(weekStart: string): string { return `${SUPPLY_ARCHIVE}?${new URLSearchParams({ [WEEK_PARAM]: weekStart })}` } -// supplyRun is the Supply run screen on the week starting on Monday -// weekStart, where a list not yet frozen can still be changed. -function supplyRun(weekStart: string): string { - return `${SUPPLY_RUN}?${new URLSearchParams({ [WEEK_PARAM]: weekStart })}` -} - function plural(n: number, one: string, many: string): string { return `${n} ${n === 1 ? one : many}` } @@ -52,7 +46,7 @@ function PostCard({ post }: { post: ArchivePost }) { const rest = post.dinners.length - CHIPS return ( @@ -170,7 +164,8 @@ function SupplyArchive({ panel }: { panel: boolean }) { // SupplyArchivePage is /plan/shopping/archive. A frozen post opens it on its // week (?week=), whose list the Supply run screen shows read-only and tagged -// Archived; a post not yet frozen opens the Supply run instead. On desktop (mock 2f) the archive is a panel at +// Archived; a post not yet frozen opens the Supply run instead, as does a +// ?week= visit to a list not yet frozen. On desktop (mock 2f) the archive is a panel at // the right of that list, this week's editable Supply run when no post is // open; on mobile and tablet it is the archive or, with a post open, the // post's list. diff --git a/web/src/shopping/SupplyRunPage.tsx b/web/src/shopping/SupplyRunPage.tsx index a43545c..44d4d0b 100644 --- a/web/src/shopping/SupplyRunPage.tsx +++ b/web/src/shopping/SupplyRunPage.tsx @@ -1,6 +1,6 @@ import { Archive, Check, CloudUpload, MessageSquare } from 'lucide-react' import { useState } from 'react' -import { Link, useSearchParams } from 'react-router' +import { Link, Navigate, useSearchParams } from 'react-router' import { defaultWeek, isMonday, localNow, weekdayKicker, weekLabel } from '../plan/week.ts' import { ApiError } from '../api/errors.ts' import { formatClockTime, useHouseholdSettings } from '../settings/api.ts' @@ -15,6 +15,7 @@ import { Skeleton } from '../ui/Skeleton.tsx' import { Tag } from '../ui/Tag.tsx' import { useGenerateSupplyRun, + supplyRunScreen, useSendToChat, useSupplyRun, useToggles, @@ -146,10 +147,10 @@ function ArchiveLink({ layout }: { layout: Breakpoint }) { // tablet up, where Archive and Send to Google Chat are actions in the // header; a send that fails says why, and one whose outcome is not known // offers no retry and leaves the status line saying so. On a current Supply run each item toggles at once (useToggles); a -// toggle refused puts the item back and says why. An archived one, or any -// opened from the Supply archive (readOnly), is read-only and tagged -// Archived, and a toggle it refused is not offered again. -function SupplyRunList({ weekStart, timezone, layout, readOnly }: WeekProps) { +// toggle refused puts the item back and says why. An archived one is +// read-only and tagged Archived, and a toggle it refused is not offered +// again. +function SupplyRunList({ weekStart, timezone, layout }: Omit) { const toggles = useToggles(weekStart) const send = useSendToChat(weekStart) const run = toggles.shown @@ -160,7 +161,7 @@ function SupplyRunList({ weekStart, timezone, layout, readOnly }: WeekProps) { ) const sendUnconfirmed = send.error instanceof ApiError && send.error.code === 'chat_unconfirmed' - const viewOnly = readOnly || run.status === 'archived' + const viewOnly = run.status === 'archived' const wide = layout !== 'mobile' const failed = toggles.failed const failedItem = failed && run.groups.flatMap((group) => group.items).find((item) => item.id === failed.toggle.id) @@ -178,11 +179,9 @@ function SupplyRunList({ weekStart, timezone, layout, readOnly }: WeekProps) {

- {run.status === 'archived' + {viewOnly ? 'This list is in the Supply archive, as it was when the week ended.' - : viewOnly - ? "This week's list as it stands, read-only in the Supply archive." - : "Uncheck anything you already have — it won't be counted."} + : "Uncheck anything you already have — it won't be counted."}

{wide && ( @@ -243,7 +242,8 @@ type WeekProps = { weekStart: string; timezone: string; layout: Breakpoint; read // WeekSupplyRun is the Supply run of the week starting on weekStart, or, // when the week has none yet, the offer to make it from the week's plan — -// not offered read-only. +// not offered read-only. Read-only, a Supply run not yet frozen opens the +// editable Supply run screen instead, where it can still change. function WeekSupplyRun({ weekStart, timezone, layout, readOnly }: WeekProps) { const run = useSupplyRun(weekStart) const generate = useGenerateSupplyRun(weekStart) @@ -270,14 +270,16 @@ function WeekSupplyRun({ weekStart, timezone, layout, readOnly }: WeekProps) { ) } - return + if (readOnly && run.data.status !== 'archived') return + return } // SupplyRunPage is the Supply run (mock 1l on mobile, 2b on tablet, 2f on // desktop) of the week the Plan opens on — next week once this week's // plan_draft time has come, otherwise this week — or of the week chosen by -// ?week=. Opened from the Supply archive it is readOnly: tagged Archived, -// with no checkbox and nothing to make or send, whatever the week. Offline, +// ?week=. Opened from the Supply archive it is readOnly: a frozen Supply run +// is tagged Archived, with no checkbox and nothing to make or send; one not +// yet frozen opens the editable Supply run screen instead. Offline, // where the settings cannot be read, it chooses the week by the schedule // saved on this device. export function SupplyRunPage({ readOnly = false }: { readOnly?: boolean }) {