Twelve selected, twelve different values
Every product I have used lies about bulk edit. I built the state model before any screen existed, generated six interface directions against it, and scored them on a rubric. The component runs on this page.
Contents
Select twelve servers and change one setting. The twelve don’t agree with each other: some are production, some staging, some development. What should the field show?
Products give one of two answers, and both are lies. A blank says no value, which is false, and on save the form submits it over values nobody looked at. The first item’s value is worse, because a filled field reads as fact. Nothing on screen says this is one of twelve answers.
A blank lies quietly. The first value lies convincingly.
The third answer is honest and nearly useless: the word Mixed. Eleven production hosts and one staging read as Mixed, and so do four of each. It tells you neither which case you are in nor how much you are about to change.
A form that misbehaves damages one record, and you notice. Bulk edit damages every selected record at once, and nothing looks broken afterwards.
Before any concept existed, I wrote down the seven ways it goes wrong:
| Hard case | What it demands |
|---|---|
| Mixed values | The field must never show a value the selected items do not share. |
| Method per field type | The legal verbs change with the kind of field. |
| No-op honesty | An operation that changes nothing has to say so. |
| Per-item preview | You can see what happens to each item, not only the total. |
| Partial failure | Some succeed, some fail, and the retry must know which. |
| Scope honesty | “All on this page” and “all matching the filter” cannot be the same click. |
| Keyboard | A tool used many times a day cannot be mouse-only. |
How the model meets each one: the model in full
The design of this thing is the state model, because the behaviour that matters is arithmetic, not layout. A drawing can show an answer without being able to produce the answer.
So the first artifact was a pure function, not a frame. plan() takes the selected items, a field, a method and a value, and returns what would happen to each item: before, after, and whether anything changes. It existed before any interface did, and every direction since was drawn against it.
A typical product reports all three as updated. The model reports two changes and one host that already had the tag, before anything is committed, so every interface built on it can be honest for free. The model and its tests: github.com/omkarpkh/bulk-edit-mixed
The component below draws; model.js decides. What will change, what already matches, the groups and the legal verbs all come from the model.
Try Log retention → Decrease by → 7 and open the clamped at 1 group. Or Tags → Clear all → Apply, which is the one operation that asks you to type the word.
A per-row diff is reviewable while the selection is small. At two hundred rows it is a long scroll, and scrolling is not review.
At scale nobody reviews rows. They review classes of change: staging to prod is one line, dev to prod another, and hosts already on prod a third that changes nothing. Each line opens to its members.
This holds at any size, because the number of distinct transitions is bounded by the field’s cardinality, not by the size of the selection. A field with three possible values produces at most three groups, whether you selected twelve hosts or twelve thousand.
Relative numbers are the exception: every host moves from its own value, so they group by outcome instead, as moved, clamped at the floor or already at the floor. That is also the only way a clamp shows up; in a flat list of two hundred rows, nobody finds it.
Nobody has used this: n = 0. No sessions, no production deployment: it runs on this page, not in a product. Until an operator uses it under time pressure, everything here is the design’s claim, not its result.
The tie was never broken. On the rubric, directions 1 and 3 tie. The build started from direction 1 before a correction turned its win into that tie, and nothing since has chosen between them. The scores
The scale hole was found late. Three evaluation passes rated the per-row diff fine at any size, because its layout holds. None asked what happens at two hundred rows; planning the build did, and grouping is the fix. What it cost the scoring