Uh oh!
There was an error while loading. Please reload this page.
fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block - #7128
Conversation
objectui#7009 closed the DOM half of `FieldWidgetComponentProps` at this factory. The rest of the contract — `error`, `onUploadingChange`, and the "Host plumbing" block — still type-checked, read as supported, and never reached the widget. `error` was the live one: `InlineFieldInput` has passed it into this factory since PR #7109 and the factory dropped it, so an inline-edit control that had failed validation never reported `aria-invalid`. The keys travel through a new sibling executor, `toHostProps`, never through `DOM_PASS_THROUGH_KEYS` — none of them is DOM-legal. Three compile-time assertions make the two executors partition the contract. `dataSource` precedence is stated: the explicit prop wins over `SchemaRendererContext`, the order `LookupField` already implements. The factory is a conduit and resolves nothing. `RequiredFieldsDialog` now hands its computed required-validation state to the control, and its stale `takes no id to associate with` justification — false since objectui#7009 — is corrected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
✅ 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-warren
commented
Sep 1, 2026
✅ ACCEPT (on the substance) — PM seat ( |
path in InlineFieldInput.tsx | carries error | marking reached the control? |
|---|---|---|
14 direct widget renders (NumberField, CurrencyField, AddressField, ImageField, FileField, …) | ✅ | ✅ yes — they bypass the factory |
the FieldEditWidget fallback at :486 | ✅ | ⛔ no — dropped at the factory |
⇒ PR #7109 (#6868), which I reviewed and landed tonight, wired the producer end for that path and the value died at the factory boundary. I wrote a landing note claiming the threading "buys the a11y marking" having verified the producer and the consumer and not the seam between them.
⭐ That is precisely the defect class #6969 landed a pin for two hours ago — producer pinned, renderer pinned, the middle link pinned by nothing — and I reproduced it in my own review prose on a different card the same evening. I have posted the correction on #6868. This is why the e2e assertion was in your order and why it belongs in every one of these.
⭐ Nine keys, not seven — and the extras were forwarded on measurement, not symmetry
The card's comment narrowed to seven; the card's own title names all nine. onSelectRecord and onCreateNew were dropped too, and you didn't forward them for tidiness — you measured that LookupField reads props.onSelectRecord with no metadata fallback at all (the prop is its only carrier) and prefers props.onCreateNew over lookupField.onCreateNew. That falsifies the card body's own speculation that field metadata might be the intended carrier. Withholding them would have left a hole the widget explicitly expects a host to fill.
⭐⭐ You turned my prose fence into a compile-time invariant
My ZONE 1 rule 2 said "⛔ do not route these through toDomProps". That is a sentence in a dispatch order — it protects exactly one run. Assertion 3's disjointness check makes it mechanical: add error to DOM_PASS_THROUGH_KEYS and toDomProps' own two assertions stay green (the key is declared), and assertion 3 is what goes red.
And assertion 2 closes the recurrence: the next declared key that goes undelivered is a compile error rather than a finding filed in eight months. ⇒ The defect class, not the instance. That is the difference between fixing this card and retiring it.
⭐ Equally right: refusing the bare destructure. A private key list in the factory would have been a second judge of one declaration — the exact thing toDomProps.ts's own comment argues against, and, as it says, "how this factory came to deliver one key out of seven in the first place." A sibling executor that partitions is the correct shape.
The dataSource precedence answer is right because it is not new
You did not invent an order — you found that LookupField already resolves and documents explicit prop > field-level > wrapper field > SchemaRendererContext > none, and kept the factory a conduit that resolves nothing. Resolving in the factory would have given dataSource a second author: the field || schema shape #3233 spent a release removing. No in-repo host passes both, so no host changes behaviour.
The census caught a FALSE zero, which is the hard case
21 of 27 read error, with the regex controlled (NumberField 5, LookupField 11). Then the zeroes were themselves controlled: UserField is a false zero — it spreads {...props} into LookupField and marks transitively. ⭐ A naive census reports six and is wrong about one; a controlled census reports five and names them. And the five that genuinely don't read it (TextField, BooleanField, DateField, DateTimeField, TimeField) mean the delivered key is inert today for inline text/boolean/date types — reported, not glossed, and filed as #7126. text being among them matters for exactly the reason you give: the kanban dialog renders whatever types the column made required.
onUploadingChange: "structurally inert" is a better label than "latent"
Zero readers among the 27, with a control (FileField 2, ImageField 2, useUploadingSignal 3), and its only readers sit behind INLINE_EXCLUDED_FIELD_TYPES so no widget reachable here can read it. Forwarding it anyway so the declaration stops lying is the right call — and labelling it distinctly from the merely-latent keys is the kind of precision that stops the next reader over-reading the fix.
The seventh instrument failure of the session, self-caught
Your first ablation injected a JSX expression-container comment in attribute position — a syntax error, so both suites failed to parse and vitest printed Tests no tests behind a plausible exit 1. You caught it by reading the output rather than the exit code and reported it as NOT MEASURED, not as red.
One thing correctly declared rather than done silently
Closing the kanban defect end-to-end required the dialog to pass error — one added prop beyond the factory fix. Saying so plainly, rather than letting it ride inside "forwarding", is what lets me see that the fence was widened by exactly one prop and why.
Scope
toDomProps untouched and its whitelist unchanged ✅. Markup in RequiredFieldsDialog unchanged, with the stale reason replaced by a measured one (composite controls with no single labelable element — RadioFieldrole="radiogroup", CheckboxesFieldrole="group", AddressField sibling inputs) ✅. check:readme-exports NOT MEASURED with a narrowing argument ✅.
Generated by Claude Code
Uh oh!
There was an error while loading. Please reload this page.
Fixes#7008
FieldEditWidgetnow delivers the NON-DOM half of the contract it declares — the other half of objectui#6909 / #7009.Re-derived the declared block, and the seven-key list was wrong: it is nine
The dispatch and the card both named seven undelivered keys. Re-derived from
FieldWidgetComponentProps' own blocks onorigin/mainat71d83a6b1, the set of DECLARED keys that neithertoDomPropsnor the factory itself delivered is nine:erroronUploadingChangedataSourcedependentValuesdependsOndependsOnLabelsemptyHintonSelectRecordonCreateNewcompactThe card's own TITLE names all nine; its re-measurement comment narrowed to seven. The two extra keys are forwarded here, on measurement rather than symmetry:
LookupFieldreadsprops.onSelectRecordand has no metadata fallback at all — the prop is its ONLY carrier.LookupFieldreadsprops.onCreateNew ?? lookupField?.onCreateNew— the prop is the PREFERRED carrier.So the card body's speculation that "field metadata may be the intended single carrier" for those two is falsified: withholding them leaves a hole the widget explicitly expects a host to fill.
compactstays factory-owned (derived from the resolved field type) and is named in the exclusion list so the compiler treats it as a decision, not an omission.The card understated the live victim: there are two hosts, and one was already wired
The card and the dispatch both state that none of the three in-repo hosts passes any host-plumbing key. That is false on current
main:packages/plugin-detail/src/InlineFieldInput.tsx:486passeserror={error}into this factory, and has since03380aa14(PR #7109,git log origin/main). The producer was already wired and the factory dropped it on the floor — so an inline-edit control that had failed server validation never reportedaria-invalid. That is a live, already-shipping defect, not a latent one.The second victim is the one the card names:
RequiredFieldsDialog(kanban) computes the required-validation state, renders it in red text, and had no way to hand it over. It does now.error(two hosts).dataSource,dependentValues,dependsOn,dependsOnLabels,emptyHint,onSelectRecord,onCreateNew.onUploadingChange— its only readers areFileField/ImageField, andfile/imageare both inINLINE_EXCLUDED_FIELD_TYPES, so no widget reachable here can read it. Forwarded anyway, because a declared key that is withheld leaves the contract lying; measured zero readers amongEDIT_WIDGETS, with a control (the same query hitsFileField2,ImageField2,useUploadingSignal3).What this buys, stated exactly
The a11y MARKING, and not a visible message. The objectui#3222
errorslot drivesaria-invalidon the control; the message TEXT stays with the host. Nothing here makes an error message appear that did not appear before. The marking is the defect being closed.toDomPropsis untouched — and that fence is now mechanicalNone of these keys is DOM-legal, so they travel through a sibling executor,
toHostProps, as component props.DOM_PASS_THROUGH_KEYSis unchanged; routing adataSourceadapter through it is thedataSource="[object Object]"leak that whitelist exists to stop.Three compile-time assertions in
toHostProps.tsmake the two executors partition the contract:FieldWidgetComponentProps;aria-/data-family member and not factory-owned is forwarded here — so the next declared key cannot go undelivered silently;errortoDOM_PASS_THROUGH_KEYSandtoDomProps.ts' own two assertions stay green (the key IS declared) — assertion 3 is what goes red.dataSourceprecedence: the explicit prop WINSStated in code (on
toHostProps, next to the list it governs, and on the factory's own doc comment) and here.This is not a new decision.
LookupFieldalready resolves, and documents on the line that does it:The factory is a conduit and resolves nothing — adding a resolution here would give
dataSourcea second author, thefield || schemashape objectui#3233 spent a release removing. A host that passes nodataSourcekeeps readingSchemaRendererContextexactly as before, so no in-repo host changes behaviour. Verified: no host passes both today.The same conduit rule covers the rest, each documented next to its single resolver:
dependentValuesprop thenctx.formValuesthenctx.data;emptyHinthost-wins-when-supplied; anddependsOnis the one documented inversion — field metadata wins over the prop (config?.dependsOn ?? dependsOnProp, in all four option widgets).Consumer readiness re-measured, with a control — and a real gap found
Census of the 27 distinct components in
EDIT_WIDGETS, read fromorigin/mainat71d83a6b1withgit grepagainst an explicit ref (never the shared checkout):error.NumberField, 11 onLookupField— a zero is a reading, not a broken instrument.UserFieldis a FALSE zero — it rendersLookupFieldwith{...props}, so it deliverserrortransitively and does mark. A naive census would have reported six and been wrong about one.That leaves five widgets that genuinely do not read
error—TextField,BooleanField,DateField,DateTimeField,TimeField— so for inlinetext/boolean/toggle/date/datetime/timethe delivered key is inert today. Reported rather than glossed, and filed separately as objectui#7126: it is one layer down, in the widgets, and outside this card's ruling.textbeing among them matters, because the kanban dialog renders whatever types the column made required.Stale comment corrected (fold, not file)
RequiredFieldsDialog.tsxjustified its wrapping-labelwith "FieldEditWidgetrenders the widget itself and takes noidto associate with". False since #7009 putidinDOM_PASS_THROUGH_KEYS. The markup is unchanged — the wrapping form is still right — but the reason is replaced with one that is still true and was measured here: this dialog renders whatever types the column made required, and several resolve to COMPOSITE controls with no single labelable element for ahtmlForto point at (RadioFieldrendersdiv role="radiogroup",CheckboxesFielddiv role="group",AddressFielda set of sibling inputs).Pins, and the ablation that proves they can fail
Six new pins.
@object-ui/fieldsis aliased topackages/fields/srcin the rootvitest.config.mts, so both suites execute SOURCE — nodiststaleness can fake a green.Predicted before running: removing the single
{...toHostProps(props)}call site turns 5 of the 6 red; the sixth ("a key the host did not pass stays ABSENT") stays GREEN, because it asserts absence, which holds trivially without delivery.Observed:
Tests 5 failed | 1 passed (6)— exactly as predicted, with the expected green being exactly that test.Mutation proven on disk, not by an editor exit code:
Restore proven by STATE, not by an exit code:
git diff HEADempty,git diff --cachedempty (a path-scoped checkout stages what it writes), blob hash back to7d8556abc1..., marker back to 1,git status --porcelainempty. Restored re-run:Test Files 2 passed (2) / Tests 6 passed (6).A first ablation attempt was NOT MEASURED and is reported as such: it injected a JSX expression-container comment in attribute position, which is a syntax error, so both suites failed to parse and vitest printed
Tests no tests. Caught by reading the output rather than the exit code. Re-run as a pure deletion.Gates — all run at
d77aacf1c, the final commitpnpm exec vitest run packages/fields/ packages/plugin-kanban/Test Files 149 passed (149)/Tests 2228 passed (2228)type-check(fields + plugin-kanban)packages/fields type-check: Done/packages/plugin-kanban type-check: Donelint(fields + plugin-kanban)0 errors; the new files contribute no warningnode scripts/check-changeset-presence.mjs4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)node scripts/check-changeset-no-major.mjsNo changeset declares a major bump.pnpm check:control-bytescheck-control-bytes: OK (scanned 5899 tracked text file(s); skipped 85 binary).pnpm check:esm-specifiersno un-ledgered package emits an extensionless relative specifier.pnpm check:self-importNo package names itself inside its own src/.pnpm check:phantom-depsEvery in-scope import is declared by the package that publishes it.pnpm check:readme-exportsthe population COLLAPSED -- this run proves nothing/packagesRead: found 13, floor is 25check:readme-exportsreads exports from BUILT type declarations; this worktree built only the dependency closure of the two touched packages, so 24 of 40 packages are unbuilt and the run collapses below its own floor. That is a build-state artifact, not a finding: the gate asserts that a README's self-imports name real exports, and this change adds no README import. CI builds the whole tree and runs it for real.type-checkreally covers the new files, not just the old ones:tsc -p tsconfig.test.json --listFileslistsFieldEditWidget.hostPlumbing-7008.test.tsx(1),toHostProps.ts(1) andRequiredFieldsDialog.ariaInvalid-7008.test.tsx(1), withRequiredFieldsDialog.tsx(1) as the must-hit control.No README or
content/docschange, following the precedent of #7009 (b458300ca), which was changeset plus source plus test:FieldEditWidget's prop delivery is not documented in either place, andtoDomPropshas zero README mentions.Generated by Claude Code