Repository navigation
fix(core): a row action's confirmText and param-dialog copy read the row as record, like the record header (objectui#12967) - #12979
Conversation
…e header's
The runner evaluated `confirmText` in its own context, which binds `record`
on the record header and nothing on a list row, so the same
`${record.display_name}` read filled from the header and raw from a row.
The runner now binds a dispatch's `params._rowRecord` as `record` for that
dispatch's templates, and hands the param-collection handler an
`interpolate` that fills the dialog's `description` and `helpText` holes in
the same scope, after localization.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
The same action's `confirmText` reads filled from a record header and from a
real ObjectGrid row; the param dialog's `description` and `helpText` are
filled through the runner's `interpolate`; a missing field degrades the same
way on both doors; a toast's `${result.*}` is unchanged.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
…a dialog field Both param runtimes now put the runner's `interpolate` on the dialog state (objectui#12967), so the field list the parity pin enumerates gains it. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
✅ 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: Inputs read: card #12967 (body, triage ① Derived judgmentsPublic-surface changes — the diff implies exactly two, both as claimed. Right.
The binding point. Row wins over a page record. Param dialog ordering.
Missing-field degradation. The evaluator's template-part path returns Toasts. The success/error toast reads Bulk path. Scope note, not a defect. Tests. The core pin drives the SAME action through a header-shaped runner and a list-shaped runner with the exact ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #12967
Clause-②: yes (widening)
What changed
One rule for both doors: a row action's templates read the row as
record, the way the record header's do.@object-ui/coreActionRunner. A new privatetemplateEvaluatorFor(action)returns the evaluator a dispatch's authored copy is read through. When the dispatch carries a row underparams._rowRecord, it binds that row asrecordfor this dispatch only. The row wins over a page-levelrecord, and the runner's own context is not touched.confirmTextis read through it. The param-collection handler now receives a third argument,interpolate, bound to the same scope.@object-ui/app-shell. Both param-collection handlers (useConsoleActionRuntime,RecordDetailView) applyinterpolateto the localizeddescription. The console handler does this BEFORE it joins the admin-override notice, because approver names are data and must never be read as a template. Both putinterpolateon the dialog state.ActionParamDialogapplies it to each param'shelpTextafter an inline per-locale map is picked, so the hole is filled on the text the user reads.ObjectGridalready passes the row as_rowRecord.Why the runner is the narrowest shared point (A1)
RecordDetailView'sActionProvidercontext is{ record: pageRecord or {}, objectName, objectFields?, user, org }.{ ...def, actionParams?, params: { _rowRecord: row } }, fired through the list page's runner, whose context carries norecord.ActionRunner.execute, and it is the one placeconfirmTextis evaluated. Binding there fixes every row door at once.record:ObjectGridrow actions: yes. This is the card's door.RelatedRecordActionsBridge(a related list's row actions): yes, and the child row now wins over the parent page'srecord.DeclaredActionsBar: yes. It stashes the page record, so nothing changes.containers.tsxpage-header dispatch: yes. Same record.action-button.tsxupdate write: yes. Same record.ObjectGrid's per-record bulk dispatch: not affected. It stripsconfirmTextand the params before dispatch, andBulkActionDialogprints its own multi-row confirm. That dialog is out of scope (see Acceptance notes).interpolateinstead of pre-filling the strings. The runner cannot filldescription/helpTextitself before calling the handler. The handler localizes that copy AFTER it receives it, either from a translation-bundle entry or from an inline per-locale map. A pre-filled literal would lose to an unfilled translation. That is why the runner hands over a function and not a filled string.Missing-field degradation (A2)
On the header,
${record.no_such_field}readsundefinedand the hole prints empty:Pause ?. Before this change the row printed the raw source. It now prints the same empty hole. A hole whose root is unbound (no row, norecord) stays as authored, which is the evaluator's existing answer.Ruled scope
${result.*}is unchanged. Pin (d) covers this.Tests
Final head
9bcaded76f.packages/core/src/actions/__tests__/ActionRunner.rowRecordTemplates-12967.test.tspins:confirmTextis filled on the header and on the row;record;interpolatefillsdescription/helpTexton both doors;${result.*}is unchanged.packages/plugin-grid/src/__tests__/rowActionRecordHole-12967.test.tsxruns the same action through a realObjectGridrow menu and through a header-shapedActionProvider, for (a) and (c).packages/app-shell/src/views/ActionParamDialog.recordHoles-12967.test.tsxchecks that the real dialog fills a stringhelpTextand a per-locale maphelpText. CONTROL: withoutinterpolate,helpTextprints as authored.RecordDetailView.paramDialogTitle.test.tsxanduseConsoleActionRuntime.overrideNotice.test.tsx. They cover: the description is filled after localization, the override notice is never interpolated, andinterpolatereaches the state.RecordDetailView.paramRuntimeParity-6431.test.tsxlists the fields the dialog reads off its state. That list gainsinterpolate.templateEvaluatorFor'sreturn new ExpressionEvaluator({ ...this.context, record: row });was replaced withreturn this.evaluator;throughablation-replace.mjs(anchor x1 to x0). Result: 6 failed, 2 passed. (a) went red on both files, for exampleExpected "Pause Acme?" / Received "Pause ${record.display_name}?". (c) showed the pre-fix row degradation (raw source). The restore was proven by blob:edf7176c00c7after restore equals HEAD, andgit diff HEADis empty. Tests resolve@object-ui/coretosrcthrough the root vitest alias, so no dist rebuild was involved.Gates (local, on
9bcaded76f)type-checkexited 0 for core, app-shell and plugin-grid, after building the app-shell dependency closure. Every touched test is in its package'stsconfig.test.jsonfile list (checked with--listFilesOnly).ActionRunner,ActionParamDialogor row actions, or namingconfirmText,_rowRecordor the param handler:--no-inline-config, JSON format): 10 files, 0 errors. The config enables no type-aware linting, so this diff cannot change the verdict on any untouched file.check:phantom-deps,self-import,unreferenced-sources,doc-example-readers,handler-key-reads,side-effects-array,element-data-source-declaration,spec-symbols,action-forward-parity,designer-field-key-parity,icon-record-names,i18n-keys,i18n-drift,i18n-designer-parity,control-bytes,new-line-citations,check-hand-rolled-comment-mask.mjs,check-changeset-fixed.mjs,check-changeset-presence.mjs.check:spec-floors: exit 1. This is pre-existing on main, as the dispatch noted. No manifest is touched.Acceptance notes
BulkActionDialog's confirm. It printsdef.confirmTextraw for a multi-row selection. There is no singlerecordthere, so a${record.*}hole cannot be filled by this rule. Observation only.@object-ui/coreminor (the widened exportedParamCollectionHandler),@object-ui/app-shellpatch. The plugin-grid change is a test file only.The implementing session is
https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB(seatdomain:ui#2).Generated by Claude Code