feat(fp): WP-07 — brief on the shared idioms + RemoteData MDX
Collapse brief.store's busy signal + nullable lastError into one Idle | Busy | Failed union (saveState gets matching tag-object style), and route brief.page's load through RemoteData + <app-async> instead of a hand-rolled @switch, via a BriefStore.remoteData projection of the machine's existing loading/failed tags -- the machine keeps owning the letter's own status lifecycle untouched. New brief.store.spec.ts covers the Busy->Idle/Failed transitions; new Foundations/RemoteData & Async MDX page documents the pattern and the WP-06 typed-loaded-slot fallback. Deviation from the original plan recorded in the WP file.
This commit is contained in:
@@ -46,7 +46,7 @@ for its existing violations, so every WP ends green.
|
||||
| [WP-04](WP-04-ui-not-infrastructure.md) | Boundaries II: `ui ↛ infrastructure` + showcase sanction | 0 · gates | done |
|
||||
| [WP-05](WP-05-parse-boundaries.md) | Parse-don't-validate closure + MDX | 1 · FP/DDD | done |
|
||||
| [WP-06](WP-06-typed-async.md) | Generic async template contexts — kill `$any()` | 1 · FP/DDD | done |
|
||||
| [WP-07](WP-07-brief-idioms.md) | Brief on the shared idioms + RemoteData MDX | 1 · FP/DDD | todo |
|
||||
| [WP-07](WP-07-brief-idioms.md) | Brief on the shared idioms + RemoteData MDX | 1 · FP/DDD | done |
|
||||
| [WP-08](WP-08-store-idiom.md) | One store idiom + machine naming + TEA MDX | 1 · FP/DDD | todo |
|
||||
| [WP-09](WP-09-pure-logic.md) | Pure-logic closure: dates + missing command specs | 1 · FP/DDD | todo |
|
||||
| [WP-10](WP-10-button-fidelity.md) | CIBG button fidelity | 2 · CIBG | todo |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# WP-07 — Brief on the shared idioms + RemoteData MDX
|
||||
|
||||
Status: todo
|
||||
Status: done
|
||||
Phase: 1 — FP/DDD core
|
||||
Depends on: WP-06 (typed `<app-async>`)
|
||||
|
||||
@@ -50,15 +50,53 @@ bypassing the shared molecule.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] No boolean-plus-error signal pairs in `brief/`.
|
||||
- [ ] `/brief` renders all four async states (check with `?scenario=slow|empty|error`).
|
||||
- [ ] Specs cover the transition union (Busy→Failed, Busy→Idle).
|
||||
- [ ] MDX renders under Foundations.
|
||||
- [x] No boolean-plus-error signal pairs in `brief/`.
|
||||
- [x] `/brief` renders all four async states (checked with `?scenario=slow|error`; see
|
||||
Deviation for why `empty` isn't meaningful here).
|
||||
- [x] Specs cover the transition union (Busy→Failed, Busy→Idle) — `brief.store.spec.ts`
|
||||
(new).
|
||||
- [x] MDX renders under Foundations.
|
||||
|
||||
## Verification
|
||||
|
||||
GREEN + `npm run test-storybook:ci`. Manual: `npm start` → `/brief` with
|
||||
`?scenario=slow`, `?scenario=error`; exercise autosave + submit + rejection flow.
|
||||
GREEN + `npm run test-storybook:ci` (197 unit tests, 137 Storybook/a11y — both up from
|
||||
WP-06's baseline by the new store spec). Manual smoke via a running `docker compose`
|
||||
stack + Playwright: `/brief` normal load, `?scenario=slow` (spinner), `?scenario=error`
|
||||
(failure alert + working retry), and `/brief?role=approver` — all with no console errors.
|
||||
|
||||
## Deviation from the original plan
|
||||
|
||||
**The machine's `loading`/`failed` tags were NOT moved out of `BriefState`.** The
|
||||
Decisions block hedges this ("only if they purely mirror the fetch") — they do, but
|
||||
removing them turns out to need more than a re-type: `createStore(initial, reduce)`
|
||||
requires a concrete `initial: BriefState` value, and once `loading`/`failed` are gone
|
||||
there is no state left to represent "not loaded yet" without inventing a second wrapping
|
||||
layer (the store's top-level signal would need to become `RemoteData<Err, LoadedState>`
|
||||
directly, with the machine's `reduce` only invoked inside the `Success` branch — a
|
||||
different wiring shape from every other machine in the app, and a ~250-line ripple
|
||||
through `brief.machine.spec.ts`). That redesign is a bigger, riskier change than this WP's
|
||||
"re-type, don't restructure" framing calls for.
|
||||
|
||||
Instead, `BriefStore.remoteData` **projects** the existing machine model onto
|
||||
`RemoteData<Error | undefined, LoadedBriefState>` (`loading`→`Loading`, `failed`→
|
||||
`Failure`, `loaded`→`Success`), and `brief.page.ts` renders that projection through
|
||||
`<app-async>`. This satisfies the actual goal (the load lifecycle renders through the
|
||||
shared molecule, not a hand-rolled `@switch`) without touching `brief.machine.ts` or its
|
||||
spec at all — `BriefState` keeps its three tags exactly as they were. The seam holds:
|
||||
`RemoteData` still owns "is the fetch done", the machine still owns "what is the letter
|
||||
doing" (draft/submitted/approved/rejected/sent) once loaded.
|
||||
|
||||
**`?scenario=empty` doesn't apply to `/brief`.** It rewrites the HTTP body to `[]`, which
|
||||
fails `parseBriefView`'s `!dto.brief` check — the same as any malformed response, so it
|
||||
surfaces as a `Failure`, not an `Empty`. A single-letter GET has no meaningful "empty"
|
||||
state (unlike a list endpoint), so this isn't a gap — `AsyncComponent`'s `Empty` branch
|
||||
simply never fires for this resource, by construction (no `isEmpty` input is passed).
|
||||
|
||||
**Reused the WP-06 fallback for the loaded slot.** `<ng-template appAsyncLoaded>` can't
|
||||
type `let-s` to the loaded value for the same structural reason WP-06 documented
|
||||
(a directive's generic can't inherit from a sibling `[data]` input) — `brief.page.ts` adds
|
||||
a `loaded` computed and narrows with `@if (loaded(); as s)`, matching
|
||||
`dashboard.page.ts`/`registration-detail.page.ts`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
@@ -67,4 +105,11 @@ Brief component stories (WP-15); machine renaming conventions (WP-08).
|
||||
## Risks
|
||||
|
||||
Autosave (debounced) interplay with the new transition union — flush ordering must stay
|
||||
as-is; the machine spec pins it.
|
||||
as-is; `brief.store.spec.ts`'s Busy→Idle/Failed tests exercise `transition()`, which
|
||||
still calls `flushSave()` before the server action exactly as before. One subtle,
|
||||
pre-existing edge case changed slightly: if a debounced autosave fails mid-transition
|
||||
(setting the error) and the transition's own server action then succeeds, the original
|
||||
code left the stale autosave error visible (it only cleared `lastError` at the very start
|
||||
of `transition()`/`resetDemo()`); the re-typed version now clears it on that same
|
||||
successful end, since `actionState` only holds one current value. Judged an acceptable,
|
||||
arguably-corrective difference, not a behavior this WP needed to preserve.
|
||||
|
||||
Reference in New Issue
Block a user