Repository navigation
fix(plugin-form): tabbed, split and wizard saves write through the simple form's outbound sequence (objectui#10563) - #10626
Conversation
…mple form's outbound sequence The simple form builds what a save writes in one inline sequence: sanitizeFormData (server-owned, computed, read-only, unknown keys, and the field-level-security verdict from fieldWriteGate), omitServerResolvedDefaults on a create, and dirtyEditPayload against the record it read on an edit. TabbedForm, SplitForm and WizardForm carried none of it, so an edit wrote the whole record (id, owner_id, created_by, updated_at, formula and FLS-refused columns) to both the host submitHandler and the OCC-guarded update, and a create seeded with a whole record posted it back. A simple form whose mobile stepper renders through WizardForm took the same route. The sequence moves into one internal function, formWritePayload, which the simple form now calls and the three layouts call in place of their raw payload. Each layout keeps the loaded-record snapshot the simple form keeps (snapshotLoadedRecord at its findOne, advanceLoadedRecord after a landed write). No new export. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC Co-authored-by: Claude <noreply@anthropic.com>
…nd pending changeset stop exempting tabbed, split and wizard The ObjectFormSchema.submitHandler JSDoc named the tabbed, wizard and split layouts and the mobile stepper route as handing over every collected value; that is false once they write through the shared sequence. The three arms' own JSDoc copies now say what an edit-mode handler receives, as the modal and drawer copies do. The README and the plugin-form docs page say every layout strips and diffs. The pending objectui#10156 changeset keeps its reading of that change and gains a dated note naming objectui#10563. Pins: an inline-member row per route and a master-detail header laid out tabbed. wizardSkipValidation's fixture field `owner` is renamed `assignee`: `owner` is on the server-owned roster, which the wizard now strips as every other layout does. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC Co-authored-by: Claude <noreply@anthropic.com>
…ued records No behaviour change; drops the four no-explicit-any warnings the new module carried. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC Co-authored-by: Claude <noreply@anthropic.com>
…d non-editable ObjectForm gates a section's field OBJECTS with applyFieldPermissions before routing to a layout, but a field a section names by bare string has no name to ask about until the layout resolves it, so the three layouts rendered a field the caller may read but not edit as a live input (the simple, modal and drawer forms render it disabled). With the save now stripping that field through formWritePayload, a value typed there would be dropped behind a 200. Each layout now passes its resolved section fields through the same applyFieldPermissions render pass ModalForm uses. Pin: a render row per route, with the editable field as its control. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC Co-authored-by: Claude <noreply@anthropic.com>
…bled Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC Co-authored-by: Claude <noreply@anthropic.com>
|
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
|
Fixes #10563
Clause-②: no
What this changes
TabbedForm,SplitFormandWizardFormnow build what a save writes through the same sequence as the simple form. That sequence issanitizeFormData, which applies the server-owned roster, thesystemflag, computed, formula and read-only columns, unknown keys, and the field-level verdict fromfieldWriteGate. Then comesomitServerResolvedDefaultson a create, anddirtyEditPayloadagainst the record the form read on an edit. Before this PR an edit on these layouts sent the whole record, includingid,owner_id,created_by,updated_at, the formula column and the field the caller may not edit. That payload went to both the hostsubmitHandlerand the OCC-guardedupdate. A simple form withmobile.stepperrenders throughWizardForm, so it is fixed by the same change.formWritePayloadinpackages/plugin-form/src/writePayload.ts. The simple form now calls it, and the three layouts call it in place of their rawdata/mergedData. There is no second sanitiser and no per-layout copy.index.tsxdoes not re-export the module, so the published surface is unchanged.snapshotLoadedRecordruns at its ownfindOne, the snapshot is cleared on the create / no-record branch, andadvanceLoadedRecordruns after a landed write. This is the sameLoadedRecordSnapshotobject and helpers, held in a ref like the simple, modal and drawer forms hold it. Each layout does its ownfindOne, so each takes its own snapshot. The wizard's existingpersistedRecordstate is left alone: it feeds field-rulepreviousand must not advance after a save.WizardFormreads the record itself on that route and seeds its values from that read, so its own snapshot is the right baseline.Premise check
Reproduced before any edit, on base
21d34d5e2. The pin was written first, then 20 rows, and run against the unmodified sources:Tests 16 failed | 4 passed (20). The 4 passing rows were the simple-form control. The tabbed edit row received{ created_by, id, name: 'Mine v2', owner_id, score, stage, total, updated_at }where the simple form sent{ name: 'Mine v2' }.Create path. The simple form sanitises a create too: the strip runs before the create/edit branch. The three layouts only ran
omitServerResolvedDefaultson a create. So the same gap existed on create, and this PR closes it. The pin'screaterow, a create seeded with a whole record, is red on base for all four non-simple routes.One step past the claim's file surface: the render half of objectui#10120
The claim scoped the three layout files to "their writePayload only". This PR also touches their
buildSectionFieldscallback, in its own commit4c2bf0c9f. That commit can be dropped on its own if the seat rules against it.Measured with a throwaway probe, not committed, under a principal with
deal.scoreeditable: false. The simple form, the simple form with sections,modalanddrawerrenderedscoredisabled.tabbed,splitandwizardrendered it as a live input.ObjectFormgates a section's field objects before routing, but a field named by bare string has nonameuntil the layout resolves it. Before this PR, typing into that input failed the save with a 403. With the new strip in place, the typed value would be dropped while the save returns 200. Each layout now passes its resolved section fields through the sameapplyFieldPermissionsrender passModalFormuses. I did this in place because the strip this PR adds is what makes that drop reachable, and because it is the other half of the same objectui#10120 invariant on the same three files. The claim's file surface needs the matching amendment.Behaviour a host or author can notice
submitHandleron these layouts receives, in edit mode, the changed fields, or the full stripped payload when nothing changed. A master-detail header laid outtabbedrendersTabbedFormwithMasterDetailForm'ssubmitViaBatchas its seam. Its parent operation now carries only the changed fields, as asimpleheader already did.MasterDetailFormandoccSave.tsxare not touched.owneris an example. The other layouts already did this.wizardSkipValidation.test.tsxused a business field literally namedowner, and it is renamedassignee. That keeps the test's meaning, and a note in its header says why.customFields, the object definition is not handed to the strip, which is the simple form's rule. An inline member the object does not declare is still written.isCreateFormMode(mode: 'create'or norecordId), the simple form's predicate. Before, it usedmode === 'create'.Docs and changesets
ObjectFormSchema.submitHandlerJSDoc in@object-ui/types: the sentence that listedtabbed/wizard/splitand the stepper as handing over every collected value is replaced. The rule now names every layout. The three layouts' ownsubmitHandlerJSDoc copies gain the edit-mode sentence the modal and drawer copies carry.plugin-form.mdxsection of the same name no longer exempt these layouts. They also say that every layout strips a create and renders a refused field disabled..changeset/10563-form-arms-write-payload.md,patchon@object-ui/plugin-formand@object-ui/types.@object-ui/typesis included because its published.d.tsJSDoc changes.Tests and gates, at
b89752678pnpm exec vitest run packages/plugin-form/ packages/types/plusapps/console/src/components/FormPage.outcomeToast.test.tsx,FormPage.sectionGroup.test.tsx,FormPage.test.ts,FormPage.viewSpec.test.ts,examples/schema-catalog/test/catalog-gallery-render.test.tsxandpackages/i18n/src/__tests__/raw-key-call-sites-3546.test.tsx, run from the repo root:Test Files 363 passed (363),Tests 7222 passed | 1 skipped (7223).packages/plugin-form/src/formArmsWritePayload-10563.test.tsx: 31 rows. There are six per route (simple as control, tabbed, split, wizard, and simple withmobile.stepper): edit, nothing changed, host seam, create, render and inline member. One more row covers a master-detail header laid outtabbed.b89752678: the four layout sources were put back to base blobs under the committed pin, with a trap restore checked by blob hash and an emptygit diff HEAD/--cached. The result wasTests 24 failed | 7 passed (31). The 7 passes are the six simple-control rows and the stepper render row. The stepper's fields were already gated by the simple form before the wizard saw them.turbo run build --filter='@object-ui/plugin-form^...'(11 tasks):pnpm --filter @object-ui/plugin-form --filter @object-ui/types run type-checkexit 0. Both packages echotype-check: Done.tsc -p tsconfig.test.json --listFilesincludes the two test files andwritePayload.ts. The@object-ui/typeschange is JSDoc only, with no type change, so no reverse type check applies.pnpm check:control-bytes:check-control-bytes: OK.pnpm check:new-line-citations:VERDICT new-cross-file-line-citations: 0 new citation(s).node scripts/check-changeset-presence.mjs:8 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s).check:pending-changeset-literals,check-changeset-no-major,check-changeset-fixed,check-doc-links,check:doc-fences,check:doc-types,check:doc-example-ids,check:unreferenced-sources,check:test-path-roots,check:handler-key-readsandcheck:installed-pin-claimsall exit 0.check-governed-queue-guard --test:NOT GOVERNED.pnpm check:changeset-claims(report-only) flags 21 pending changesets that nameobjectql.ts,ObjectForm.tsx,WizardForm.tsxorplugin-form.mdx. I read every paragraph. None concernssubmitHandleror a save payload, and all still hold. The one that did go false is the objectui#10156 entry, corrected above.check-changeset-overwritereports that modification as its legitimate "correcting a declaration" case..ts/.tsxfiles (8 files in the JSON report, 0 errors), per rule against base content linted at the same paths.react-hooks/exhaustive-depsgoes from 2 to 1 inTabbedForm.tsxand inSplitForm.tsx:objectSchemawas read but not listed. The newwritePayload.tshas 0 warnings. The new pin has 9no-explicit-any, fromas anyfixtures in the sibling files' style. Everything else is unchanged.eslint.config.jshas no type-aware linting (noparserOptions.project/projectService), so a verdict on an untouched file cannot move.pnpm check:readme-exports, reason: it needs every package built, and exitsthe population COLLAPSEDwith 25 of 40 unbuilt. The README edit is prose only, with no fenced code touched. CI builds everything and runs it.Ablation, at
b89752678Each leg went through
ablation-replace.mjs, which requires the anchor to hit once, verifies the write on disk and restores by blob withgit diff HEADempty.SplitFormrouted back to rawdata.Tests 5 failed | 26 passed (31): exactly the five split payload rows. The simple control and every other route stay green.formWritePayload, with the diff kept.Tests 15 failed | 16 passed (31): the nothing-changed, create and inline-member rows on all five routes. The edit and host-seam rows stay green because the diff alone keeps unchanged refused columns off an edit. That is why the nothing-changed row exists.TabbedFormrender gate removed.Tests 1 failed | 30 passed (31): the tabbed render row only.Acceptance notes
ModalFormandDrawerFormstill spell the same steps inline. They differ from the simple form in two ways: they hand the object definition to the strip whatevercustomFieldsholds, and they omit create defaults onmode === 'create'alone. They were not moved here because they are outside this card and neither difference was shown to break a save. Moving them ontoformWritePayloadwould be a separate change.MasterDetailForm) and objectui#10565 (occSave.tsx) remain open. The seat takes them serially after this lands.Attribution:
domain:uiseat #1, sessionsession_01BA3nKVUwKQJf8DBxrSVtNC.Generated by Claude Code