fix(console,plugin-form): render a form section that references a field group (#8641) - #8727
Merged
Merged
Conversation
…ld group (#8641) `@objectstack/spec` 17.3.0 lets a `form.sections[]` entry declare its members either way — enumerate `fields`, or point `group` at one of the object's declared `fieldGroups` (objectstack#13855, ADR-0085 §5). `apps/console`'s `FormPage` has its own section builder, on none of `@object-ui/plugin-form`'s code path, and it read `sec.fields ?? []` and `sec.label` — neither of which a `{ group }` section carries. Reproduced in the DOM before repairing, on both routes: the `<section>` was emitted with its border and padding and then stood empty — no heading, no inputs, no diagnostic. "Renders nothing" was one word off; a submitter saw a blank card where the group's fields belong. `buildSections` now resolves the reference through `resolveSectionGroupReferences`, newly published from `@object-ui/plugin-form` — the same function `ObjectForm` resolves through, so all three consumers share one assembler. No assembly rule is re-implemented on the console side: declared order, the empty-group drop, the ungrouped trailing bucket and the collapse / `visibleWhen` passthrough all come from `deriveFieldGroupLayout` via that package's single adapter. Reaching for the derivation directly would have meant re-spelling the `collapse` enum onto this renderer's boolean pair, which is that constraint's exact prohibition. `ObjectSchemaPayload` carries `fieldGroups` and the internal `/meta/object/:name` loader copies it: that rebuild is key by key, so an uncopied key is gone before the builder can see it. The public `/f/:slug` payload forwards the server's object schema whole and needed no such line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
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
|
os-justin
marked this pull request as ready for review
September 9, 2026 01:35
os-justin
enabled auto-merge
September 9, 2026 01:35
This was referenced Sep 9, 2026
… freezing a spec version (#8641) Two red checks on this PR, both in the README block this PR added. `Doc Snippet Type Check` — the block called `resolveSectionGroupReferences` with an `authoredSections` nothing declared (TS2304). It is now declared at the type the function actually takes: `ObjectFormSection[]`, imported from `@object-ui/types`, which is what `packages/plugin-form/dist/sectionGroups.d.ts` spells for that parameter. That type declares `group` and leaves `fields` optional, so it is also the shape the surrounding prose is about. Not `any`: a snippet that type-checks by erasing its own types teaches nothing and leaves the gate asserting nothing. `doc-version-claims` — the paragraph froze "`@objectstack/spec` 17.3.0" into prose no gate can re-measure. Deleted rather than inventoried: the sentence keeps its meaning from objectstack#13855 and ADR-0085 §5, which do not go stale, and the version floor now points at this package's own `package.json` entry (`^17.0.0`), which moves when the dependency moves. The literal was TRUE at this commit — the installed spec is 17.3.0 — and that is exactly the condition under which it would have gone silently false later. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
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
|
os-justin
deleted the
claude/issue-8641-console-formpage-section-group
branch
September 9, 2026 02:33
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 #8641
Step one was REPRODUCTION — the card said so about itself
The card recorded, honestly, that its finding came from reading
sec.fields ?? [], not from rendering anything. So it was rendered first, onorigin/mainata29ae2d66, with the fixture this PR ships:and in the DOM, identical on both routes (internal
/forms/:nameand public/f/:slug):The card's premise re-derived against the installed
@objectstack/spec17.3.0, not quoted:rounded-md border bg-card p-4— so the submitter sees an empty bordered card where the group's inputs belong, twice, with no diagnostic on any channel. Not an invisible omission; a visible blank.The choice: A, and why
A — export the resolver from
@object-ui/plugin-formand call it frombuildSections.The dispatch put the trade honestly, so here is the reasoning against both named costs.
Why B is not merely worse but disqualified. The standing constraint of #7051 / objectstack#13855 is that no assembly rule may be re-implemented on the objectui side — not declared order, not the empty-group drop, not the ungrouped trailing bucket, not collapse /
visibleWhenpassthrough, and the dispatch's own test is "if your arm needs any of them re-spelled, it is the wrong arm."deriveFieldGroupLayoutreturns{ key, label, description, collapse, visibleWhen, fields }wherecollapseis the enum'none' | 'collapsible' | 'collapsed'.RenderableSectionhas no such key — it has the boolean pair{ collapsible, collapsed }. So arm B cannot land its result on a section at all without re-spelling the collapse mapping and passingvisibleWhenthrough by hand, whichderiveFieldGroupSectionsalready spells eleven lines away. That is not a judgement call about elegance; it is the prohibition, literally. B also re-spells the match-by-key, thelabel ?? keyfallback and the unresolvable-reference diagnostic.The cost of A, paid explicitly. One new published name (plus one type). It is the same surface decision this barrel already made for the same file: #6059 published
omitServerResolvedDefaults/isRequiredInFormbecauseFormPageis "the SECOND form renderer in this repo" and had to hand-compose the rule. This is the third consumer of the same class, and the addition is deliberately as small as that one: the resolver and the options type its signature requires (#7324's rule — a consumer must be able to name what it passes; a type adds no runtime surface).hasSectionGroupReference,resetSectionGroupReports,GROUP_OWNED_SECTION_KEYSandSECTION_LAYOUT_KEYSstay module-private, and that withheld set is pinned, not left to the reviewer's eye.The subpath trap was checked, not assumed.
@object-ui/plugin-form'sexportsmap declares"."and nothing else. A subpath specifier would resolve in the console's dev server (a Vite alias matches by PREFIX) and then failBuild ConsolewithTS2307— objectui#8710, measured on PR #8668. The import is from the package root, and@object-ui/console:build(which runstsc && vite build && tsc -p tsconfig.plugin.json) is green on this branch.Why not C. Not mine to take — and the measurement does not point at it: the reference app is what teaches people to use the product, and it currently teaches that a spec-legal section renders as an empty box.
What changed
packages/plugin-form/src/index.tsxresolveSectionGroupReferences+ResolveSectionGroupsOptionspackages/plugin-form/README.mdsections[].groupoutside this package" sectionapps/console/.../FormPage.tsxbuildSectionsresolves the reference;ObjectSchemaPayloadcarriesfieldGroups;ObjectFieldDefcarriesgroup; the internal/meta/object/:nameloader copiesfieldGroupsThat last line is load-bearing:
loadInternalFormrebuilds the object schema key by key, so a key it does not copy is gone before the builder can see it. The public/f/:slugpayload forwards the server'sobjectSchemawhole and needed no such line — which is exactly why both routes are pinned separately rather than one standing in for the other.formTypeis forwarded, sogroupon aformType: 'wizard'section gets the same refusal here that the spec door gives, instead of this renderer honouring a shape the spec declines to accept.Evidence
The discriminating axis
Not "the form renders" and not "the section is non-empty" — both pass on the pre-fix builder. The pin is two sections referencing two different groups, each asserted against concrete field identifiers in order, and the one that carries the weight references
billing, the SECOND declared group. That is not decoration: PR #8644's author found their own rows staying green under a constant-resolution caricature because they referenced the first group, which is what such a caricature returns. Measured below: under the caricature thecontact_infoassertions stay green and only thebillingones turn red.Ablation — read sites only, from a committed tree
Every leg:
trap … EXIT INT TERMwith absolute paths; the mutation proven on disk in both directions (deleted-anchor count down, injected-anchor count up) plusgit hash-objectdiffering from the HEAD blob plus a line-total delta; restored by state (git checkout HEAD -- ABSOLUTE_PATH, never the bare form) and verified withgit diff HEAD --quiet. Classification is per-test from vitest's JSON reporter, with harness-death strings counted separately.sectionGroups.ts:byKey.get(group)->derived?.[0]1888346e→c807bafa, lines Δ0FormPage.tsx: resolver call removedc31f2ca2→0fca9673, lines Δ−12plugin-form/src/index.tsx: export line removed7e4e3040→e953a2f5, lines Δ0export * from './sectionGroups'7e4e3040→76ebce2c, lines Δ0Leg 3's fingerprint is unmistakable and is the whole reason the second group was chosen:
Leg 4 restores today's behaviour and reddens both group sections and both diagnostic pins:
Two assertions were never observed red, and are reported as such rather than counted.
PREMISE: @objectstack/spec ACCEPTS a bare { group }is a premise probe against the installed package — its red condition is a spec regression, which no ablation of this diff can produce.CONTROL: a form with no group reference is built exactly as beforeis the control that must stay green in every leg: without it, everytoEqualabove would be equally satisfied by a renderer that draws nothing.Suites and gates — commands and verdicts
Run from the repo root with paths (objectui#3378 / #3288), on
666717c69:vitest run apps/console/.../FormPage.sectionGroup.test.tsx packages/plugin-form/src/__tests__/sectionGroupResolverPublished-8641.test.tsvitest run --project @object-ui/consolevitest run packages/plugin-form/vitest run packages/plugin-view/ packages/plugin-designer/ examples/vitest run packages/app-shell/turbo run build --filter='!@object-ui/site'turbo run type-check --filter=@object-ui/console --filter=@object-ui/plugin-formturbo run lint --filter=@object-ui/console --filter=@object-ui/plugin-formnode scripts/check-readme-exports.mjs0 unbuilt); the first run reported36 unbuiltand was NOT MEASUREDnode scripts/check-changeset-presence.mjscheck-changeset-fixed/-no-major/-overwritenode scripts/check-control-bytes.mjsgrep -naPover the untracked new files: no hits)node scripts/check-spec-symbol-derivation.mjsnode scripts/check-governed-queue-guard.mjs --test …Affected packages taken from
TURBO_SCM_BASE=a29ae2d66 turbo ls --affected(9), not guessed from the diff.tsc --listFiles), rather than assumed:apps/consoleincludesFormPage.sectionGroup.test.tsx(94 test files in program),plugin-form'stsconfig.test.jsonincludes the surface pin. A typecheck that excluded them would have been NOT MEASURED.Changeset
Written per the gate's own verdict line, not per a label —
apps/consoleis not private and@object-ui/consolepublishes, soskip-changesetwould have been a phantom..changeset/8641-console-formpage-section-group.md:@object-ui/plugin-form: minor(new export),@object-ui/console: patch(the fix), mirroring #6059's precedent.Out of scope, filed
#8725 —
FormSectionSpec(app-shell) re-declaresfieldsas REQUIRED, so the spec-legal{ group }shape does not compile for a TypeScript author (this PR's fixture casts around it), andSchemaFormis a fourth form-section renderer whose threes.fieldsreads are unguarded and above the JSX. Filed with no assignee and no labels; its reachability was not measured and the issue says so. Not fixed here: widening that type is a behaviour ruling on a different renderer, not a mechanical change.Collisions
git ls-remote --heads origin 'gh-readonly-queue/*'at branch time: PRs 8689, 8692, 8697, 8698, 8699 queued. Each one's file list was fetched; none touchesFormPage.tsx,packages/plugin-form/**ormetadata-admin/**.🤖 Generated with Claude Code
https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Generated by Claude Code