Repository navigation
fix(console): FormPage degrades an unregistered widget to the type renderer (objectui#10472) - #10553
Conversation
…nderer The widget leg of resolveFieldWidgetKey took any authored widget and handed it to resolveFormWidgetType, whose answer for a spelling nothing registers is the text widget, so a select field with an unknown widget rendered a free-text input. The spec's FieldSchema.widget declares that an unregistered widget degrades to the type renderer. The leg now asks the same resolver whether the spelling names anything and falls through to the arity and type legs when it does not. Pinned with real widgets: an unknown widget on a select renders the select on both authored places, a registered widget still wins, and a retired spelling still reaches its tombstone. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
…rmpage-widget-degrade
|
changeset-claim-re-read
|
✅ 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
|
Contract reviewServed-tier: Reviewed against merge-base ① Derived judgmentsThe binding text. Installed
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
Fixes #10472
What changed
FormPage'sresolveFieldWidgetKeytook an authoredwidgetwhenever one was present and returnedresolveFormWidgetType(widget). That resolver answers a spelling nothing registers with thetextwidget, so aselectfield carrying an unknownwidgetrendered a free-text input. The spec'sFieldSchema.widgetdescribe (installed@objectstack/spec@17.4.0, read verbatim; its placeholder is spelled WIDGET here because GitHub strips angle-bracket fragments): "Form widget override — names a registered field component (resolved asfield:WIDGET) to render this field instead of thetypedefault. Degrades to thetyperenderer when unregistered."The
widgetleg now runs only whennamesRegisteredWidget(widget)holds. Otherwise the row falls through to the arity leg and thetypeleg of the same resolver.apps/console/src/components/FormPage.tsx(+44 / -6), newapps/console/src/components/FormPage.widgetDegrade-10472.test.tsx(+135), new.changeset/10472-formpage-widget-degrades-to-type.md(+19,@object-ui/consolepatch).resolveFormWidgetTypeitself, which is the same call the leg already made and the resolver whose loadersgetLazyFieldWidgetmounts. It is read for its documented unknown answer ("anything unknown falls back totext"): a spelling answered withtextthat is not itselftextnamed nothing. A registered key, a retired spelling (its tombstone) and an alias the resolver knows keep resolving as before.Premise check (H1–H4)
origin/main111fa4c. The widget leg wasif (field.widget) return resolveFormWidgetType(field.widget);.111fa4cwith real widgets on/forms/:name:{ type: 'select', options, widget: 'no_such_widget' }renderedINPUT[type=text].{ type: 'boolean', widget: 'no_such_widget' }renderedINPUT[type=text].735c9edwith the same probe: the select field rendersBUTTON[role=combobox]plus the hiddenSELECT. The boolean field rendersBUTTON[role=switch].{ type: 'password', widget: 'no_such_widget' }rendersINPUT[type=password].{ type: 'select', multiple: true, widget: 'no_such_widget' }renders the multiselect group.widget: 'radio'on a select rendersradiogroup.widget: 'owner'renders the tombstone.widget: 'secret'on a text field rendersINPUT[type=password].widget: 'json'on a text field renders the code widget'sTEXTAREA.packages/components/src/renderers/form/form.tsx), was measured with the dev's probe shape and real widgets afterregisterAllFields(). It shows the same symptom, and it is not changed here.select/field:selectwithwidget: 'no_such_widget', and a metadata-carriedfield.widget: 'no_such_widget', all renderINPUT[type=text].field:booleanwith the same widget rendersINPUT[type=text]too.735c9ed:{ type: 'field:text', widget: 'secret' }and{ type: 'field:text', widget: 'ui:password' }both renderINPUT[type=password].resolveWidgetType(boolean seeding), the cascade-clear effect's ownwidget || type, and the renderresolvedType. It would also have to update the core validator'sresolveAuthoredFieldWidgetId, which says it mirrors that precedence.field:-qualified spellings are not ones the shared resolver knows, so after this PR they degrade to the type inFormPage. Measured at735c9ed:widget: 'field:owner'on auserfield renders the person picker (lookup-trigger), with no refusal. Before this PR it rendered a workingINPUT[type=text], also with no refusal.widget: 'field:textarea'rendersTEXTAREAon atextareafield andINPUT[type=text]on atextfield.field:-qualified spellings as registry keys: the tombstone forfield:owner, andTEXTAREAforfield:textareaon a text field.widget: 'owner'still reaches the tombstone inFormPage, and that is pinned here. That the tombstone names the row's livetype(data-retired-field-type="user") is objectui#10471, left as is. Whether afield:-qualified spelling is authorable on a form view at all was not measured here (the card lists it as unmeasured).Tests (HEAD
735c9ed, after mergingorigin/main)pnpm exec vitest run apps/console/src/components/FormPage.widgetDegrade-10472.test.tsxgave 4 passed (4) onfb70e86. The same 4 are inside the directory run below.pnpm exec vitest run apps/console/src/components/gave Test Files 31 passed (31), Tests 407 passed (407). VITEST-EXIT=0.pnpm turbo run type-check --filter @object-ui/console --concurrency=2gave Tasks 36 successful, 36 total. VERDICT command-exit 0.tsc --noEmit --listFilesinapps/consolelists the new test file (1 hit). TSC-EXIT=0.pnpm exec eslinton the two changed source files reported 0 errors and 20 warnings. All 20 are outside this diff's hunks (react-refresh / no-explicit-any / set-state-in-effect, already present).check:control-bytes,check:test-path-roots,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:new-line-citations(0 new),check:changeset-claims,check:pending-changeset-literals,check-changeset-presence.mjs(1 changeset for 1 released package),check-changeset-no-major.mjs,check:unreferenced-sources,check:comment-mask-corpus, andcheck-governed-queue-guard.mjs --test(NOT GOVERNED).Ablation (fix committed first, mutated through
ablation-replace.mjs, restored to HEAD with an emptygit diff HEAD)if (field.widget) {. Result: 2 failed | 2 passed (4). The two degrade pins went red and both controls stayed green.namesRegisteredWidgetwas made to always answer false. Result: 2 failed | 2 passed (4). The registered-radiocontrol and the retired-ownercontrol went red, and the degrade pins stayed green.Acceptance notes
.changeset/10179-formpage-shared-field-resolver.mdsays "An authoredwidgetwins overtype". After this PR that holds only for a widget that names something registered. This PR's changeset states the refinement, and both publish in the same release. That body was not edited because it is outside the claimed file surface.FormPagestill resolves alias spellings inwidget(jsonrenders code,secretrenders password), while the record form treats them as unregistered (jsonrendersINPUT[type=text]). Both were measured above. That difference existed before this PR and is recorded here, not decided.Generated by Claude Code