Repository navigation
fix(components): action:bar / action:group / action:menu members evaluate properties.params like a top-level action:button (objectui#10290) - #10786
Conversation
…like a top-level action:button (objectui#10290)
A member of action:bar, action:group or action:menu is drawn by its
container and never passes through SchemaRenderer, so the evaluation that
resolves `${record.id}` inside properties.params never ran for it. The bar's
inline members reached the handler with the raw template, and overflow,
group and menu items did not forward properties.params at all.
Each container now evaluates its members' properties with the
SchemaRenderer memo's own per-key rule and scope, exported from
@object-ui/react as useConfigBagEvaluator(). action:bar evaluates an inline
member where it composes it; action:group and action:menu evaluate an item
when they run it, so an overflow member is evaluated once. A node-level
params object is still not read as values.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…559 on UIActionSchema) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…r exports its plain half (objectui#10290) A hook declared in SchemaRenderer.tsx puts that file under the React Compiler lint, which then reports three pre-existing render-time performance.now() / Date.now() debug probes as react-hooks/purity errors (measured: 0 errors without the hook, 3 with it). The hook moves to hooks/useConfigBagEvaluator.ts; SchemaRenderer.tsx exports the plain evaluateConfigBagInScope() it calls, which applies the memo's own per-key rule and scope. The scope builder's predicate scope is typed unknown instead of any. The pin test's schema cast names BaseSchema. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…luator (objectui#10290) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
|
changeset-claim-re-read
|
✅ 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
|
…ening) Contract review on e8b96c0 (FAIL, text/semver only) found that two new `@object-ui/react` exports (`useConfigBagEvaluator`, `evaluateConfigBagInScope`) widen the published surface, so the bump must be `minor`, not `patch`. The changeset's paragraphs 2-4 are replaced with the review's corrected text (fixes the self-contradictory api-window sentence, the "returns the memo's own evaluation" overstatement, and the false "SchemaRenderer applies the same rule through it" claim), and a new paragraph declares the minor as Clause-② widening. `static-params.ts` gets two comment-only fixes: the group/menu parenthetical now says the `properties.params` channel is read off the authored object (no declared `properties` key on `UIActionSchema`), not off a declared key; and Zone 1's docblock now says a container member is evaluated by its container with the same rule and scope, since it never passes through `SchemaRenderer`. No code line changed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
Contract reviewServed-tier: ① Derived judgmentsEverything below comes from reading the code. No probe was run: nothing needed execution to settle.
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL What stops it landing:
The code (one evaluator, scope parity, no double evaluation, ruling A warnings, React rules) passes. The |
✅ 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
|
Contract reviewServed-tier: A narrow round-2 review, run in-seat by the dispatching seat, of the delta ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
Fixes #10290
Clause-②: yes (widening)
What changed
action:barevaluates an inline member'spropertieswhere it composes the member foraction:button/action:icon(withEvaluatedPropertiesinstatic-params.ts). An overflow member goes to the bar'saction:menuas authored, and that renderer evaluates it when it runs it, so no value is evaluated twice.action:groupandaction:menuread an item'sproperties.params, evaluated with the same evaluator (readMemberStaticParamValues), and forward it asActionDef.params. An item that carries anActionParam[]list forwards both channels, asaction:buttondoes. A node-level objectparamskeeps its objectui#10462 handling: dropped with one development warning; thetype: 'api'window is unchanged when the item carries noproperties.params; when it carries both,properties.paramsis forwarded and the node-level object is dropped without a warning.@object-ui/react: the memo's per-key rule, its scope builder and itspropertiesloop move from the memo closure to module scope inSchemaRenderer.tsx(evaluateConfigValueWith,configEvaluationScope,evaluateConfigBagWith). The memo calls them; its behaviour is unchanged. New exports:useConfigBagEvaluator()(hooks/useConfigBagEvaluator.ts) and its plain halfevaluateConfigBagInScope()(SchemaRenderer.tsx). File-surface widening, declared: the route needs these exports..changeset/10290-container-member-params-evaluated.md:@object-ui/componentspatch,@object-ui/reactminor (Clause-② widening: two new public exports; the fixed group releases at that minor).H1: premise, measured on base
17b323e5caInstrument: a one-off probe (not committed) that printed each handler's
params, then the committed pin file run at the base in a detached control worktree. The page is a record page bound throughRecordContextProviderto{ id: 'rec_1' }. The member is authoredproperties.params: { objectName: 'account', recordId: '${record.id}' }, and the handler isnavigate_edit.paramsthe handler receivedaction:buttonthroughSchemaRenderer{ objectName: 'account', recordId: 'rec_1' }action:barinline member{ objectName: 'account', recordId: '${record.id}' }action:barmember,component: 'action:group'action:barmember,component: 'action:menu'(overflow)undefinedaction:barmember spilled pastmaxVisibleundefinedaction:group, inline displayundefinedaction:group, dropdown displayundefinedaction:menuundefinedThe
component: 'action:icon'bar row was added to the pin after the probe. It is red on base in the pin run below.So PR objectui#10460 evaluates on none of the three containers, and the premise holds. The overflow, group and menu paths did not read
properties.paramsat all.H2: the evaluator and the route
SchemaRenderer'sevaluatedSchemamemo,evaluateConfigValue(key, value)does the work. Aparamsbag goes throughmapParamsLeaves(utils/paramsBag.ts) with a visitor that callsevaluator.evaluateon each string leaf. Any other value goes throughExpressionEvaluator.evaluateper value, with a CEL envelope preserved and a data object passed through.evaluatorisnew ExpressionEvaluator(scope)from@object-ui/core. Its scope is the ambient predicate scope, pluscurrent_user(an alias ofuser), plusrecord(the RecordContext row, only when one is bound), pluspage(page variables). None of this was exported before this PR.action:groupdropdown,action:menu, and the bar overflow) areDropdownMenuItemleaves, not registry nodes, soSchemaRenderercannot mount them. For inline members, (b) would also run thepropertieshoist on each member, a second run of the node visibility and enablement gate, and thedata-obj-*and style plumbing. That is a wider behaviour change than the card asks for.datarow prop (what the members'visible/disabledpredicates bind asrecord), the RecordContext row, page variables and the ambient predicate scope. The last three are the contextsSchemaRendererreads, and the containers render under the same providers.useConfigBagEvaluatorreads exactly the memo's three inputs. So a member's templates bind the RecordContext row, as a top-levelaction:button's do. Thedataprop is not bound into the template scope, andSchemaRendererdoes not bind it either.hooks/and not inSchemaRenderer.tsx. Measured: a hook declared inSchemaRenderer.tsxputs that file under the React Compiler lint. The lint then reports three pre-existing render-timeperformance.now()/Date.now()debug probes asreact-hooks/purityerrors (0 without the hook, 3 with it).H3:
visible/disabledon these pathsMeasured on base with a one-off probe on the same 7 paths plus the control. The predicates were
record.id == 'rec_1'(true on the page),record.id == 'rec_2'(false),disabled: record.id == 'rec_1', and the template form${record.id == 'rec_1'}.datarow passed (asDetailViewpassesdata={data}): all 8 rows evaluate correctly. True renders, false is absent, disabled-true is disabled, and the template form renders. This is identical to the control. These gates are evaluated already, and nothing was widened.datarow (RecordContext only):recordis unbound on the predicate tier, which bindsrecordfrom thedataprop throughusePredicateRecordContext. The predicates fault: fail-closed (absent) on theaction:button/action:menuleaves, fail-soft (shown) on theaction:groupleaves. The control top-levelaction:buttondoes the same. This is not container-specific. It is recorded under Acceptance notes, with no card.Pins and ablation
New file
packages/components/src/renderers/action/__tests__/action-container-member-params-10290.test.tsx, 17 tests:action:buttonthroughSchemaRendererresolves${record.id}. Green on base and head.properties.paramswith${record.id}resolved". The paths are: bar inline, baraction:icon, bar group-placed, bar menu-placed, bar spilled pastmaxVisible, group inline, group dropdown, and menu.paramsOBJECT is still not a values channel, and says so once" (ruling A).Red on base, green on head:
17b323e5ca(control worktree, same test file):Tests 8 failed | 9 passed (17). The 8 failures are exactly the 8 "resolved" rows. The control and the 8 node-level rows are green.e8b96c0161:Tests 17 passed (17).Ablation: one leg, through objectstack
scripts/ablation-replace.mjsin WRAP mode, on the committed head. Inpackages/react/src/hooks/useConfigBagEvaluator.ts, the anchorevaluateConfigBagInScope(bag, predicateScope, boundRecord, pageVariables);becamebag; /* ABLATION-10290 */, so the hook hands the bag back unevaluated.8b01c529→f4c00a8f, andTests 8 failed | 9 passed (17).8b01c529, andgit diff HEADis empty.@object-ui/reactthrough the rootvitest.config.mtsalias topackages/react/src, so the mutated source is what ran.Gates
At head
e8b96c0161unless noted.packages/components/src/renderers/action/,packages/react/, and the scripts tests for check-action-forward-parity (plus its closing-keyword test), check-doc-expression-carriage and check-component-surface-parity. Result:Test Files 130 passed (130),Tests 1883 passed (1883), lockVERDICT command-exit 0.type-check(tsc --noEmit && tsc -p tsconfig.test.json) for@object-ui/reactand@object-ui/components: Done / Done ata60555f057, afterpnpm --filter @object-ui/react build. The only later commit is changeset text.--listFilesOnlycount 1).dist/hooks/useConfigBagEvaluator.d.tsis the only declaration of the hook indist, so the green components type-check read the rebuilt.d.ts..ts/.tsxfiles, with inline config on (aseslint .runs per package): 0 errors, 48 warnings.eslint.config.jshasfiles: ['**/*.{ts,tsx}'], and there is no package-level config inpackages/reactorpackages/components.--format json.parserOptions.projectorprojectService), so this diff cannot move a verdict on an untouched file.SchemaRenderer.tsxhas 2 fewerno-explicit-anyand 1 morereact-refresh/only-export-components(for the new plain export).check:action-forward-parity,check:new-line-citations(0 new),check:control-bytes,check:handler-key-reads,check:self-import.check:unreferenced-sources,check:phantom-deps,check:unused-deps,check:side-effects-array,check:component-surface-parity(report-only).check:changeset-claims(report-only). The four pending changesets it names forSchemaRenderer.tsxwere read, and none is falsified.check:pending-changeset-literals,check:test-path-roots,check:vi-mock-specifiers,check-changeset-presence.mjs,check-changeset-no-major.mjs,check-doc-expression-carriage.mjs.check:eager-closure. Reason: exit 2, PREREQUISITE NOT MET. There is noapps/console/dist/eager-closure.json, which needs a console build. Declared to CI.pnpm lintand the fullpnpm test.Serial
bb7d5e71, was fetched into a private ref.git merge-tree --write-treeof HEAD against it exits 0 with no conflicts. It touchesaction-bar.tsx, and the merge is clean.origin/mainatbf14b646(5 commits past the base): merge-tree exits 0. None of those commits touch this PR's files, so there is no merge commit.Acceptance notes
content/docs/guide/record-edit-modes.mdteachesproperties.paramson a top-levelaction:buttononly. The same authoring now works on a container member. The guide was not edited, because it is outside this order's file surface. If it is extended, it needs one caveat: the development-build unevaluated-expression diagnostic runs in theSchemaRenderermemo, so an unresolvable template on a container member reaches the handler raw without that report. Carrier: none named.element:button's inlineactionandpage:header's actions still read noproperties.params. They are objectui#10462 surfaces, not containers.action:barmember oftype: 'api'with an objectparams. The overflowaction:menureads it (the objectstack#5777 window, entry semantics), and the inline path ignores it (action:buttonsemantics). This dates from objectui#10460 and objectui#10462, and is not changed here.recordfrom the host'sdataprop, and the template tier binds it from RecordContext. On a host that provides RecordContext but nodata, arecord.*predicate faults while arecord.*template resolves. This predates this PR and is identical on a top-levelaction:button. It was not measured on a real host page.@object-ui/reactexports are declaredminor(Clause-② widening).Session:
https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhNGenerated by Claude Code