PR: #374: feat(documents): register bulk select + bulk action bar [Story-1516] by @ankitmaheshwari-com
View PR →
Story 1516 · EPIC-1489 Documents & Intake · DOCS-BULK

The Documents Register bulk bar — select many, act once

Story-1510 shipped the pure view-state engine; Story-1514 made a single row tell the truth about one document. Story-1516 lets a litigator act on many documents at once: tick a set of rows and a dark bulk bar rises over the register carrying the four Mode-1 verbs — tag · link to proceeding · mark filed · download — plus a tri-state select-all and a clear. The brains stay where they belong: the bar is pure presentation that emits intents; the row-id set is the only payload.

🛰️ Satellite — why bulk is not "a button times N"

A document register at matter scale is 300–500+ rows. The work a lawyer does to it is rarely per-row: "tag these forty as evidence", "file this bundle against the writ petition", "pull the originals for the hearing". The naive build bolts a kebab menu onto each row and calls it done — so a forty-document action becomes forty trips through a menu, and the register stops being a workbench and becomes a chore. WHY this Story exists: the unit of action must match the unit of thought. The litigator thinks in sets — a slice, a tag, a proceeding's worth of paper — so selection must be a first-class, set-shaped gesture, and the verbs must operate on the set, once.

Detonate the naive fix: a per-row menu also lies about scale. It looks fine in a 6-row Storybook fixture and collapses in the real 400-row corpus where the actual work happens. Selection + a bulk bar is the only shape that stays honest as the corpus grows.

✈️ Airplane — the shape of the change

One new leaf, one wiring change, no new engine code. The selection set already lived in RegisterTable (rows and a header box were built in DOCS-TABLE). Story-1516 turns that latent state into action:

The bar, as it renders over the table:

3 selected + Tag ⛓ Link to proceeding ⚖ Mark filed ⬇ Download Clear ✕

🔬 Microscope — the decisions that carry weight

WHY the bar emits intents, not mutations

Mode-1 discipline (PRD v2): the verbs are deterministic-now in shape but the live mutations — write a tag, attach a filing link, flip a filing state, sign a batch download — land in EPIC-009. So the bar's contract is a row-id set, nothing more. RegisterTable collects the selected ids and calls the parent's intent; the parent owns what a verb does. This keeps the leaf testable in isolation and means the live-data seam is a single, named handoff rather than logic smeared through the component.

WHY "select-all" means select-all-visible

The header box selects every row in the current model — i.e. every row that survives the active slice — not just the current page and not the whole un-sliced corpus. That matches the lawyer's mental model: "select all" means "all of what I'm looking at". The indeterminate state exists for the same reason — it tells the truth when a slice is partially selected instead of pretending the box is a clean on/off.

WHY a ref, not a prop, for indeterminate

indeterminate is a DOM property with no React/HTML attribute — you cannot set it in JSX. The callback ref writes node.indeterminate = someSelected on every mount/update, the single supported path. It is a visual-only third state; the box's checked value still drives the toggle.

Tokens, i18n, a11y

Storybook — verify before live wiring

Per the Storybook-first rule, the leaf is built and verified in isolation before any live consumer exists (RegisterTable is not yet mounted in the authed app; that wiring is its own follow-up):

Live-data seam: the four onBulk* intents are the single handoff EPIC-009 wires to real mutations (tag write, filing link, filing-state flip, signed batch download). Until then they are honest Mode-1 no-ops in the stories.

Acceptance

CriterionState
select-all-visible + multiselectdone tri-state header box + per-row boxes
bulk actions wireddone tag · link · mark filed · download intents on selected ids
Storybook stories per leaf (before live wiring)done bulk-bar leaf + table bulk story
i18n (en + hi), tokens, a11y, empty statesdone bar hides at 0; both locales; no color literals

Files changed

6 file(s), +201 / -1 lines

Path Role + -
messages/en.jsonConfig+5-0
messages/hi.jsonConfig+5-0
src/components/documents/register/register-bulk-bar.stories.tsxComponent+33-0
src/components/documents/register/register-bulk-bar.tsxComponent+110-0
src/components/documents/register/register-table.stories.tsxComponent+17-0
src/components/documents/register/register-table.tsxComponent+31-1