Pattern: staging / batch result
Who uses it: whoever maintains data in bulk — an uploaded file, an interface run, a mass posting. How often: month-end and whenever a supplier sends a spreadsheet. What "done" looks like: every submitted row either became a record or is still on screen saying why it didn't. This is the landing place that makes the bulk action contract work for data the user has not seen yet. Not for a handful of records — three or four rows do not need a validate/apply split with per-row verdicts; edit them one at a time in a plain detail form. This pattern earns its complexity only once the count is large enough that a human cannot eyeball every row before committing.
The state after validation
1 ready, 1 to check, 2 cannot import. Every row below says which it is and why.
| # | Vendor | Cost centre | Amount | Result |
|---|---|---|---|---|
| 1 | Acme Supply Co. | CC-4021 | $4,200.00 | Ready Ready to import |
| 2 | Globex Industrial | CC-1180 | $56,000.00 | Check Matched "Globex" to Globex Industrial |
| 3 | Nobody Ltd | CC-1180 | $900.00 | Cannot import No vendor matches "Nobody Ltd" |
| 4 | Initech GmbH | CC-99 | $100.00 | Cannot import Cost centre "CC-99" is not CC-nnnn |
Zero new components. The row tints are the
data table's existing data-row-state; the reason on each
row is a badge plus words, so
the tint is never the only signal. There is no "ok" tint
— a row with nothing wrong is a normal row, and a third colour would
make the two that need attention harder to find.
Anatomy
- Intake — a paste box or file upload. Whatever the source, the rows arrive as text the server can parse.
- Validate action — explicitly separate from apply, because the whole point is seeing the outcome before committing to it.
- Summary — an alert counting all three outcomes in one sentence.
- Staging table — one row per submitted line, in the order submitted, each carrying its own verdict.
- Apply action — names the number it will act on ("Apply 2 valid rows"), and is disabled when that number is zero.
Data contract
POST /import action=validate csv=<pasted rows>
-> 200, the SAME screen with every row classified. Nothing is created.
POST /import action=apply csv=<the validated rows>
-> 200, a success alert with the count APPLIED, and the rows that could
not be applied still listed. Never a bare "done".
Row classification is the SERVER's, and only two of the three states put a
tint on the row:
ok -> no data-row-state. A normal row; its confirmation is the badge.
warning -> data-row-state="warning" applied, with a caveat
error -> data-row-state="error" not applied, ever Two posts to one endpoint, distinguished by an action field — not two endpoints, because validate and apply must agree on the parse exactly, and the cheapest way to guarantee that is one code path. The validated rows travel back in the second request, so the server never holds per-user staging state between them.
Deliberately no 4xx for a bad
row. Both actions always return 200 — a row failing validation
is DATA (data-row-state="error" in the classified result),
never a transport-level error, which is what lets one response carry a
mix of ok/warning/error rows in the same screen. A genuinely malformed
request (unparseable file, missing action field) is the one case that
IS a 4xx, and gets a plain error banner rather than a partial result.
States
| State | What ships |
|---|---|
| Nothing pasted yet | just the intake box — no empty table, no zero-count summary |
| Validated, all ready | a success alert; the apply button names the count |
| Validated, mixed | counts for all three outcomes, and the two that need attention are tinted |
| Validated, none applicable | the apply button is disabled — the one place disabled beats absent, because the control's whole job is to tell you the count is zero |
| Empty submission | the "nothing to validate" empty state, not a table with no rows |
| After apply | a success alert with the number APPLIED, and every un-appliable row still listed to fix and re-submit |
| Re-validated | verdicts are recomputed from scratch. Like bulk actions, per-row results are TRANSIENT — a stale "cannot import" on a row the user just fixed is worse than no message |
| No JS | fully functional — this page has no JS dependency. Every row's tint (data-row-state) and verdict badge are server-rendered classification, not JS state; the "Apply" and "Start over" controls shown here are static illustration (type="button", no script anywhere on the page), since a real staging screen posts the validate/apply actions from the Data contract above regardless of JS |
Why this instead of an editable grid
Bulk data maintenance splits into four patterns, and only one of them wants a grid. Row-swap inline edit covers config tables (inline editing); master-detail covers records with more fields than fit a row; mass change covers "set field X for N rows"; and this pattern covers true bulk work, where the editing happens in the user's spreadsheet — which beats any web grid at being a spreadsheet — and the web's job is to validate and report. Reaching for a grid to avoid building this one is how a screen ends up with 200 hand-edits and no audit of what failed.
Components used
complexity 3 of 4