Repository navigation
feat(components,i18n): a create form says when the server will fill a field (objectui#12108) - #12120
Conversation
… field (objectui#12108) A field whose defaultValue is a CEL expression or a runtime token is left empty on a create form, and the submit omits it so the server resolves the default at insert. The form said nothing about it: the field opened empty, or on "Select an option". The form renderer now draws "Set automatically when saved." in the field's description slot while the field is server-owned (isServerOwnedValue, the classifier the create-mode required suppression already reads) and its live value is empty by isMissingForRequired, the predicate omitServerResolvedDefaults drops the key by. A typed value removes it. It is not drawn on a control that takes no input, so a view-mode form, which the renderer reads as create, shows none. One locale key, form.serverDefaultHint, in all ten packs. Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz Co-authored-by: Claude <noreply@anthropic.com>
…changeset (objectui#12108) The plugin-form README's create-form table and the CRUD guide now say a field left empty for the server carries "Set automatically when saved.", and when it does not (edit, a control that takes no input, a boolean). The classifier's consumer list in server-owned-value.ts and the schemaDefaults.ts docblocks name the hint as the reader it now has. The README's six cross-file line addresses are re-cited by symbol or quoted text, as AGENTS.md #11 asks of a touched file. Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz Co-authored-by: Claude <noreply@anthropic.com>
… (objectui#12108) The help line under a field was a module-level `FieldDescription` component, which added a react-refresh/only-export-components warning to form.tsx. It is now `renderFieldDescription`, the shape of `withReadonlyHostGroup` beside it; the DOM it draws is unchanged. Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz Co-authored-by: Claude <noreply@anthropic.com>
✅ 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: Inputs read: card objectui#12108 (body and both comments: the Claim and the os-dev-report), PR objectui#12120 (body, the 17-file list, the net diff against Check-runs on the head, read at 2026-10-11T05:43Z: 31 success, 3 skipped (the two coverage jobs and dependabot), 8 still ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged:
Not judged here: the live-browser look of the hint; the eager-closure delta (Bundle Analysis success; the Console Performance Budget comment on the PR thread reads PASS). ② Semver level
③ Boundary flags
Deviations
Out-of-scope findings
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #12108
Clause-②: yes
Refs: #11914
What changes
A create form now says when the server will fill a field. A field whose
defaultValueis a CEL Expression envelope ({ dialect: 'cel', source: 'today()' }) or a runtime token (NOW(),current_user) opens empty: the client cannot evaluate it, and the submit leaves the key out so the server resolves it at insert. The form renderer now draws Set automatically when saved. in that field's description slot while the field is empty. One locale key,form.serverDefaultHint, in all ten packs.Where it lands, and why not where the claim expected
The claim's file surface named
plugin-formorfields. Measured, the code that draws the field row ispackages/components/src/renderers/form/form.tsx(renderFormField), so the hint lands there, per the dispatch's producer-side clause.schemaDefaults.tsgets docblock lines only.fieldData?.placeholderinTextField,config?.placeholderinSelectField), not off the host'splaceholderprop, andDateField,DateTimeField,TimeField,BooleanFieldandUserFielddraw none. A placeholder could not reach the date field atoday()default sits on.ObjectForm,ModalForm,DrawerForm,TabbedForm,SplitFormandWizardFormall render through theformrenderer, which already computesisServerOwnedValue(field, isCreateForm)per field for the create-moderequiredsuppression, and holds the live value.serverOwnedis computed once inrenderFormFieldand read by both therequiredsuppression and the hint. Emptiness isisMissingForRequired, the predicateomitServerResolvedDefaultsdrops a key by, so the hint shows exactly while the submit will leave the key out. A typed value removes it.FormDescription, after any authoreddescription;FormControlalready names that element's id in the control'saria-describedby.previousValuesin edit mode only, so the renderer reads amode: 'view'form as create; every field there is disabled, and this gate keeps the hint off it. The cost: a locked field on a real create form gets no hint, which is a missing hint, not a wrong one.Coverage, measured
serverDefaultHint-12108.test.tsxdrivesObjectFormthrough all seven routes (flat, sections,modal,drawer,tabbed,split,wizard) with the real registered widgets:TextFieldDateFieldSelectField; the trigger still reads "Select an option"NumberFieldNOW()tokenDateTimeFielddescriptionTextFieldSelectField, seeded "Open" as todayTextFieldAlso pinned: the hint goes when a value is typed and comes back when it is cleared; it is in the control's
aria-describedby; edit and view forms (flat and modal) show none.Not covered: a boolean (see Acceptance notes), and the console's second form renderer,
apps/console/src/components/FormPage.tsx(/forms/:name,/f/:slug).The required-field measurement
A required field with a CEL default does not block Save. Pinned for
dateandselectin six routes (flat, sections, modal, drawer, tabbed, split): no required marker,createis called, both keys are absent from the payload.createDefaults.test.tsxalready pinned the same rule ontextfields. The wizard's submit is its last-step button and is not driven here; its hint is pinned.isRequiredInFormin the builders andresolveFieldRuleState'sserverOwnedValuein the renderer, both onisRuntimeDefault. This PR does not change either.origin/maina18c5149): the insert path runsapplyFieldDefaultsbeforevalidateRecordInScope(the[#4633]note invalidateDatastates it), andapplyFieldDefaultsevaluates a{ dialect, source }envelope for a field that arrives absent or null. The server fills the field; it does not refuse it.applyFieldDefaultslogs a warning and leaves the field unset, and a required field is then refused by the server. Nothing on the client can see that case.Ablation
At
662a0ba, throughablation-replace.mjs(anchor hit once, blob changed, restored blob equals HEAD,git diff HEADempty):Gates
Head
662a0baunless noted.@object-ui/componentsexit 0 at662a0ba;@object-ui/i18n,@object-ui/core,@object-ui/plugin-formexit 0 atad59f8b(ad59f8b..662a0batouchesform.tsxonly, a module-private change). Each output echoestype-check.--listFileson plugin-form'stsconfig.test.jsonlists the new test.packages/plugin-form/173 files, 2127 passed, 1 skipped;packages/components/src/renderers/form/71 files, 495 passed, 17 skipped;packages/i18n/82 files, 1316 passed, 13 skipped;packages/core/src/validation/9 files, 152 passed.check:i18n-keys,check:i18n-drift,check:i18n-dead-keys(the new key is not a candidate),check:i18n-designer-parity,check:new-line-citations(0 new),check:control-bytes,check-changeset-presence,check-changeset-no-major,check:changeset-claims,check:pending-changeset-literals,check:test-path-roots, the threecheck:vi-mock-*,check:esm-specifiers,check:self-import,check:phantom-deps,check:doc-types,check:doc-example-ids,check:doc-fences,check-doc-links,check:doc-example-readers.check:eager-closureandcheck:eager-locale-catalogues: exit 0 at head and at base5f75cfa, from two consolevite builds. Delta: +128 B gzipped (+301 B raw) over the same 290 eager chunks:ui-components+110 B,i18n-locale-en+21 B, the rest one-digit hash-name noise.enstays the only eager catalogue.eslint --print-configresolves a config for each of the 14, none ignored. Count: 14 files in the--format jsonoutput. Invariance:eslint.config.jssets noparserOptions.projectorprojectService, and no rule ineslint-rules/reads the disk, so this diff cannot move an untouched file's verdict.form.tsxcarries 70 warnings, equal to its base blob; the new test adds 5no-explicit-anywarnings, theas anypatterncreateDefaults.test.tsxuses.check:doc-snippetsandcheck:doc-examples(prerequisite: a 34-package build). Neither doc's diff touches a fenced block: 0 of 33 changed lines sit inside a fence, and the same detector finds 341 and 204 fenced lines in the two files.check:readme-exportsneeds every package built; this README diff touches no export listing. Repo-widepnpm lint, the fullpnpm testand a live browser pass are CI's or were not run.Clause-② surface
renderFieldDescriptionis module-private).form.serverDefaultHint, in ten packs.TranslationKeyswidens, so@object-ui/i18nisminor.data-server-default-hint="true", a locator in the manner ofdata-required-marker.Acceptance notes
false(the cloud#972 seed inform.tsx'sdefaultValues), so the submit sendsfalseand the server stores it instead of evaluating the declaration. Measured in this pin's first run: the payload held{ status: 'open', active: false, … }. No hint is drawn there, which is the true answer. Not changed here: exempting server-owned booleans from that seed is a UX decision (what a switch shows while undecided), not a mechanical one. Reported to the seat.FormPage, the console's second form renderer, already leaves server-owned blanks out and lowersrequiredon them, but draws no hint. It is outside this card's file surface; reported.Session:
https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKzGenerated by Claude Code