Repository navigation
fix(plugin-form): a record dialog's collapsed section opens in place, not behind the console's loading screen (objectui#12559) - #12571
Merged
objectstack-fleet[bot] merged 6 commits intoOct 15, 2026
Conversation
…s own slot (objectui#12559) A collapsed section's first expand drew a not-yet-loaded lazy field widget with no Suspense boundary in the form, so the suspension unwound to the host's nearest boundary; the console's record dialog mounts under the shell's root boundary, so the whole console (dialog included) was swapped for the root fallback. Each registered widget now renders behind a per-widget boundary cached per component, with a control-height placeholder. Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB Co-authored-by: Claude <noreply@anthropic.com>
… lint-clean the 12559 pin host (objectui#12559) Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB Co-authored-by: Claude <noreply@anthropic.com>
…alog-section-toggle
…(objectui#12559) Move the boundary from every registered field widget (form renderer) to the modal form's dialog bodies, the shape FlowRunner gives its screen body. The per-widget boundary changed the readiness contract of every form in the product (labels drawn before their controls; a pinned test went red on it), while the defect is specific to an overlay a host mounts outside its page boundary. form.tsx is back to main. Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB Co-authored-by: Claude <noreply@anthropic.com>
…opped waiting (objectui#12559) Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB Co-authored-by: Claude <noreply@anthropic.com>
…alog-section-toggle
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
|
Contributor
Author
Review: ACCEPT — objectui PR #12571 ·
|
objectstack-fleet
Bot
deleted the
claude/issue-12559-dialog-section-toggle
branch
October 15, 2026 21:04
sunhaolin
pushed a commit
that referenced
this pull request
Oct 16, 2026
Resolves ModalForm.tsx against objectui#12548 (PR #12556, the per-section 12-track grid) and objectui#12559 (PR #12571, the dialog-owned Suspense boundary). The sectioned dialog keeps this branch's grid width (sectionedDialogColumns) and lays each section out through sectionedGridLayout, so both landed behaviours stand. Claude-Session: https://claude.ai/code/session_01JZnppgHRp4sREcouhkQYDB 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 #12559
Clause-②: no
What was wrong
The field widgets
@object-ui/fieldsregisters areReact.lazy: a type's chunk is fetched the first time a form draws it, and that first render suspends. A section that starts collapsed keeps its fields out of the DOM, so the click that opens it is often the first render of a type (currency, url, address). Nothing insideModalFormcaught that suspension, so it landed on the host's nearestSuspense. The console mounts the record dialog beside its routes (AppContent, after the routeErrorBoundary), so the nearest boundary isConsoleShell's root one around the whole app: the whole console, dialog included, was swapped for the root fallback while the chunk loaded. The full-page create route sits inside the route's own boundary and hid only its page.Producer, with evidence
ModalForm's ownuseState(collapsedSections), below the dialog; the console's key for the dialog is the object name plus the record id ornew, which a toggle cannot change. The section header is adivwithrole="button"drawn by the form renderer'sSectionDivider, not a submit button (PM assumption A2 disproved). A toggled value wins over the declared one inresolveSectionCollapse(host.live ?? declared), and the new pin re-renders the host with a fresh schema object to hold that (A3).ModalForm, a rootSuspenseabove it, no route boundary between, served by the console's Vite at 1440x900): right after the header click the host fallback mounted and the dialog's computeddisplaywasnone; 200 ms later the fallback had unmounted and the dialog came back with the section expanded and the typed value kept. In that harness React keptModalForm's state.The fix
ModalFormowns its body's wait, the same shapeFlowRunnergives its screen body (framework#3528 precedent, pinned byFlowRunner.suspense.test.tsx). Both dialog bodies (the stacked/tabbed form and the master-detail one) render inside aSuspensewhose fallback isModalFormBodyPending. The header and footer stay on screen, and the form underneath is hidden, not unmounted. It is distinct frommodal-form-skeleton, which stays the record read's loading state. The first open of a dialog whose types are not loaded yet is covered the same way.A first attempt put a boundary around every registered field widget in the form renderer (
packages/componentsform.tsx). I reverted it inside this branch: it changed the readiness contract of every form in the product (labels drawn before their controls), andtabbedFormSectionPredicate-6237went red on it. The defect belongs to an overlay that a host mounts outside its page boundary.form.tsxis byte-equal to main.Pins (
packages/plugin-form/src/__tests__/modalSectionToggleSuspense-12559.test.tsx)The host is the console's shape: a root
Suspensewhose fallback counts its mounts, a host that mints a mount id once, andModalFormunder a stable key, handed a fresh schema object every render. Theurlwidget is swapped for one whose chunk lands only on release.getObjectSchemacall, no read skeleton, the section expanded with its fields, and the value typed in the other section kept.ObjectForm, simple) still expands a collapsed section.Ablation (via
ablation-replace.mjs, mutation proven on disk and restored,git diff HEADempty): with the bodySuspenseremoved, pin 1 goes RED at the host-fallback count (expected 2 to be 1), and pins 2-4 stay green. A first ablation attempt was refused by the tool as a no-op (the replacement text already occurred as a substring) and is not counted. Under jsdom React keeps state across the hide, so the remount half of pin 1 stays green even under ablation. The pin that fails is "the host's boundary never takes the dialog off screen".sectionedFormValues.test.tsxused the footer as its readiness signal. The footer now draws while the body waits, so the test also waits for the body placeholder to go before it countsformelements.Gates (final head
cc9467aed, unless noted)pnpm --filter '@object-ui/plugin-form^...' build: exit 0.pnpm --filter @object-ui/plugin-form type-check(tsc +tsconfig.test.json;--listFilesincludes the new test): exit 0 at276d22c17. The only later change in this package is a test file, which the targeted run below covers.ModalForm,section-dividerorcollaps, plus the pin:29 passed, 393 testsatcc9467aed.vitest run packages/plugin-form/at276d22c17: 184 of 185 files passed. The one red wassectionedFormValues(above), fixed and re-run green atcc9467aed. The full directory was not re-run on the final head.**/*.{ts,tsx}; 2 files per--format json; no type-aware linting configured, so the diff cannot move any untouched file's verdict): 0 errors.ModalForm.tsxhas 20 warnings, the same as main.check-hand-rolled-comment-mask,check-changeset-fixed,check-changeset-presence,check-changeset-no-major: all exit 0.packages/app-shell/(an app-shell full run held the shared lock for over 30 minutes) and the browser reading on HotCRM.Acceptance notes
DrawerFormand the record detail drawer were not audited. The per-widget boundary in the form renderer would close the whole class, at the cost of the readiness change above. That is a design call for the maintainer, not this card. Carrier: none.origin/main. This diff touches only the dialog body wrapper and the end ofModalForm.tsx, not the section layout code those PRs edit.Session:
https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB(seatdomain:ui#2, PM claim 6200039614).Generated by Claude Code