The register, actually on the route
For five Stories the new Documents Register has been built, leaf by leaf,
behind glass: the view-state engine (1510), the β View popover and
folder-pills (1511), per-column header menus (1513), the detail drawer
(1515), bulk select (1516), and the saved-views API wiring (1517). Every
one verified green in Storybook. And the whole time, the live route
/matters/[id]/documents still rendered the legacy
DocumentRegister β a static chip-bar mock with fake
categories. This Story is the one move that makes all that work visible to
counsel: it swaps the mount.
π°οΈ Satellite β a feature nobody is mounted to is a feature nobody has
The most expensive defect in this codebase is the one that demos green and never ships: work declared done at the component boundary (passes in Storybook, passes a fixture test) while the system boundary β the real route, real backend, the empty first-run state β is never exercised. Six Stories of register can be perfect in isolation and still be, to the lawyer opening a matter, nothing at all: they see the old mock. The register exists only when the route renders it.
The naive version of "mount it" is a one-line import swap. That is the trap. Mounting live means closing the four data seams the Storybook harnesses faked β saved views, the row corpus, the status timeline, the filing derivation β through the real trust boundary, with a first paint that is not a spinner, and proving the zero-document first run (the actual state of every new matter) renders an empty register, not a crash. Swap the mount and retire the legacy, so old and new can't drift apart.
βοΈ Airplane β prefetch on the server, hydrate on the client, the same flight every list flies
PRAMAAN has exactly one shape for a list surface: the RSC
prefetches into a server QueryClient,
dehydrates it, and wraps the client tree in
HydrationBoundary; the client reads the same query key from a
warm cache β server-rendered first paint, no refetch flash. Story
1517 already built the saved-views half of that round-trip (proxy +
gateway + generated hooks). This Story flies it on the live route.
page.tsx (RSC)
ββ mattersGateway.listDocuments(id) βββΆ DocumentListItem[] (server gateway, already wired)
β toRegisterDocuments(...) βββΆ RegisterDocument[] β prop
ββ prefetchQuery(appListDocumentSavedViewsQueryKey(id),
documentSavedViewsGateway.list) βββΆ dehydrate
β
<HydrationBoundary> βββββββββββββββββββββββββββ
<LiveMatterDocumentsPage matterId documents={rows} />
ββ <SavedViewsRegister> ββuseAppListDocumentSavedViews(id)βββΆ reads warm cache
The one analogy that holds all the way down: the register is a stage, and this Story is raising the curtain. Every actor was rehearsed in a closed room (Storybook). Raising the curtain doesn't rewrite the play β it connects the stage to the house lights (the route), the script to the prompter (the saved-views cache), and lets the audience (counsel) finally see it. The actors don't change; their wiring to the real room does.
π¬ Microscope β the four seams, closed
Saved views β RSC prefetch, best-effort
page.tsx prefetches the matter's views with the exact
generated key (appListDocumentSavedViewsQueryKey(numericMatterId))
and the server gateway as the queryFn, mirroring the
document-types route. WHY the prefetch is wrapped in
try/catch and swallows: a saved-views blip must not
404 the whole Documents page β the client useAppListDocumentSavedViews
hook refetches through its /api/internal/* proxy and renders
its own viewsLoadFailed state. The documents themselves still
ride orNotFound, because no documents = no page.
The row corpus β a prop, not a hook
SavedViewsRegister was authored to take
documents: RegisterDocument[] as a prop β the row
corpus is server-owned, the saved-views are hook-owned. So the live page
adapts the wire rows once with toRegisterDocuments(rows) and
passes them down. WHY a prop and not useAppListMatterDocuments:
the documents-list browserβserver proxy does not exist yet (the systemic
gap tracked as Story-1049), and the route already fetches the corpus
server-side through the typed gateway. Feeding it as a prop is the wired
truth today; the optimistic upload add (onUploaded prepends a
row) keeps working because the adapter is pure and re-runs on every
rows change.
The detail drawer β lifted into the live container
RegisterTable always exposed onOpenDocument, but
nothing upstream filled it β clicking a row did nothing. This Story
threads one optional prop through RegisterView and lets
SavedViewsRegister own the drawer: it tracks the open row id,
builds the drawer view-model with toDocumentDetail, and mounts
DocumentDetailDrawerContainer (whose
useAppListDocumentStatusEvents hook streams the timeline).
WHY the container, not the leaf: the drawer's data hooks
belong with the corpus and the query client β the leaf stays
Storybook-verifiable without a backend.
Mode-1 honesty β AI/derived columns render empty
The adapter maps every Mode-2/AI field (ai name, document type, tags,
proceeding, pages) to its null/empty placeholder, and the filing
type/state to null (no batch filing-link endpoint or proxy
exists yet β a documented Mode-1 seam, not a gap to paper over). The
register renders these as "processing"/"β" until EPIC-009 fills them, so
a new matter looks truthful, not broken.
DocumentRegister is deleted (and
its dead documentRegisterRow builder pruned from
document-format.ts, keeping only the still-used
formatDocumentSize), so old and new cannot drift. The new
mount is proven against the real route in pnpm build, and the
empty-corpus first run is a first-class Storybook story β
not a fixture that only ever carries the happy data.
What shipped
| Acceptance criterion | Where | |
|---|---|---|
| Route renders the NEW register end-to-end | live-matter-documents-page.tsx β SavedViewsRegister (chrome + slicer + view popover + table + drawer + bulk + saved-view pills) | done |
Legacy DocumentRegister no longer mounted | git rm document-register.tsx; legacy row builder pruned from document-format.ts | done |
Data wired from src/gen (no codegen) | saved-views hook + RSC prefetch; status-events hook in the drawer; rows via the src/gen-typed gateway + toRegisterDocuments adapter | done |
RSC prefetch + HydrationBoundary first paint | page.tsx best-effort prefetchQuery on the generated key | done |
| Mode-1 Β· empty states Β· a11y | AI/derived columns render placeholders; zero-doc register empty state; row open is keyboard/ARIA-labelled | done |
| Storybook for the mounted register | saved-views-register.stories.tsx β Default, NoSavedViews, EmptyCorpus | done |
| i18n (en + hi) Β· tokens | existing MatterDetail.documents.* / DocumentsRegister.* both locales (no new copy); semantic tokens only | done |
Verify: pnpm typecheck β, pnpm lint (0 errors),
pnpm test (623 passed), pnpm build β,
storybook:drift β, git diff --check β.
Files changed
7 file(s), +190 / -211 lines
| Path | Role | + | - |
|---|---|---|---|
| src/app/[locale]/(workspace)/matters/[matterId]/documents/page.tsx | App route | +34 | -11 |
| src/components/documents/register/register-view.tsx | Component | +4 | -0 |
| src/components/documents/register/saved-views-register.stories.tsx | Component | +100 | -0 |
| src/components/documents/register/saved-views-register.tsx | Component | +34 | -13 |
| src/components/matters/document-register.tsx | Component | +0 | -115 |
| src/components/matters/live-matter-documents-page.tsx | Component | +12 | -6 |
| src/lib/matters/document-format.ts | Library | +6 | -66 |