feat(plugin-form): honour a section visibleWhen on the tabbed layout arm - #6791
Queued
os-sales wants to merge 1 commit into
Queued
feat(plugin-form): honour a section visibleWhen on the tabbed layout arm#6791os-sales wants to merge 1 commit into
os-sales wants to merge 1 commit into
Conversation
The tabbed arm of the one grouping contract ruled 2026-08-29 (objectui#6237, option A). An authored `FormSection.visibleWhen` was dropped on the `tabbed` route while split/drawer/modal and the flat layout honoured it. `TabbedForm` already synthesises the renderer's `fieldTabs` — the same machinery the modal tabbed arm runs on — so the predicate was only being dropped on the way there, at three points that now all carry it: ObjectForm's tabbed section map, `FormSectionConfig` (which declared no such key), and TabbedForm's `fieldTabs` synthesis. The three ruled semantics are therefore inherited, not re-implemented: hidden tabs' values still submit, their fields skip client-side validation, hiding the active tab re-selects deterministically, and engagement stays structural on the declared tabs so a predicate cannot collapse the strip mid-interaction. Two boundaries are deliberate. A single-section tabbed form never engages the tab arm, so it degrades to the untabbed layout's own predicate mechanism (a chrome-less section-divider claiming its members); forms without a predicate are unchanged. Wizard steps still take no predicate and now say so in the type — `WizardStepConfig` omits the key, since a step predicate is a different contract — with the runtime diagnostic narrowed to that one remaining arm. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
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
|
This was referenced Aug 29, 2026
os-sales
marked this pull request as ready for review
August 29, 2026 15:00
os-sales
added this pull request to the merge queue
Aug 29, 2026
Any commits made after this event will not be merged.
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.
Part of #6237 — the
formType: 'tabbed'arm of the one grouping contract ruled 2026-08-29 (option A). Deliberately not a closing keyword: two things named at the bottom stay open, and one of them wants a ruling.Base:
107babef6. Final commit:02641f20f(every reading below taken there).What was broken, re-derived on this base
ObjectFormrebuilds each section key by key, per layout route. Thesplit,drawerandmodalmaps each copyvisibleWhen; the flat arm carries it on thesection-dividerpseudo-field. Thetabbedmap copied nothing, so an authoredFormSection.visibleWhennever reached a renderer on that route at all.The useful measurement is that this was not a missing mechanism.
TabbedFormalready synthesises the renderer'sfieldTabs— the very machinery themodal+contentLayout: 'tabbed'arm runs on, whereFormFieldTab.visibleWhenhas been evaluated since PR #6619. The predicate was only being dropped on the way there, at three points, all three of which now carry it:ObjectForm's tabbed section map.FormSectionConfig— which declared no such key.TabbedForm'sfieldTabssynthesis.So this arm reaches the existing evaluator by the existing route. The renderer is unchanged; nothing in
packages/componentsorpackages/typeswas touched.The three ruled semantics — two settled by existing evidence, one decided here
The ruling names three semantics the design must answer. Stating plainly which were decided by whom:
1. Hidden tab and required fields — SETTLED, inherited. Ruled 2026-08-27 (visibility gates drawing only; hidden-group values still submit; hidden-group fields skip client-side validation, with the server as the loud floor) and implemented in the renderer for
fieldTabsby PR #6619. Because a hidden tab's panel is simply not drawn, its fields unmount — the same mechanism a field's own false predicate uses — so react-hook-form keeps the values and skips the unmounted controls. This arm inherits that; it is not re-implemented beside it. Pinned intabbedFormSectionPredicate-6237.test.tsx, including the control that a required field on a merely inactive (not predicate-hidden) tab still blocks — without which "does not block" would be satisfied by a form that stopped validating altogether.2. The active tab hides itself — SETTLED, inherited.
activeFieldTabderives over the visible tabs: the user's pick if still visible, else the declared default, else the first visible tab. The pick is kept rather than cleared, so the chosen tab becomes active again when its predicate re-admits it. Never an empty panel. Two pins, including one where the declared default is the hidden tab.3. Collapse below two tabs — SETTLED for the case everyone means; DECIDED HERE for the other. This name covers two situations:
(3b) is the only thing this PR decided. Left alone it would have made things worse, and that is why it is in scope rather than deferred:
ObjectFormstops reporting the tabbed gap now that the arm supports the key, so the single-section case would have gone from loudly-inert to silently inert — this card's own defect class, reintroduced by its own fix.The defined degradation is the untabbed layout's own mechanism (#6236): a
section-dividerrow claiming its members by name, so the verdict gates the whole group through the identical unmount path and the ruled semantics stay identical across both shapes. It is chrome-less on purpose — no label, no description, andSectionDividerrenders nothing without them — because a single-section tabbed form draws no tab strip today, so its heading is already absent; synthesising a visible one would change the layout of every such form under the banner of a predicate fix. It is emitted only for a section that actually authored a predicate, so existing forms are unchanged. Three pins, including the no-predicate row that holds that last property.The shared-type trap, and why the fix is an omission
WizardFormborrowedFormSectionConfigas its step type. Declaring the predicate on the shared type would have made it writable on a wizard step while the wizard renderer ignores it — exactly the declared-but-unenforced shape this card family exists to close.WizardStepConfigisFormSectionConfigwithvisibleWhenomitted, so the key is writable precisely where it is honoured, and a wizard step literal carrying it is a compile error. That is pinned as a type assertion (@ts-expect-errorundertsconfig.test.json), which fails the build if the key ever becomes writable there — prose cannot hold that boundary.This changes nothing that used to work:
FormSectionConfigdid not declarevisibleWhenbefore this card either, so the key was already a type error on a wizard step. Only the tabbed arm widened. The widened surface is the plugin-form barrel's exported section type, notpackages/types.The runtime diagnostic from PR #6733 is narrowed to
wizard, the one arm still inert. A diagnostic that keeps warning about a feature that started working is a false alarm, and false alarms are how a real one stops being read. Its remedy sentence now namestabbedas a working arm, with a pin on the sentence itself.Verification, at
02641f20fpnpm exec vitest run packages/plugin-form/ packages/components/src/renderers/form/—Test Files 134 passed (134),Tests 1174 passed (1174). Run at the final commit, from the repo root per AGENTS.md.@object-ui/plugin-form type-checkexit 0 (tsc --noEmit && tsc -p tsconfig.test.json) after building the dependency closure. Positive control that it really reads my files: an earlier run went red naming the two stale'tabbed'arguments in the diagnostic pin.git rev-parse --show-toplevel). Neutralised only thefieldTabspredicate copy, leaving the ObjectForm map and the degradation gate intact. Mutation proven on disk by anchored count (1 to 0) plus blob change3628c3cfto393a4be6, not by an editor exit code. Predicted 6 red and observed exactly those 6, row for row: the AuthoredFormSection.visibleWhenis dropped by all four plugin-form layouts — declared, bridged, then never evaluated on the object-view chain #6111 matrix's tabbed DENIED row, both semantic-1 rows, both semantic-2 rows, and the semantic-3a row. The controls held: semantic 1's visible-tab row, semantic 3a's ALLOWED row, all three semantic-3b rows (they run on the degradation mechanism, which the mutation deliberately left standing — that is what proves the two paths are genuinely separate rather than one edit's collateral), both type rows, and the whole diagnostic pin. Restore proven both ways: blob hash equal to the HEAD blob AND an emptygit diff HEAD. A first attempt aborted on its own guard, which is recorded because the guard was right to fire:g.visibleWhenis copied at two sites, so a loose count read a correct, precise mutation as a failure.dist/index.d.ts(not source, not cache): the wizard step line drew exactlyTS2353 ... 'visibleWhen' does not exist in type 'WizardStepConfig'while the tabbed section line on the same compiled declaration compiled clean. A bare-error control in the same query proved the probe file is genuinely compiled.check:control-bytes"OK (scanned 5636 tracked text file(s))";check:self-import"No package names itself inside its own src/";check:phantom-deps"Every in-scope import is declared by the package that publishes it";check:vi-mock-specifiersOK;check:spec-symbolsOK;check:i18n-keys"Every in-scope call-site key resolves against the en pack";check:i18n-drift"No en value changed in this range";check:changeset-presence"7 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)";check:changeset-no-major"No changeset declares amajorbump". No author-facing string was added, so no i18n key is owed — the diagnostic is a developer console message, English-only per Commandment #-1.check:readme-exports: measured for this package after building it — plugin-form's rows are clean, andWizardStepConfigresolves out of the builtdist/index.d.ts. The gate's overall exit 1 is entirely the "type entry not on disk, run build first" class across other unbuilt packages: tree state, not a diff verdict, and this diff touches zero README lines.eslint --no-inline-configon the 8 changed files, 0 errors, and 0 net new warnings — checked against the base versions of the same files, not just counted. That comparison earned its keep: it caught my tabbed-map line adding a 57thno-explicit-anytoObjectForm.tsx.ObjectFormSection.visibleWhenis in fact declared inpackages/types, so theas anythe three sibling maps use is unnecessary; dropping it restored the file to its baseline 60 and means a rename of that key is now caught by the compiler here instead of silently copyingundefined— the exact silent-drop failure this line exists to fix. Narrowing is sound: the config does no type-aware linting, so this diff cannot move an untouched file's verdict; the full sweep is CI's run.What stays open, precisely
isLastStep, final-gate and re-selection semantics that none of this machinery supplies. Measured business pull remains zero. Unruled, so not built; the type omission and the runtime diagnostic keep it honest meanwhile.../objectstack: it does not, and structurally cannot —packages/objectql's rule validator has zero references toFormSectionorsections; it validates against the object's field definitions, and never sees the form view. Server-sidevisibleWhenexists only for choice options. So the ruled "server is the loud floor" is internally consistent, and the residual authoring hazard is a genuinely object-required field placed inside a predicate-hidden section, which will be rejected by the server. Teaching the server to read a form predicate would be a published validation contract change in@objectstack/objectql— outside this fence and outside any ruling, so it is reported rather than attempted.Generated by Claude Code
Generated by Claude Code