PR: #378: feat(documents): mount the Documents Register live, retire legacy [Story-1527] by @ankitmaheshwari-com
View PR →
Story 1527 Β· EPIC-1489 Documents & Intake Β· DOCS-REGISTER-MOUNT

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.

Done = system boundary. This is the Story-1517 named seam, closed: the legacy 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 criterionWhere
Route renders the NEW register end-to-endlive-matter-documents-page.tsx β†’ SavedViewsRegister (chrome + slicer + view popover + table + drawer + bulk + saved-view pills)done
Legacy DocumentRegister no longer mountedgit rm document-register.tsx; legacy row builder pruned from document-format.tsdone
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 adapterdone
RSC prefetch + HydrationBoundary first paintpage.tsx best-effort prefetchQuery on the generated keydone
Mode-1 Β· empty states Β· a11yAI/derived columns render placeholders; zero-doc register empty state; row open is keyboard/ARIA-labelleddone
Storybook for the mounted registersaved-views-register.stories.tsx β€” Default, NoSavedViews, EmptyCorpusdone
i18n (en + hi) Β· tokensexisting MatterDetail.documents.* / DocumentsRegister.* both locales (no new copy); semantic tokens onlydone

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.tsxApp route+34-11
src/components/documents/register/register-view.tsxComponent+4-0
src/components/documents/register/saved-views-register.stories.tsxComponent+100-0
src/components/documents/register/saved-views-register.tsxComponent+34-13
src/components/matters/document-register.tsxComponent+0-115
src/components/matters/live-matter-documents-page.tsxComponent+12-6
src/lib/matters/document-format.tsLibrary+6-66