fix(plugin-form): the record dialog draws a { group } form section; ModalForm resolves its sections through the one resolver (objectui#11542) - #11551
Merged
objectstack-fleet[bot] merged 4 commits intoOct 3, 2026
Conversation
…s through the one resolver (objectui#11542)
`ModalForm` is mounted directly by the console's More actions › Edit / New
dialog and by action-opened modals, which spread the form view's sections
into it as authored. Only `ObjectForm` resolved `form.sections[].group`
(`withGroups`), so a `{ group }` section reached the dialog with no
`fields`, built an empty body and was dropped by the empty-body filter: no
tab in the tabbed layout, no header in the stacked one.
`ModalForm` now resolves its sections through `resolveSectionGroupReferences`
against the object schema it already loads, above both content layouts. The
call returns its input unchanged when no section uses `group`, and every
resolved member still passes `gateFields`.
Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
Co-authored-by: Claude <noreply@anthropic.com>
…oup }` sections (objectui#11542) Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2 Co-authored-by: Claude <noreply@anthropic.com>
…any` (objectui#11542) Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2 Co-authored-by: Claude <noreply@anthropic.com>
…e/issue-11542-modal-form-group-sections Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ 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
|
objectstack-fleet
Bot
deleted the
claude/issue-11542-modal-form-group-sections
branch
October 3, 2026 14:17
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 #11542
Clause-②: no
The measurement the card asked for
The card asks where the More actions › Edit / New dialog's tabbed form takes its sections: "either a branch before
withGroupsor the gate answering false there". Measured: a branch beforewithGroups, taken one level higher thanObjectForm. The dialog never entersObjectForm, so neitherwithGroupsnor its gatecanResolveGroupsruns on that path; the gate never gets the chance to answer at all.AppContent(the console's global record dialog) anduseActionModal(action-opened modals), both in@object-ui/app-shell, mountModalFormdirectly withformType: 'modal'and spreadresolveFormViewLayout(objectDef)into its schema. That helper copiesformView.sectionsas authored and setscontentLayout: 'tabbed'for a tabbed form view.ModalFormhad no call toresolveSectionGroupReferences. A{ group }section reached it with nofields,buildSectionFieldsbuilt it an empty body (it readssection.fields ?? []), and the empty-body filter that runs above the layout fork dropped it. Both content layouts lost it: tabbed (no tab) and stacked (no header).origin/main6f5719e1cwith the new pin and no source edit: 9 failed, 2 passed, the 2 passing legs being the hand-enumerated control. The tabbed leg read the tab keysidentity, contact_infowhereidentity, contact_info, buying_centrewas expected, which is the hotcrm symptom.The fix
ModalFormresolves its sections once, throughresolveSectionGroupReferences(the resolverObjectForm'swithGroupsuses, which derives fromderiveFieldGroupLayout), with the same four inputs:objectName,formType,objectDefandresolvable. The call sits above both content layouts.objectDefis the object schema the dialog already loads (itsobjectSchemastate), so the fix adds no request, and the form keeps showing its skeleton until that schema lands. The call returnsschema.sectionsitself when no section usesgroup. That includes every section listObjectForm's own modal route hands over, which arrives already resolved, so no other modal takes a new path.No assembly rule is re-implemented.
sectionGroups.ts,ObjectForm.tsxand everyapp-shellhost are untouched. Landing:packages/plugin-form/src/ModalForm.tsx, as the claim expected, plus the pin, one@object-ui/plugin-formpatch changeset and one README paragraph.The PM's six mechanism hypotheses, each measured
ModalFormdirectly, and the pin was red onorigin/mainas stated above.objectDefinput. The diff reads that existing state and adds no fetch.createandedit, and all of them were red before and are green after.gateFields, which isgateFormFields) exactly like enumerated ones, because that gate runs per section on the built fields, after resolution. The pin puts a read-denied member and a write-denied member inside the group: the first is not drawn, the second is drawn locked, and the third member stays live as the control, in create and in edit. On theObjectFormside, read but not pinned here: its pre-withGroupsapplyFieldPermissionsseesfields: undefinedon a group section, but every arm it routes to re-gates its resolved fields throughgateFormFields, so that ordering opens no path around FLS.plugin-formfinds one:FieldDesignerin@object-ui/plugin-designer, which builds its own sections in code (Basic / Type-specific / Advanced, each carryingfields) and never passes a form view's sections.ObjectForm'sformType: 'drawer'route resolves above its fork. The README now says that a directly mountedDrawerFormdoes not resolve them.Also measured: the master-detail arm (a form view with
subforms) hands raw sections toMasterDetailForm, which renders its parent throughObjectForm. A{ group }section there was therefore already resolved. A throwaway probe confirmed this for both content layouts and was deleted, not committed.Pins
packages/plugin-form/src/__tests__/modalFormSectionGroupReference-11542.test.tsx(new file, so the trunk's restamped comment line informSectionGroupReference-7051.test.tsxis untouched). It rendersModalFormthe wayAppContentdoes, with noObjectFormabove it, using the hotcrm shape: two hand-enumerated sections beside{ group: 'buying_centre', columns: 2 }. The object declares two groups and the form references the SECOND one, so a constant resolution draws the wrong members and the exclusivity assertion catches it.identity, contact_info, buying_centre, the group tab carries the group's label, and its panel holds exactly the group's members in declared order (none of the other group's).create()payload.form-section-group-unknownreport. That report only fires against a LOADED object definition, so this leg also shows the dialog'sobjectDefreaches the resolver.Reverse verification (from committed state)
The script restored
ModalForm.tsxto its6f5719e1cblob with atrapon EXIT, INT and TERM to put it back. It checked the file on disk: the resolver marker count went from 2 to 0, and the on-disk hash equalled the base blob146186f. It then ran the pin: 9 failed, 2 passed, the same split as the pre-fix run. Finally it restored withgit checkout HEAD -- PATHand checked the result: the on-disk hash equals the HEAD blob, the marker count is back to 2,git diff HEADis 0 bytes and the status is clean. The test imports../ModalFormfrom source (and@object-ui/plugin-formis aliased tosrcinvitest.config.mts), so there was nodistleg.Verification on the merged head
202a8c0origin/main6158e4c(the@objectstack/*17.6.0 trunk) was merged in with a merge commit andpnpm installwas re-run, as the PM asked. All of the following ran on202a8c0with a clean tree:pnpm --filter '@object-ui/plugin-form^...' build: exit 0, Scope 12 of 47 workspace projects.pnpm --filter @object-ui/plugin-form test: exit 0, Test Files 162 passed (162), Tests 1859 passed and 1 skipped (1860). The package has 162 test files at HEAD (161 at base), so the new pin is in the count.pnpm --filter @object-ui/plugin-form type-check: exit 0. The script name was echoed,tsc --noEmit && tsc -p tsconfig.test.json, and the test project lists the new file (--listFilesOnly).plugin-formpackage (its ownlintscript iseslint .): exit 0, 202 files in the JSON report, 0 errors.ModalForm.tsxhas 20 warnings, equal to its base blob's 20.--max-warningsis not set in this repo.ModalForm(via thesrcalias): 11 files, exit 0, 137 passed. These arecreateModalHonorsFormView,useActionModal.resolve,useActionModal,useConsoleActionRuntime,AppContent.declaredVisibilityKeys,DashboardView.modalTarget,RecordDetailView.modalDispatch,RecordDetailView.paramDialogTitle,recordFormNavigation,App.uploadAltitude-10131andFormPage.visibleWhen.pnpm check:control-bytes,check:new-line-citations(0 new citations),check:changeset-claims,check:pending-changeset-literals,check-changeset-presence,-no-major,-overwrite,-fixed,check-test-path-roots,check-type-check-coverage,check:unreferenced-sourcesandcheck-doc-links.check:readme-exports. Its precondition is every package built (it refused with "the population COLLAPSED", 25 packages unbuilt), and that is CI's lane. The README edit is prose only and adds no fenced block or import binding, which is what that gate judges.Acceptance notes
ModalFormSectionConfigstill declaresfieldsrequired and nogroup; the hosts pass the form view's sections untyped. The declared face is left unchanged on purpose, because the claim'sClause-②: norests on no published input or type moving.DrawerFormmounted directly does not resolve{ group }sections. No host mounts it that way with authored sections today, so it reaches no public door. No carrier..form. This PR changes how the dialog renders that view's sections. Disjoint by file, andMetadataProvider.tsxis untouched.Session:
https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2(an os-dev run dispatched by the PM loop, claim comment 5969222831).Generated by Claude Code