Repository navigation
fix(fields): an empty array is not a cell value, and eight renderers fabricated one - #8586
Merged
Merged
Conversation
…fabricated one objectui#8481 repaired the shared read renderers whose output for `[]` was blank. These are the other half of that census — the renderers whose output for `[]` was not blank but WRONG, a value the record does not hold. Re-measured by rendering every registered field type on e411c3e (after 639ca9d, which moved none of them): `boolean`/`toggle` drew a CHECKED checkbox, the number / currency / percent family printed a `0` (and a 0% progress bar), `email` / `url` / `phone` wrapped nothing in a live `mailto:` / `tel:` / empty-href anchor, `color` drew a swatch with no colour, and `date` drew `formatDate`'s hand-rolled em-dash with no accessible name. The ruling is per renderer, not one predicate. `[]` holds no boolean, so a boolean column holding it holds NO value — not `false`; only `[]` moves there (`isEmptyMultiValue`), a real `false` is still an unchecked box. The number family's `0` was `Number('')`, a coercion artefact, so its guard is on the coerced text (`isBlankCellText`) and deliberately also sweeps a stored `''`. The link family draws no anchor with nothing to link to; `color` no swatch without a colour string; `date` reaches the shared affordance instead of a private dash. `{}` is untouched everywhere; `json` and `file` keep the objectui#8481 fence. A consumer pin in plugin-grid covers both grid widths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-justin
marked this pull request as ready for review
September 8, 2026 13:35
os-justin
enabled auto-merge
September 8, 2026 13:35
This was referenced Sep 8, 2026
os-justin
deleted the
claude/issue-8490-fields-empty-array-fabrication
branch
September 8, 2026 14:04
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #8490
What this changes
Eight cell renderers in
@object-ui/fields(twelve field types) drew a value the record does not hold when the stored value was[], and a ninth drew a placeholder that was not the shared one. They now render the sharedEmptyValueaffordance — the muted em-dash carrying aNo valueaccessible name — exactly as they already did fornull. One consumer pin inplugin-gridcovers both grid widths. Tag-shaped fragments are spelled in words throughout this body (the GitHub sanitizer eats them, even inside backticks).The census was re-derived first — the card's eight are still eight
Measured by rendering every registered field type (53 keys, extracted from
getCellRenderer's map at test time so a new key cannot be missed) against[],{},''andnullone411c3e58, the merge base. The three shas in the brief were each verified as ancestors of that base (merge-base --is-ancestorexit 0, which is the self-proving direction; not shallow).639ca9dc0(the objectui#8481 landing) moved none of the eight: all twelve types in the card's table still fabricated, and the ninthdaterow still drewformatDate's private dash. So the PR keeps the card's shape.[]one411c3e58boolean,toggleBooleanCellRendereraria-checkedtrue; a completion-named field drew the green "Completed" indicatorEmptyValuenumber,slider,ratingNumberCellRenderer0EmptyValuecurrencyCurrencyCellRenderer0EmptyValuepercent,progressPercentCellRendererprogressbarrole,aria-valuenow0, and the text0%EmptyValueemailEmailCellRendererhrefofmailto:and no text, plus a "Copy email" buttonEmptyValueurlUrlCellRenderer_blankanchor with an emptyhrefEmptyValuephonePhoneCellRendererhrefoftel:and a phone icon, plus a copy buttonEmptyValuecolorColorSwatchCellRendererEmptyValuedate(adjacent ninth)DateCellRenderertabular-numswith an emptytitleholding a bare em-dashEmptyValueRe-running the same census on the fixed tree and diffing cell by cell: 19 of 212 cells moved, 193 unchanged. The 19 are the 13 types above on
[], plus the six number-family types on''(the declared sweep below).{}moved nowhere.One correction to the record while measuring: objectui#8481's narrative said
plugin-detailwas already protected byhasCellValue. It is not —hasCellValuereturnstruefor every non-null object, arrays included (it delegates object values to the cell renderers by design), soDetailSectionreached these renderers with[]too and the checked checkbox was on the record page as well as the grid. OnlyRelatedList'sisValueEmptypre-checks arrays. The fix being in the shared renderer covers both.The ruling is per renderer, not one predicate
The card asked for this distinction explicitly. Every row lands on
EmptyValue, but for different reasons and with different sweep widths — andNo valueis a true statement at each site, because in each case the record holds no value of the field's type:boolean/toggle—[]is not a boolean and holds no entries, so the column holds no value, notfalse. Only[]moves (isEmptyMultiValue, the objectui#8481 helper, now with a fourth caller). A realfalseis still an unchecked box; anactive-named field holding a realfalsestill draws its "Off" badge. Scalar truthiness coercions (0,'','false',{}) are untouched by this guard — that is a separate decision, recorded as objectui#8582.number/slider/rating/currency/percent/progress— the0wasNumber(''), a JavaScript coercion artefact, not a stored zero. The guard is therefore on the coerced text (isBlankCellText: null, or a string that trims to nothing), which deliberately also sweeps a stored''and whitespace — the identical fabrication one input shape over. Declared and pinned. A real stored0still prints0/0%with its bar.email/url/phone— nothing to link to means no anchor and no copy button in the DOM.['']has no address either;['ada@example.com']still links (the test is on the coerced text, not onArray.isArray).color— no colour string means no swatch.date— the coerced-empty branch reaches the shared affordance instead offormatDate's private dash. An unparsable string still takes that dash today; that is a decision (DateTimeCellRendereralready answers it with the affordance) and is filed as objectui#8581.Evidence — every leg was run, none predicted
packages/fields/src/__tests__/cellRenderers.fabricatedValue-8490.test.tsx— 29 cases in three blocks: THE DEFECT (15[]cases, each asserting the fabricated artefact's absence first, then the affordance), POPULATED (10 load-bearing cases), THE BOUNDARY (4). Each[]case is ordered so that its failure on the unfixed tree and its failure under a dead harness land on different sentences.Three ablation legs, each run against the committed implementation (
index.tsxblobb4d6dfe48), each mutation proven on disk in both directions before the run and restored by state afterwards (git diff HEADempty;git hash-objectof the path equal to the HEAD blob; trap onEXIT INT TERMwith absolute paths). No dist preflight was needed and that is declared: the root vitest config aliases every@object-ui/*specifier tosrc, so both the mutation and the restore are live without a build.index.tsxswapped in (isBlankCellTextcount 0, base-only guard line count 3, 3338 lines)boolean: a checked checkbox is a fabricated value for [],number: the digit 0 is a fabricated value for [],percent: a 0% progress bar is a fabricated value for [],email: a live anchor with an empty target is a fabricated affordance for [],color: a bordered swatch box with no colour is a fabricated value for [],date: the value span must not be drawn around nothing, plus the two''/['']boundary sentences. All 10 POPULATED cases pass on the base — they measure what the base already did right.EmptyValueunconditionally (ABLATION-8490marker count 9)booleanscalar-0boundary, each on its own sentence:boolean: a real true must still draw its checkbox,boolean: a real false must still draw its checkbox,completed: a real true must still draw the indicator,active: a real false must still draw the Off badge,number: a real 0 must still print,email: a real address must still draw its anchor,email: a one-entry array still links to its one address,color: a real colour must still draw its swatch,date: a real date must still draw its value span. Survivors, declared as scope: the 15[]cases and three boundary cases pass under the caricature by design — they cannot discriminate it, which is why the POPULATED block exists.data-slot="empty-value"renamed incomponents/src/custom/empty.tsx(KILLED count 1, original 0)[]case on…: the shared EmptyValue affordance must be present(thedaterow onthe em-dash must be the shared affordance, not a bare span…) — a different sentence from leg A on every row, proving the defect assertions do not navigate by the thing the caricature erases.Harness-death signatures (
Cannot read properties of null,Unable to find,TypeError,is not a function) counted in every leg's log: 0 in A, B and C — every failure is the discriminating assertion's own message.Recorded for honesty: the first attempt at leg A was a null operation. The BASE file I wrote carried a
BASE=prefix,git showfailed on the malformed ref and wrote an emptyindex.tsx; all 29 cases failed withTypeError: resolveCellRendererType is not a function, and my on-disk gate only checked the deleted-text count (0) — the injected-text count was also 0. Discarded; redone with a gate on both counts and the line total. The restore-by-state check held on the null run too.Consumer pin:
packages/plugin-grid/src/__tests__/fabricatedValueCell-8490.test.tsx— desktop row withis_active: []/amount: []draws no checkbox and no0and carries two affordances; the populated row in the same grid still checks its box and prints its real0; the sub-768px card view repeats both.Verification
pnpm exec vitest run packages/fields/— 140 files / 2399 tests passed (command-exit 0 under the shared verify lock).pnpm exec vitest run packages/plugin-grid/ packages/plugin-kanban/— 149 files / 1250 tests passed.pnpm exec vitest run packages/plugin-detail/— 143 files / 1281 tests passed.pnpm exec vitest run packages/plugin-dashboard/ packages/plugin-list/plus, in the same run, the 30 test files elsewhere that name a fields cell renderer orEmptyValue(app-shell 24, plugin-gantt 3, plugin-view 1, plugin-report 1, console 1) — 201 files / 2020 tests passed. Local total: 633 files / 6950 tests, every run with command-exit 0 under the shared verify lock.pnpm --filter @object-ui/fields run type-check— echoedtsc --noEmit && tsc -p tsconfig.test.json, exit 0, after building the dependency closure (pnpm --filter '@object-ui/fields^...' build).tsc -p tsconfig.test.json --showConfiglists the new test file (and the objectui#8481 control), so the test program covers it.pnpm --filter @object-ui/fields lintandpnpm --filter @object-ui/plugin-grid lint— 0 errors. eslint's own--format jsoncounts 221 files linted infieldsand 152 inplugin-grid; the population is each package'seslint ., and the config is not type-aware, so this diff cannot move a verdict in any file it did not touch — the untouched packages' lint is invariant and left to CI'spnpm lint. Warnings are the repo's pre-existing 935 / 784 at warn level: 0 ofindex.tsx's 124 warning lines fall on the 78 lines this PR added (intersected against the-U0diff), and the two new test files carry fiveno-explicit-anywarnings on the sameas anyharness shapes the objectui#8481 tests use.check:control-bytesOK (6770 files);check-changeset-presenceOK;check-changeset-fixedOK;check-governed-queue-guard --teston the four paths: NOT GOVERNED;check:unreferenced-sourcesOK;check-type-check-coverageOK (45/46 viatype-check, 42/42 test programs).pnpm checkand the rest oflint.yml's repo-wide scans go to CI.Declared narrowing.
turbo ls --affectedlists 20 packages. Run locally in full:fieldsand the read-mode cell surfaces (plugin-grid,plugin-kanban,plugin-detail,plugin-dashboard,plugin-list), plus the grep-selected files elsewhere. Sent to CI:app-shell(653 files),console(90),plugin-form(84 — it consumes field widgets, not the cell renderers this PR touches), and the rest of the affected list. Checkable reason: the diff changes only the bodies of nine cell renderers reached throughgetCellRendereror direct import, and only for inputs whose coerced text is blank; the packages sent to CI reach those renderers only through the plugins run here.Boundary and out of scope
{}is untouched everywhere (the objectui#8481 fence);jsonstill draws the array literal andfilestill states0 files(pinned again here as THE BOUNDARY).markdown/html/richtextpaint a childless container for[]— the blank class, not this card's), objectui#8581 (DateCellRenderer's dash for an unparsable string), objectui#8582 (BooleanCellRendererreads'false'and{}as checked).patch— a correction of false output, not a capability. No README or guide documents the empty-value behaviour of these renderers, so no docs moved.Session:
session_01YBWFb5YgMU5dw8p2VKj16S(written in prose so it survives a later edit of this body).🤖 Generated with Claude Code
Generated by Claude Code