The Documents Register row — derived filing, hierarchy, flags, scale
Story-1510 shipped the pure view-state engine and the leaf components that draw its output. Story-1514 makes a row tell the whole truth about a document: its derived filing type/state, its place in the annexure hierarchy, whether it is privileged work product or under legal hold, whether its processing failed and can be retried — and it makes the table survive a matter-scale corpus without melting the DOM. The brains stay in the pure core; this Story welds the last two data seams into it (the filing-link derivation and the parent that drives the indent) and finishes the row's visual vocabulary.
🛰️ Satellite — why the row is not just "more columns"
A litigator does not read a document register as a file list; she reads it as a case posture. The naive build treats every column the same — "render the field" — and so it renders the filing state straight off the document. That is wrong at the data-model level: the filing lifecycle lives on the Filing, never on the document (ADR-125). A document's filing type/state is a read-time derivation through its link — and for an annexure, through its parent's filing. Bolt that onto the document row and you have smeared filing truth across two places that will drift. WHY this Story exists: the row must surface derived facts as derivations, the hierarchy as real nesting, and the protective flags as first-class — because a privileged work-product page rendered as filed evidence is not a UI nit, it is a malpractice surface.
✈️ Airplane — the shape of the change
Three seams, one of them brand-new:
| Seam | What it does | WHY it is here |
|---|---|---|
| filing-link join (adapter) | toRegisterDocuments(items, filingLinks) joins each row to its DocumentFilingLinkView by id → fills filingType, filingState, parentDocumentId | filing truth is derived (FN-FILING-LINK), not stored on the document; the join is the only place that truth enters the row |
| hierarchy indent (engine, already pure) | the engine pulls each annexure under its present parent and stamps indent; the row draws a └ connector | nesting is data (parent_document_id), so it was always in the engine — it just needed real parents, which the join now supplies |
| row vocabulary (leaves) | privilege / legal-hold chips, a failed-row Retry affordance, localized filing-state labels | the protective + failure axes the ACs call out, each a real, labelled control |
| scale (table) | flatten the grouped model to a render-item stream and paginate it | a register is built for 300–500+ rows; a bounded page keeps the DOM small without abandoning grouping or the semantic table |
// the join is the whole data delta — pure, testable, optional links
toRegisterDocuments(items, filingLinks) →
RegisterDocument{ filingType, filingState, parentDocumentId, ... }
🔬 Microscope — the details that earn the row
1. Filing is derived, and the cell knows it
DocumentFilingLinkView carries the linked Filing's
filingType (a display name) and filingState
(the nine-value FilingState enum: filed,
served, …). The adapter stores both raw; the cell localizes
the enum via filingStateLabels.<value> with a
raw fallback, and renders an unlinked document as — (the
contract's own rule), never as "0" or blank. An annexure that
ridesParentFiling shows its parent's state — because the
engine resolved the parent, not because the row guessed.
2. The hierarchy was already pure — it needed real parents
view-state.ts already nests children under a present parent
and stamps indent. The reason annexures rendered flat was
upstream: the adapter hard-coded parentDocumentId: null.
The filing-link join removes that lie; the engine's nesting lights up
with no engine change, and the row adds the └ connector so
the indent reads as containment, not as accidental whitespace.
3. Scale without abandoning grouping or the table
A virtualized <table> means absolutely-positioned rows
— at war with grouping, sticky group headers, and a11y. So the model is
flattened to one ordered render-item stream (group headers and
rows interleaved) and that stream is paged at
REGISTER_PAGE_SIZE (100). A collapsed group keeps its header
and drops its rows; the page never mounts more than one window. The math
is a pure function with its own unit tests — the table just renders a
page and the shared Pagination primitive drives it.
What shipped
Engine — src/lib/documents/register/ (pure)
| File | Change |
|---|---|
adapter.ts | toRegisterDocument(item, filingLink?) + toRegisterDocuments(items, filingLinks) — join each row to its DocumentFilingLinkView → derived filingType/filingState/parentDocumentId |
pagination.ts new | buildRegisterItems (flatten, collapse-aware) + paginateRegister + REGISTER_PAGE_SIZE |
Leaf / composition — src/components/documents/register/
| Component | Change |
|---|---|
DocumentFlagBadge new | privilege (crimson shield) / legal-hold (amber lock) chip, compact + labelled variants |
RegisterDocumentRow | annexure └ connector, privilege/hold chips, failed-row Retry button (onRetry) |
RegisterCell | localizes the derived filingState enum, — when unlinked |
RegisterTable | renders the paginated render-item stream + Pagination; emptyKind (register / savedView / noResults); threads onRetryDocument |
Tests 11 new
adapter.test.ts— filing-link derivation (type/state/parent), unlinked → null, and the by-id join across a subset of links.pagination.test.ts— flatten (ungrouped, grouped, collapsed), page clamping, and a 480-row corpus bounded to one window.
Stories (each leaf, story before live wiring)
DocumentFlagBadge— privileged / legal-hold / compact / both.RegisterDocumentRow— failed + Retry, annexure indent, privileged, legal-hold, both.RegisterTable— large paginated corpus, with-retry, saved-view empty, no-results, empty register.
House rules honored
- Mode discipline — filing type/state and the parent are deterministic derivations (FN-FILING-LINK), not Mode-2 AI; the AI columns (clean name, document type, tags) stay nullable and render processing/empty until EPIC-009.
- Contracts (HR-22) — the join consumes the generated
DocumentFilingLinkView/FilingStatefromsrc/genonly; no hand-written mirror, andpnpm codegenwas not run. - i18n — new copy (
filingStateLabels.*,table.retry/retryRow) added to bothen.jsonandhi.json; flags reuse the existingflags.*keys. - Design tokens — no hex/rgb/hsl; the flag chips use the crimson/amber semantic ramps (badge/alert use), status hues stay reserved for status.
- a11y — Retry and pagination are real labelled buttons; the compact flag chip carries its label on
title+aria-label; the└connector isaria-hidden. - Scale AC — paged at 100 items/window so a 300–500-row corpus never mounts more than one page.
- Storybook-first — this Story wires no route; the live register page + its filing-link fetch land in a later DOCS Story.
Verify green
pnpm typecheck # clean
pnpm lint # clean
pnpm test # passing (11 new)
pnpm build # success
Observability (ADR-124): this slice adds no executable workflow boundary, route, job, or external call — a pure adapter/pagination core plus presentational leaves — so it is instrumentation-exempt; spans land when the live register page and its filing-link fetch are wired.
Files changed
15 file(s), +664 / -179 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +14 | -1 |
| messages/hi.json | Config | +14 | -1 |
| src/components/documents/register/document-flag-badge.stories.tsx | Component | +45 | -0 |
| src/components/documents/register/document-flag-badge.tsx | Component | +53 | -0 |
| src/components/documents/register/presentation.ts | Component | +19 | -0 |
| src/components/documents/register/register-cell.tsx | Component | +10 | -5 |
| src/components/documents/register/register-document-row.stories.tsx | Component | +33 | -1 |
| src/components/documents/register/register-document-row.tsx | Component | +58 | -29 |
| src/components/documents/register/register-table.stories.tsx | Component | +32 | -8 |
| src/components/documents/register/register-table.tsx | Component | +101 | -122 |
| src/lib/documents/register/adapter.test.ts | Library | +74 | -1 |
| src/lib/documents/register/adapter.ts | Library | +34 | -11 |
| src/lib/documents/register/index.ts | Library | +6 | -0 |
| src/lib/documents/register/pagination.test.ts | Library | +98 | -0 |
| src/lib/documents/register/pagination.ts | Library | +73 | -0 |