Repository navigation
fix(plugin-form): merge object-form.customFields over the metadata-generated field set - #9948
Conversation
`customFields` is registered as "Field definitions merged over the set generated from object metadata. With inline definitions and no data source, this becomes the only field source." The renderer replaced instead: a non-empty `customFields` took an early return that called `setFormFields(schema.customFields.map(normalizeVisibility))` above the metadata branch, and the object's schema was never even fetched — while the per-member merge the prose describes sat inside that branch as dead code, its `customFields?.find((f) => f.name === name)` only ever running over an empty array. The declared contract wins (objectui#9778, maintainer ruling 2026-09-18): the metadata is now fetched whenever an object name and a data source are present, and the members merge over the generated set — override in place, keep what no member names, append what metadata never declared. With no data source there is nothing to merge over, so the members stay the only field source, exactly as the registration's second sentence says; an object the adapter cannot describe falls back to that same source instead of turning a form that used to render into an error panel. The member pin flips with it, one row per direction, and its `getObjectSchema` row inverts from "never fetched" to "fetched once". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
|
changeset-claim-re-read ✅ Nothing to re-readNo pending changeset names a file this change touches, no address in this pull request's own prose points at a line this change moves, and no changeset here negates a package it declares. An earlier revision did. That request to re-read does not apply to the current diff. This comment is updated in place on every re-run rather than posted again, so the thread does not grow one per push. Compared the checked-out tree with |
✅ 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
|
…name its falsifier `8738-object-form-fields-description.md` publishes verbatim into the CHANGELOG and still read "silently skipped by `SimpleObjectForm` … and by `buildFlatFields` … — no throw, no warning, no empty-state". Both named read sites now call `warnUnresolvedTopLevelField`, so the silence is gone. The claim ROTTED, it was not born false, and the two take opposite repairs: the body landed in `fd9bf26df0` at 2026-09-09T14:28:48Z and `8fda009057` (objectui#8859, route 1 of the same card) added the de-duplicated `console.warn` at 2026-09-09T16:00:31Z — 92 minutes later. So the original reading is kept and dated rather than rewritten, with the falsifier named; rewriting it would erase a true reading and misdate the behaviour for every CHANGELOG reader. Read as a paragraph, not a line: the same fact also dated the opening "route 1 … is a separate ruling still pending", and "Behaviour is unchanged" was judged in the same pass — still true of this entry's own change (a registration description), now spelled "unchanged by this entry" so the scope cannot be read as a claim about the release. What did not change is stated too: the member is still skipped, with no throw and no empty-state. Prose only — frontmatter and semver level untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
✅ 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
|
Ejected from the merge queue on a textual conflict. One file conflicted, `packages/plugin-form/src/ObjectForm.tsx`, in one hunk — the inline-fields effect, which objectui#9778 (landed as part of #9948) and this branch both edited. Resolved as a UNION, because the two sides answer different questions and neither is preferred: - this branch changed WHAT seeds the form: `setInitialData(schema.initialData || schema.initialValues || {})` becomes `setInitialData( resolveInitialRecord(schema))`, the per-member merge; - objectui#9778 changed WHEN the loading flag may drop: inline members no longer short-circuit the metadata read, so `setLoading(false)` is now conditional on nothing being about to be fetched, with the comment that states why. Both are kept verbatim. The effect's dependency array auto-merged to the union of what the two sides read, which is correct for both. The branch's second `ObjectForm` seed site, in `fetchInitialData`, auto-merged untouched. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
Fixes #9778
Clause-②: no
object-form.customFieldsnow MERGES over the metadata-generated field set, per the director-seat ruling on the card (batch #161 item 5, summon #24, maintainer 「其他同意」 2026-09-18T12:07Z). The registration description is the contract and is unchanged.Clause-② tested against this diff, not inherited: the diff adds 0 lines containing
export(lit control on the same corpus: 18 added lines containcustomFields) and no new key on any published payload — an existing branch is made reachable. Answer stands atno.What changed
ObjectForm.tsx, three sites:customFieldsused to runsetFormFields(schema.customFields.map(normalizeVisibility))and return above the metadata branch, so the object's schema was never even fetched.objectNameand a data source are present, inline members or not — without the generated set there is nothing to merge over, and the merge lookupschema.customFields?.find((f) => f.name === name)stays the dead code objectui#8071 measured. The members-only schema (an object with no fields) became the fallback rather than the inline path's fixed answer: it is what the registration's second sentence describes, "with inline definitions and no data source, this becomes the only field source".setFormFields(generatedFields).Two smaller consequences, both deliberate:
willFetchDatatail clears it. Without this an emptyformsat on screen for the duration ofgetObjectSchema.getObjectSchemathat throws (or returns nothing) now falls back to the members-only source when inline members are present, instead of replacing a form that used to render with an error panel. The metadata is an overlay on this path, not a prerequisite.The pins, one row per direction
objectFormCustomFieldsMembers-8071.test.tsxflips from pinning the replace behaviour to pinning the merge:name; the merged set iscustomer, note, amount, zzgetObjectSchemainverted fromtoBe(0)totoBe(1)customer/amount, which no member names, still render with the object's own labelscustomFieldsis UNAUTHORED — the read is.length > 0, not truthinessOne collateral red, repaired in place and called out here:
ObjectForm.mobileFullscreen.test.tsx's "commits a fullscreen edit back into the form state" mounts acustomFieldsmember namedsummaryagainst an object that also declareschangelog, so the merge draws two expand affordances and an unscopedfindByTestIdfound both. The lookup is now scoped to the field the case is about; nothing else in the file moved.Reverse verification (ablation)
The fix was committed first, then
ObjectForm.tsxalone was reverted to this branch's base commit and the flipped pins re-run:objectui#9778markers went 5 to 0 and the replace pathsetFormFields(schema.customFields.map(normalizeVisibility))came back (1 occurrence); blob85d6c1c4against HEAD'scd4bd869expected [ 'zz', 'note' ] to deeply equal [ 'customer', 'note', 'amount', 'zz' ];expected +0 to be 1;expected null to be 'Customer';expected [ 'zz', 'yy' ] to deeply equal [ 'customer', 'note', 'amount', …(2) ]), rows 5-6 green as they should be, since neither is about the mergegit checkout HEAD -- ObjectForm.tsx(trap-guarded, absolute path): blob back tocd4bd869,git diff HEADempty, markers back to 5Tests
Run at
096496e052:pnpm --filter @object-ui/plugin-form test— 100 files passed, 940 passed | 1 skipped (941)pnpm --filter @object-ui/plugin-form type-check— exit 0 (tsc --noEmitplustsconfig.test.json), afterpnpm --filter '@object-ui/plugin-form^...' build— without that dependency-closure build it reportsTS2307 Cannot find module '@object-ui/*'on every workspace import, which is a missingdist, not a type errorcustomFieldsthrough:packages/plugin-view/+packages/plugin-designer/+registry-inputs-spec-parity.test.ts+public-block-binding-reach.test.tsx— 66 files, 757 passed. The fenced ledger's counts do not move, exactly as the dispatch predicted, so it was not edited.check:control-bytes,check:new-line-citations,check:changeset-claims,check:pending-changeset-literals,check:test-path-roots,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:element-data-source-declaration,check-changeset-presence.mjs,check-changeset-no-major.mjs— all exit 0.eslinton the three changed files: 0 errors (64 pre-existingno-explicit-anywarnings).check:sdui-registration-pins— NOT MEASURED: exit 2 is its own prerequisite refusal ("No console build to weigh at apps/console/dist/assets"), not a red. This diff adds and removes no registration; CI builds the console and runs it.Acceptance notes
Out-of-scope findings, neither touched here:
ObjectFormspreads the schema —customFieldsincluded — intoDrawerForm/ModalFormforformType: 'drawer' | 'modal', and each of those runs its ownif (schema.customFields?.length) { setFormFields(schema.customFields); }. So an author who takes the registration at its word gets a merge on the flat presentation and a replacement on the other two, from one declared key. Out of this card's file surface (the ruling namesObjectForm.tsx) and it needs its own pins. Dedupe words:DrawerForm customFields replace·ModalForm customFields replace·presentation arms merge parity·formType drawer inline fields·customFields merge unreachable.object-form.customFieldsstill describes the replace behaviour in prose ("they REPLACE the metadata-generated set …getObjectSchemaasserted at zero calls … handed back as a finding rather than repaired"). It is reviewed prose, not a computed assertion — its counts do not move and its test stays green (measured above) — and the file is held by open PR objectui#9924, so it was not edited. Successor: whoever lands objectui#9924, or the next objectui#8071 slice that edits that ledger.🤖 Generated with Claude Code
https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
Generated by Claude Code