9 file(s), +567 / -29 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +2 | -0 |
| messages/hi.json | Config | +2 | -0 |
| src/components/matters/add-matter-launcher.tsx | Component | +25 | -16 |
| src/components/matters/live-matters-list-page.tsx | Component | +10 | -7 |
| src/components/matters/matters-live-register-row.stories.tsx | Component | +78 | -0 |
| src/components/matters/matters-live-register-row.tsx | Component | +48 | -5 |
| src/lib/matters/optimistic.test.ts | Library | +146 | -0 |
| src/lib/matters/optimistic.ts | Library | +129 | -0 |
| src/lib/query/invalidation.ts | Library | +127 | -1 |
The matters list already read through the forum pattern (RSC prefetch →
HydrationBoundary → useAppListMatters), but its
create path used useAppCreateMatter with invalidate-on-success
only. Per the AGENTS.md optimistic-writes doctrine that is not optimistic:
the user waited the full create round-trip (~1.8 s) plus the list
refetch (~1.5 s) before the new row appeared — measured at ~5 s
live on trace 6a5218e9dd3b5080dc330ddea8028a31. This revision
makes the row appear the instant the wizard is submitted, then reconciles
with server truth.
optimisticCollectionInsert() in
src/lib/query/invalidation.ts returns
onMutate/onError/onSuccess/onSettled
mutation options for any collection create. It is generic over the page
shape, item type, mutation variables, and result: a caller supplies a
codec (how to read/write the item list on a cached page and how to id an
item), a tempItem builder, a serverItem
resolver, a matches predicate, and an onError
reporter. No call site hand-rolls an onMutate.
onMutate cancels in-flight collection reads, snapshots the
matching page-1 cache entries, and prepends the temp item.
onError restores the snapshot and calls the reporter (the
rollback path is mandatory). onSuccess swaps the temp item
for the server row by temp id. onSettled invalidates the
collection so server truth wins.
src/lib/matters/optimistic.ts.
optimisticMatterInsert() binds the helper to the matters
collection: the temp row is a real MatterListItem in
discovery status with a pending:-prefixed id
(the pending marker), matterId 0 and an empty
matterRef as placeholders. The server row is read from the
create response's embedded identity block.
add-matter-launcher.tsx now spreads
optimisticMatterInsert(...) into the create mutation and no
longer navigates to the new matter's Brief on success — that jump would
have hidden the very row this makes instant. The just-minted-type cleanup
(MMW-27) still runs on failure.
matters-live-register-row.tsx checks
isPendingMatter(): a pending row is a non-interactive
div (no Link, nothing to navigate to), visually
animate-pulse opacity-60, showing a “Creating…”
sub-line and an em-dash where the matter number will be.
matterMatchesListParams() prepends into a cached page only
when the new discovery matter actually matches that page's
filters (status / client / type / search). A Closed-filtered view, a
different client's view, a page 2 entry, and any date-window view are
left untouched and reconciled by the onSettled invalidate
instead of being polluted.
| Criterion | Where |
|---|---|
Reusable optimistic insert helper in invalidation.ts, no per-component onMutate |
optimisticCollectionInsert() in src/lib/query/invalidation.ts |
| Submitting the wizard shows the row instantly; drawer closes immediately | add-matter-launcher.tsx: onOpenChange(false) before the awaited create; onMutate prepends synchronously |
| Pending row non-clickable + visually distinct; swaps to the real clickable row on success | MatterLiveRegisterRowView pending branch; helper onSuccess swap by temp id |
| On server error the temp row is removed and an error toast shows | helper onError snapshot restore + onError: () => toast.error(...) in the launcher |
| Temp row only in matching cache entries; non-matching invalidated | matterMatchesListParams() + onSettled invalidate |
| Storybook story demonstrates the pending-row state | matters-live-register-row.stories.tsx (Pending, PendingAboveSettled) |
| i18n: new copy in both locales; tokens only | Matters.pendingRef / Matters.pendingCreate in en.json + hi.json; pending style uses existing utilities/tokens |
| Verify gate green | pnpm lint, pnpm test (1085 passing), pnpm build, Storybook build all pass |
The forced-failure path is covered by
src/lib/matters/optimistic.test.ts — “rolls the
pending row back and reports when the create fails”: after
onMutate prepends the pending row, invoking
onError restores the snapshot (the cache page returns to empty)
and the reporter is called with the failure. The swap, filter-awareness,
and prepend behaviours have their own cases in the same file.