PR: #372: feat(documents): register row — derived filing, hierarchy, flags, scale [Story-1514] by @ankitmaheshwari-com
View PR →
Story 1514 · EPIC-1489 Documents & Intake · DOCS-TABLE

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:

SeamWhat it doesWHY it is here
filing-link join (adapter)toRegisterDocuments(items, filingLinks) joins each row to its DocumentFilingLinkView by id → fills filingType, filingState, parentDocumentIdfiling 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 └ connectornesting 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 labelsthe 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 ita 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.

Why this matters (fixtures lie in your favor): the row is verified against the real states it will meet — a still-processing Mode-1 row, an OCR failure with a live Retry, an annexure under its parent, a privileged + held document, and a 480-row corpus — not a single happy fixture. The failed/empty/scale states are the designed states, storied, not afterthoughts.

What shipped

Engine — src/lib/documents/register/ (pure)

FileChange
adapter.tstoRegisterDocument(item, filingLink?) + toRegisterDocuments(items, filingLinks) — join each row to its DocumentFilingLinkView → derived filingType/filingState/parentDocumentId
pagination.ts newbuildRegisterItems (flatten, collapse-aware) + paginateRegister + REGISTER_PAGE_SIZE

Leaf / composition — src/components/documents/register/

ComponentChange
DocumentFlagBadge newprivilege (crimson shield) / legal-hold (amber lock) chip, compact + labelled variants
RegisterDocumentRowannexure └ connector, privilege/hold chips, failed-row Retry button (onRetry)
RegisterCelllocalizes the derived filingState enum, — when unlinked
RegisterTablerenders the paginated render-item stream + Pagination; emptyKind (register / savedView / noResults); threads onRetryDocument

Tests 11 new

Stories (each leaf, story before live wiring)

House rules honored

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.jsonConfig+14-1
messages/hi.jsonConfig+14-1
src/components/documents/register/document-flag-badge.stories.tsxComponent+45-0
src/components/documents/register/document-flag-badge.tsxComponent+53-0
src/components/documents/register/presentation.tsComponent+19-0
src/components/documents/register/register-cell.tsxComponent+10-5
src/components/documents/register/register-document-row.stories.tsxComponent+33-1
src/components/documents/register/register-document-row.tsxComponent+58-29
src/components/documents/register/register-table.stories.tsxComponent+32-8
src/components/documents/register/register-table.tsxComponent+101-122
src/lib/documents/register/adapter.test.tsLibrary+74-1
src/lib/documents/register/adapter.tsLibrary+34-11
src/lib/documents/register/index.tsLibrary+6-0
src/lib/documents/register/pagination.test.tsLibrary+98-0
src/lib/documents/register/pagination.tsLibrary+73-0