fix(plugin-form): seed create forms from the object schema's declared defaults - #4068
Merged
Merged
Conversation
… defaults
A field declared `required: true, defaultValue: 'draft'` opened the console's
create dialog empty, with a required marker — forcing the user to pick a value
the system already knew, with side-effecting neighbours one click away.
The server was never at fault: omitting the field from a create request stores
the declared default (`ObjectQL.applyFieldDefaults`). The gap was container-side.
`ObjectForm` seeded its opening values from the object schema; the five other
object-form containers did not — their create branch set the form data to
`initialData || initialValues || {}` and never looked at the schema. The
console's create dialog is the global `<ModalForm>`, one of those five.
Modal / Drawer / Tabbed / Split / Wizard now seed through one shared module,
`schemaDefaults`, with three boundaries pinned in both directions:
- create only — `ObjectForm`'s pass ran in every mode, so an edit form showed
a default over a column the record leaves unset, arming a silent write of a
value the user never chose;
- static defaults only — `NOW()` / `current_user` tokens and CEL Expression
envelopes are instructions the server resolves per insert; `ObjectForm` had
been seeding them verbatim, submitting the literal text `NOW()` as a field
value and suppressing the resolution the declaration asked for;
- caller values still win over a schema default.
Field-level `defaultValue` only, never a select option's `default: true`: the
insert path resolves the former and nothing else, so seeding from the latter
would preselect values the server would never have applied.
Fixes #4047
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
This was referenced Aug 10, 2026
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 10, 2026 03:54
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Aug 10, 2026
…ubmittable on create (objectstack-ai#4069) (objectstack-ai#4084) `@objectstack/spec` lets `defaultValue` be a runtime instruction rather than a value — the `DEFAULT_VALUE_TOKENS` family (`NOW()` / `current_user`) or a CEL Expression envelope — which `ObjectQL.applyFieldDefaults` resolves per insert for any field arriving absent or null. objectstack-ai#4068 therefore leaves such fields empty in a create form: seeding the literal text `NOW()` and submitting it would suppress the very resolution the declaration asked for. Correct for an optional field; combined with `required: true` it deadlocked. The control opened empty, the client-side required rule refused the submit, and there was nothing sensible for the user to type. Measured on origin/main: `dataSource.create` was never called, on both the flat and sectioned paths. Per the maintainer's 2026-08-10 ruling on objectstack-ai#4069 (option A), in CREATE mode a runtime `defaultValue` now suppresses the client-side required rule and the field is omitted from the payload — omitted, not sent empty, because a rendered control registers regardless of seeding and would otherwise carry `undefined` (a key a data source may still write) or `''` (neither absent nor null, so it stores a blank and defeats the declaration). Seeding and the required rule read ONE predicate, `isRuntimeDefault`, so a form can never seed a field it also refuses to submit. Edit mode, static literal defaults and typed values are unchanged, each pinned in both directions. Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3 Co-authored-by: Claude <noreply@anthropic.com>
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 #4047
Premise: confirmed, and the producer is one level up from where the card looked
The card's mechanism hypothesis was that seeding logic exists in
ObjectForm.tsxand the create dialog "bypasses it — likely a wiring gap". Measured againstorigin/main, that is right about the symptom and wrong about the shape: there is nothing to wire.ObjectFormis not on the create dialog's path at all, andModalFormis not a consumer of it — it is a peer container with its own create branch, which simply never read the object schema:The same line, verbatim, in
DrawerForm,TabbedForm,SplitFormandWizardForm.ObjectForm(the flat container) was the only one of the six that seeded. The console's create dialog is the global<ModalForm>mounted byapp-shell'sAppContent, so nothing inapp-shellneeded touching — the defect and the fix are both inpackages/plugin-form.Reproduced as a failing test before any source change (
createDefaults.test.tsx, 9 red / 14 green onorigin/main): the four sectioned containers all showedexpected 'Select an option' to contain 'Draft'.The card's server-side ruling held up independently:
ObjectQL.applyFieldDefaults(packages/objectql/src/engine.ts) resolvesdefaultValueon the insert path for any field that arrives absent or null, which is exactly why the issue'scurlthat omitsstatusstoresdraft.Which spelling is honoured —
defaultValue, not the option'sdefault: trueThe card asked for this to be stated rather than invented, so: field-level
defaultValueonly.Both spellings do exist in the spec vocabulary —
SelectOptionSchemadeclaresdefault: z.boolean().optional(). But the vocabulary is not the contract; the insert path is, and it readsfield.defaultValueand nothing else. The only consumer of option-leveldefaultfound anywhere on the platform is a nullability heuristic inpackages/lint/src/validate-expressions.ts(isNullableField), which is an analysis, not an application.So a form that also seeded from option-level
defaultwould preselect a value the server would not have applied had the field been omitted — a UI-only default that disagrees with the stored outcome. That is the renderer-side second dialect AGENTS.md #0.1 forbids. No precedence is invented here: one key is honoured because one key is enforced. If option-leveldefaultis meant to mean "the initial value", that belongs at the producer, not here.Three boundaries, each pinned in both directions
1. Create only.
ObjectForm's pass ran in every mode:initialDatawins for keys the record carries, so a storednullwas safe — but a key the record omits entirely fell through to the default. An edit form then displayedDrafton a column the row has never had set, arming a silent write of a value the user never chose on the next save of any other field. It is now gated on!schema.recordId || schema.mode === 'create'— deliberately the same "no persisted record" test the data-fetch effect two hundred lines above already uses, so the two cannot drift apart. The sectioned containers seed inside their existing create branch and never touch the edit path.The edit-direction test uses a record that omits
statusrather than one carryingstatus: null. The null variant passes with or without the gate (the explicit null overwrites the default either way) — it would have been green for an empty reason. Reverse-verified: replacing the gate withtrueturns exactly that assertion red,expected 'Draft' not to contain 'Draft', and nothing else.2. Static defaults only — and this half is a fix in its own right. A
defaultValuemay be an instruction the server resolves per insert: theNOW()/current_userruntime tokens (DEFAULT_VALUE_TOKENS) or a CEL Expression envelope{ dialect, source }.ObjectFormhad been seeding those verbatim, which the red baseline measured directly:That puts the literal text
NOW()into a datetime input and then submits it as the field's value — which suppresses the very resolution the declaration asked for, sinceapplyFieldDefaultsonly fills fields that arrive empty. Roughly a hundred platform objects declaredefaultValue: 'NOW()', so propagating the old behaviour into the console's create dialog would have been a regression shipped under a bug fix.schemaDefaultsseeds literals only, using the spec's ownisRuntimeDefaultTokenpredicate rather than a local copy of the token list.3. Callers still win.
initialData/initialValuesoutrank a schema default — a lookup prefill or a duplicate-record seed is the more specific instruction.Scope
One shared module,
packages/plugin-form/src/schemaDefaults.ts, consumed by all six containers. Fixing onlyModalFormwould have left the identical line in four siblings, so the same card returns the moment an object's form view is authored as a drawer or a wizard; the wizard's "create another" reset is included for the same reason (a second entry must open like the first). NoGridField/ grid-column file is touched, per the declared region exclusion against #3569.Verification
vitest run packages/plugin-form— 39 files, 375 tests passed.vitest run packages/plugin-view packages/plugin-designer packages/app-shell— 326 files, 3056 passed, 1 skipped (the consumers of these containers).pnpm --filter @object-ui/plugin-form type-check— clean (tsc --noEmit+ type tests), after building the dependency closure--filter '@object-ui/plugin-form^...'.pnpm --filter @object-ui/plugin-form lint— 0 errors.node scripts/check-control-bytes.mjs— OK.Changeset:
.changeset/create-form-seeds-declared-defaults-4047.md(patch, user-visible). Docs updated per AGENTS.md #2 —packages/plugin-form/README.mdgains a "What a create form opens with" table, andcontent/docs/guide/building-crud-app.mdstep 6 states the create/edit split.Generated by Claude Code