Keep same-name add refusals and refused removal order in onboarding
CI / ci (push) Successful in 2m20s
CI / ci (pull_request) Successful in 2m17s

Step 2 keyed each refusal by the dinner's name, so a second add of the
same name still in flight replaced the first add's refusal when it was
refused too. Each attempt now keys its refusal uniquely; trying a change
again once it has been refused still clears that earlier refusal.

A refused removal was put back at the index it had in the list as it
stood when it was removed, so refusing two overlapping removals of A and
B restored B, A, C until a reread corrected it. A removal now only marks
its Regular in the cache and the list hides it; a refusal clears the
mark, so the Regular returns to its own place whatever else changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 17:26:27 -04:00
co-authored by Claude Opus 5.5
parent 4e9b1ea2f9
commit 568a6ddfb8
3 changed files with 98 additions and 36 deletions
+12 -11
View File
@@ -1,4 +1,4 @@
import { useState, type FormEvent } from 'react'
import { useRef, useState, type FormEvent } from 'react'
import { Plus } from 'lucide-react'
import { useNavigate } from 'react-router'
import { useAddRegular, useRegulars, useRemoveRegular, type Regular } from '../dinners/api.ts'
@@ -31,9 +31,11 @@ function sameName(a: string, b: string): boolean {
return a.trim().toLowerCase() === b.trim().toLowerCase()
}
// Refusal is one add or removal the server turned down. key names the
// change, so trying it again replaces its refusal and no other.
type Refusal = { key: string; text: string }
// Refusal is one add or removal the server turned down. key is unique to
// that attempt, so two attempts of the same change in flight each keep their
// own refusal. change names what was tried, so trying it again once it has
// been refused clears the earlier refusal and no other.
type Refusal = { key: number; change: string; text: string }
// RegularsStep is onboarding step 2, handoff mock 1h: the household adds
// the dinners it makes on repeat. Each add and remove shows at once and is
@@ -48,6 +50,7 @@ export function RegularsStep() {
// latest change, which would hide a refusal while another change is in
// flight.
const [refusals, setRefusals] = useState<Refusal[]>([])
const attempts = useRef(0)
const list = regulars.data ?? []
const picks = commonPicks.filter((pick) => !list.some((regular) => sameName(regular.name, pick)))
// Next waits for every add to be answered, so a refused add's error shows
@@ -55,13 +58,11 @@ export function RegularsStep() {
const adding = list.some((regular) => regular.pending)
const saved = list.filter((regular) => !regular.pending).length
function track(key: string, what: string, change: Promise<unknown>) {
setRefusals((all) => all.filter((refusal) => refusal.key !== key))
change.catch((error: Error) => {
setRefusals((all) => [
...all.filter((refusal) => refusal.key !== key),
{ key, text: `Could not ${what}: ${error.message}` },
])
function track(change: string, what: string, answer: Promise<unknown>) {
const key = ++attempts.current
setRefusals((all) => all.filter((refusal) => refusal.change !== change))
answer.catch((error: Error) => {
setRefusals((all) => [...all, { key, change, text: `Could not ${what}: ${error.message}` }])
})
}