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=<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 <noreply@anthropic.com>
This commit is contained in:
@@ -409,20 +409,26 @@ describe('SupplyArchivePage frozen post', () => {
|
|||||||
expect(screen.queryByText(/Still open/)).not.toBeInTheDocument()
|
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', <SupplyRunPage />)
|
||||||
|
|
||||||
|
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)
|
mockViewport(1440)
|
||||||
const fetchMock = stubServer({
|
stubServer({
|
||||||
[`GET ${ARCHIVE('week')}`]: archive('week'),
|
[`GET ${ARCHIVE('week')}`]: archive('week'),
|
||||||
'GET /api/v1/shopping/2026-10-05': () => jsonResponse(200, CURRENT),
|
'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', <SupplyRunPage />)
|
||||||
|
|
||||||
expect(await screen.findByText('Archived')).toBeInTheDocument()
|
expect(await screen.findByRole('checkbox', { name: 'Corn tortillas' })).toBeEnabled()
|
||||||
expect(screen.getByText('Corn tortillas')).toBeInTheDocument()
|
expect(screen.queryByText('Archived')).not.toBeInTheDocument()
|
||||||
expect(screen.queryAllByRole('checkbox')).toEqual([])
|
expect(screen.queryByRole('complementary', { name: 'Supply archive' })).not.toBeInTheDocument()
|
||||||
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([])
|
|
||||||
view.unmount()
|
view.unmount()
|
||||||
|
|
||||||
await renderPage()
|
await renderPage()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Icon } from '../ui/Icon.tsx'
|
|||||||
import { IconButton } from '../ui/IconButton.tsx'
|
import { IconButton } from '../ui/IconButton.tsx'
|
||||||
import { Segmented } from '../ui/Segmented.tsx'
|
import { Segmented } from '../ui/Segmented.tsx'
|
||||||
import { Skeleton } from '../ui/Skeleton.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 styles from './SupplyArchivePage.module.css'
|
||||||
import { SUPPLY_ARCHIVE, SUPPLY_RUN, SupplyRunPage } from './SupplyRunPage.tsx'
|
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 })}`
|
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 {
|
function plural(n: number, one: string, many: string): string {
|
||||||
return `${n} ${n === 1 ? one : many}`
|
return `${n} ${n === 1 ? one : many}`
|
||||||
}
|
}
|
||||||
@@ -52,7 +46,7 @@ function PostCard({ post }: { post: ArchivePost }) {
|
|||||||
const rest = post.dinners.length - CHIPS
|
const rest = post.dinners.length - CHIPS
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={post.frozen ? archivePost(post.weekStart) : supplyRun(post.weekStart)}
|
to={post.frozen ? archivePost(post.weekStart) : supplyRunScreen(post.weekStart)}
|
||||||
className={[styles.post, post.current && styles.current].filter(Boolean).join(' ')}
|
className={[styles.post, post.current && styles.current].filter(Boolean).join(' ')}
|
||||||
>
|
>
|
||||||
<span className={styles.kicker}>
|
<span className={styles.kicker}>
|
||||||
@@ -170,7 +164,8 @@ function SupplyArchive({ panel }: { panel: boolean }) {
|
|||||||
|
|
||||||
// SupplyArchivePage is /plan/shopping/archive. A frozen post opens it on its
|
// 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
|
// 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
|
// 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
|
// open; on mobile and tablet it is the archive or, with a post open, the
|
||||||
// post's list.
|
// post's list.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Archive, Check, CloudUpload, MessageSquare } from 'lucide-react'
|
import { Archive, Check, CloudUpload, MessageSquare } from 'lucide-react'
|
||||||
import { useState } from '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 { defaultWeek, isMonday, localNow, weekdayKicker, weekLabel } from '../plan/week.ts'
|
||||||
import { ApiError } from '../api/errors.ts'
|
import { ApiError } from '../api/errors.ts'
|
||||||
import { formatClockTime, useHouseholdSettings } from '../settings/api.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 { Tag } from '../ui/Tag.tsx'
|
||||||
import {
|
import {
|
||||||
useGenerateSupplyRun,
|
useGenerateSupplyRun,
|
||||||
|
supplyRunScreen,
|
||||||
useSendToChat,
|
useSendToChat,
|
||||||
useSupplyRun,
|
useSupplyRun,
|
||||||
useToggles,
|
useToggles,
|
||||||
@@ -146,10 +147,10 @@ function ArchiveLink({ layout }: { layout: Breakpoint }) {
|
|||||||
// tablet up, where Archive and Send to Google Chat are actions in the
|
// 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
|
// 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
|
// 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
|
// toggle refused puts the item back and says why. An archived one is
|
||||||
// opened from the Supply archive (readOnly), is read-only and tagged
|
// read-only and tagged Archived, and a toggle it refused is not offered
|
||||||
// Archived, and a toggle it refused is not offered again.
|
// again.
|
||||||
function SupplyRunList({ weekStart, timezone, layout, readOnly }: WeekProps) {
|
function SupplyRunList({ weekStart, timezone, layout }: Omit<WeekProps, 'readOnly'>) {
|
||||||
const toggles = useToggles(weekStart)
|
const toggles = useToggles(weekStart)
|
||||||
const send = useSendToChat(weekStart)
|
const send = useSendToChat(weekStart)
|
||||||
const run = toggles.shown
|
const run = toggles.shown
|
||||||
@@ -160,7 +161,7 @@ function SupplyRunList({ weekStart, timezone, layout, readOnly }: WeekProps) {
|
|||||||
<UnconfirmedStatus at={run.chat_send_unconfirmed_at} timezone={timezone} />
|
<UnconfirmedStatus at={run.chat_send_unconfirmed_at} timezone={timezone} />
|
||||||
)
|
)
|
||||||
const sendUnconfirmed = send.error instanceof ApiError && send.error.code === 'chat_unconfirmed'
|
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 wide = layout !== 'mobile'
|
||||||
const failed = toggles.failed
|
const failed = toggles.failed
|
||||||
const failedItem = failed && run.groups.flatMap((group) => group.items).find((item) => item.id === failed.toggle.id)
|
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) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className={styles.helper}>
|
<p className={styles.helper}>
|
||||||
{run.status === 'archived'
|
{viewOnly
|
||||||
? 'This list is in the Supply archive, as it was when the week ended.'
|
? 'This list is in the Supply archive, as it was when the week ended.'
|
||||||
: viewOnly
|
: "Uncheck anything you already have — it won't be counted."}
|
||||||
? "This week's list as it stands, read-only in the Supply archive."
|
|
||||||
: "Uncheck anything you already have — it won't be counted."}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{wide && (
|
{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,
|
// 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 —
|
// 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) {
|
function WeekSupplyRun({ weekStart, timezone, layout, readOnly }: WeekProps) {
|
||||||
const run = useSupplyRun(weekStart)
|
const run = useSupplyRun(weekStart)
|
||||||
const generate = useGenerateSupplyRun(weekStart)
|
const generate = useGenerateSupplyRun(weekStart)
|
||||||
@@ -270,14 +270,16 @@ function WeekSupplyRun({ weekStart, timezone, layout, readOnly }: WeekProps) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return <SupplyRunList weekStart={weekStart} timezone={timezone} layout={layout} readOnly={readOnly} />
|
if (readOnly && run.data.status !== 'archived') return <Navigate to={supplyRunScreen(weekStart)} replace />
|
||||||
|
return <SupplyRunList weekStart={weekStart} timezone={timezone} layout={layout} />
|
||||||
}
|
}
|
||||||
|
|
||||||
// SupplyRunPage is the Supply run (mock 1l on mobile, 2b on tablet, 2f on
|
// 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
|
// 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
|
// 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,
|
// ?week=. Opened from the Supply archive it is readOnly: a frozen Supply run
|
||||||
// with no checkbox and nothing to make or send, whatever the week. Offline,
|
// 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
|
// where the settings cannot be read, it chooses the week by the schedule
|
||||||
// saved on this device.
|
// saved on this device.
|
||||||
export function SupplyRunPage({ readOnly = false }: { readOnly?: boolean }) {
|
export function SupplyRunPage({ readOnly = false }: { readOnly?: boolean }) {
|
||||||
|
|||||||
Reference in New Issue
Block a user