Skip to content

fix(form): a split form keeps BOTH panels' values (#2153) - #3012

Merged
os-zhuang merged 1 commit into
mainfrom
claude/jolly-bhabha-189806
Jul 30, 2026
Merged

fix(form): a split form keeps BOTH panels' values (#2153)#3012
os-zhuang merged 1 commit into
mainfrom
claude/jolly-bhabha-189806

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

The defect

SplitForm had the data-loss defect that #2987 fixed in ModalForm / TabbedForm for #2959 / #2153. renderSections rendered one SchemaRenderer — one react-hook-form instance and one <form> element — per section, and its two groups of sections live in separate ResizablePanel children. Each panel therefore owned isolated form state:

  • submitting from one panel's action bar sent only that section's fields and silently dropped everything typed on the other side of the divider;
  • a cross-panel field rule was dead: a visibleWhen in the right panel referencing a left-panel field never saw that field in its rule record, so the predicate faulted and failed open — the field the author meant to hide was always shown.

The regression test proves the first one exactly. Against main, filling all four fields across both panels and submitting produced:

AssertionError: expected { subject: 'Printer on fire' } to match object { subject: …, …(3) }
-   "description": "Smoke coming out of tray 2",
-   "priority": "high",
-   "status": "open",
    "subject": "Printer on fire",

The fix

A single <form> cannot straddle two panels from inside them, so the panel group became a layout the form renderer owns — option (b) from the issue discussion. New FormSchema.fieldPanes (+ fieldPanesOrientation, fieldPanesResizable) mirrors fieldTabs: the <form> wraps the whole ResizablePanelGroup and each pane holds only fields, so one react-hook-form instance spans the divider.

Sections inside a pane render behind the inline section-divider header, each at its own declared column density within the form's shared grid (applyAutoColSpan(fields, formColumns, section.columns)) — the same arrangement the stacked/tabbed hosts use.

Two behaviours fall out of moving the panels into the renderer:

  • splitResizable: false now actually pins the divider. It previously only hid the grip; nothing passed the panel library's disabled, so the separator stayed draggable.
  • each pane is its own @container, so a multi-column section collapses to fewer columns as its panel is dragged narrower instead of overflowing.

A single-section split form is not a split, so the renderer falls back to a plain field list rather than a one-panel resizable group (same > 1 guard fieldTabs uses).

Tests

  • packages/plugin-form/src/sectionedFormValues.test.tsx — 3 new SplitForm cases (both panels' values in the payload; section headers staying in the pane that owns them; a right-pane condition reading a left-pane field). Verified failing against main first, all three for the right reason.
  • packages/components/src/renderers/form/__tests__/form-field-panes.test.tsx — 9 renderer-level cases pinning the fieldPanes contract: one <form>, both panes' values, cross-pane rules, unclaimed fields still rendered, single-pane fallback, fieldTabs precedence, the divider's pinned/draggable states, and that the pane schema keys never leak onto the <form> DOM element (the follow-up bug fix(form): a tabbed/sectioned modal keeps every tab's values (#2959, #2153) #2987 hit).

51 test files / 508 tests green across plugin-form, the form renderer and types; turbo type-check green for all three packages.

Browser verification

Ran the split form in a real browser against a zero-backend harness (throwaway console Vite entry, deleted before commit):

  • one <form>, containing both panes;
  • typing urgent into the left pane's Subject revealed the right pane's conditional Escalation contact field — the cross-panel rule working live;
  • the submit payload carried every field from both sides: {subject, reporter, status, priority, description, escalation};
  • no React console warnings (no DOM prop leak), inline section headers, per-pane column grids, one action bar below the split.

One correction worth recording: I initially believed numeric pane sizes were being read as pixels by react-resizable-panels v4 (its own docs say so) and wrote that up as a third fix. A same-frame A/B in the browser showed numeric 40/60 → 40/60% and string "40"/"60" → 40/60% — identical. There was no pixel bug; that claim is out of the changeset. The renderer still passes percentage strings so the unit is explicit, and the comment says only that.

🤖 Generated with Claude Code

SplitForm rendered one SchemaRenderer — one react-hook-form instance and one
<form> element — PER section, and its two groups of sections live in separate
ResizablePanel children. So each panel owned isolated form state: submitting
from one panel's action bar sent only that section's fields and silently
dropped everything typed on the other side of the divider. Filling both panels
and clicking Create persisted { subject } alone.

The same isolation killed cross-panel field rules: a visibleWhen in the right
panel referencing a left-panel field never saw that field in its rule record,
so the predicate faulted and failed OPEN — the field the author meant to hide
was always shown.

Both panels are now ONE form, the same way #2959 fixed the tabbed hosts. A
single <form> cannot straddle two panels from INSIDE them, so the panel group
became a layout the form renderer owns: a new FormSchema.fieldPanes
(+ fieldPanesOrientation, fieldPanesResizable) mirroring fieldTabs, where the
<form> wraps the whole ResizablePanelGroup and each pane holds only fields.
Sections inside a pane render behind the inline section-divider header, each at
its own declared column density within the form's shared grid.

Also fixed by moving the panels into the renderer: splitResizable: false now
actually pins the divider (it only hid the grip before, leaving the separator
draggable — nothing passed the panel library's `disabled`).

Each pane is its own @container, so a multi-column section collapses to fewer
columns as its panel is dragged narrower instead of overflowing.

Verified in a real browser (zero-backend console harness): one <form> holding
both panes, typing 'urgent' in the left pane reveals the right pane's
conditional field, and the submit payload carries every field from both sides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Jul 30, 2026 10:03am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 27.9 KB 350 KB
Entry file index-C2Qs1Luy.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.20KB 2.97KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.25KB 1.01KB
auth (org-roles.js) 6.72KB 2.85KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 457.65KB 99.92KB
core (index.js) 2.16KB 0.78KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 134.67KB 34.24KB
fields (index.js) 222.07KB 54.35KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 25.17KB 5.80KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.45KB 10.67KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.05KB 1.53KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 6.84KB 2.42KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.90KB 12.35KB
plugin-charts (index.js) 60.52KB 17.11KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 110.24KB 28.47KB
plugin-designer (index.js) 210.56KB 42.56KB
plugin-detail (index.js) 216.52KB 53.02KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 104.79KB 25.28KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 180.86KB 47.45KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 102.39KB 24.18KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 40.32KB 10.53KB
plugin-timeline (index.js) 25.75KB 7.32KB
plugin-tree (index.js) 8.36KB 2.81KB
plugin-view (index.js) 85.88KB 20.99KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 3.47KB 1.54KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 0.77KB 0.41KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 2.00KB 0.96KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.04KB 1.93KB
types (system-fields.js) 2.39KB 1.17KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 1.08KB 0.64KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-zhuang
os-zhuang merged commit aa35561 into main Jul 30, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/jolly-bhabha-189806 branch July 30, 2026 10:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant