Repository navigation
fix(core): refuse a single-record action's row count before its dialog, and translate the refusal (objectui#13384) - #13395
Conversation
…g, and translate the refusal (objectui#13384) A single-record script action placed in list_toolbar (HotCRM's "Add to Campaign") launched with 0 or 2+ rows ran its whole param dialog, then refused after Confirm, in English under zh-CN. ActionRunner now asks the handler it will dispatch to for an early refusal (`refuseBeforeInput`, optional on ActionRunnerHandler) before the confirmation and the param dialog. createServerActionHandler answers it with the same record-id resolution the dispatch runs, so a refused launch toasts and sends nothing before the user types anything; one row behaves as before. The two refusals now carry pack keys (actions.selectRowFirst, actions.selectExactlyOneRow) in all ten packs; resolveServerActionRecordId returns the key as `errorKey`, and the runner translates the early refusal through its translator by that key. Claude-Session: https://claude.ai/code/session_01WJozybDmRH8z2c5SD7vJkj Co-authored-by: Claude <noreply@anthropic.com>
…le server-action wrapper (objectui#13384) createConsoleServerActionHandler wrapped core's handler in a new function, so the runner never saw `refuseBeforeInput` and the console kept opening the param dialog before refusing. The wrapper now forwards it from core's handler (built with the wrapper's own resolveRecordId), so a single-record action launched from a list toolbar with 0 or 2+ rows is refused before any dialog, with no pre-opened tab and no request. The objectui#12331 list-toolbar suite now asserts that its 0- and 2-row launches open no dialog, beside the toast and the absent request; the consoleServerAction suite pins the forward, alone and through a real ActionRunner. Claude-Session: https://claude.ai/code/session_01WJozybDmRH8z2c5SD7vJkj Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Face: the ① Derived judgmentsThe diff widens three public types of
The rule is asked of the handler rather than held in the runner. That is the right altitude: a related-list grid publishes The console wrapper ② Semver levelThe changeset declares
③ Boundary flags
Implemented-by: VERDICT: PASS |
✅ 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
|
…the early refusal, not a dialog (objectui#13384) `?runAction=create_campaign` runs a record-scoped action as the list page mounts, with no row selected. Since objectui#13384 such a run is refused for its row count before its param dialog opens, so the control case's "the param dialog opens" no longer holds. Its claim (the same link arms on a view that does not wire the action, and is consumed) is now shown by the refusal toast, with no dialog; the not-armed case also asserts no such toast. The fixture keeps the same action, so the differential stays. Claude-Session: https://claude.ai/code/session_01WJozybDmRH8z2c5SD7vJkj Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Face: the same ① Derived judgments
The deep link supplies no record id in any form the resolver misses, so the new behaviour is right and the pin's premise changed. The case keeps its claim, that the link arms and is consumed on a view that does not wire it. It now shows the run by the refusal toast and the absence of a dialog. Its "not armed" twin now also asserts that no refusal toast appears. The judgments of the earlier record (the three widened types, the forward, the record-page exemption) are unchanged, because no source file moved. ② Semver levelUnchanged: ③ Boundary flags
Implemented-by: VERDICT: PASS |
✅ 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
|
席位核对ACCEPT — objectui#13384 (p3, S), head Seat: domain:ui#1 · Clause-②: yes(合同复核见本 PR 上的两条
|
|
Re-queued · At 19:06Z the merge queue dropped this PR. Its merge-group CI run 40005992766 went red on one job: It is not this PR's failure:
No fix exists to port into this PR, so the seat re-enables auto-merge once. That is this failure's one re-run. A second timeout on the same shard is treated as real. Generated by Claude Code |
Closes #13384
Clause-②: yes (widening: ActionRunnerHandler / ServerActionHandler gain optional refuseBeforeInput; ServerActionRecordIdResolution gains optional errorKey; nothing is refused)
What changed
A single-record action launched from a list toolbar with 0 or 2+ rows selected is now refused before any confirmation or param dialog opens. It gets the same toast as before and sends nothing. With exactly one row the dialog behaves as before. The refusal is also translated now: it read English under zh-CN.
@object-ui/coreActionRunner:ActionRunnerHandlergains an optionalrefuseBeforeInput(action, context).executeasks the handler it will dispatch to, after thecondition/disabledgates and beforeconfirmTextand the param collection. A refusal goes through the usual post-execution (error toast,onFailure), and nothing is dispatched. If the refusal names anerrorKey, the runner translates it throughsetTranslator's translator, with the English as default. This is the same channel the runner's ownRUNNER_TEXTuses, so core still takes no i18n dependency.@object-ui/coreserverActionHandler:actions.selectExactlyOneRow,actions.selectRowFirst).resolveServerActionRecordIdreturns the key aserrorKeybeside the Englisherror.createServerActionHandlerattachesrefuseBeforeInput, which runs the same resolver the dispatch runs, including an injectedresolveRecordId. So a record page, which resolves against its own record, is never refused.recordId.@object-ui/app-shellcreateConsoleServerActionHandler: forwardsrefuseBeforeInputfrom core's handler, which it builds with the wrapper's ownresolveRecordId. Without the forward the runner never saw the member and the console kept its dialog-first order.@object-ui/i18n: both keys are added to all ten packs. No German low-high quote span is added, so thede-quote-pairing-3876counts are unchanged.Why the runner asks the handler instead of holding the row rule itself: on a record page, a related-list grid publishes
selectedRecords: []into the page's runner context. A runner-side copy of the rule would refuse everyrecord_headeraction there. Only the handler knows its own resolver.Tests
Run on head
024092b03:pnpm exec vitest run --maxWorkers=2overpackages/core/src/actions/__tests__/,packages/i18n/, and the 21 app-shell test files that readconsoleServerAction,resolveServerActionRecordIdortoolbarScriptSelection: 146/146 files, 2113 passed, 13 skipped.ActionRunner.recordRowGuard-13384(core):recordIdis not judged early.serverActionHandler.test.ts: pins forerrorKeyandrefuseBeforeInput.consoleServerAction.test.tsx: pins the forward, both alone and through a realActionRunner. 0 and 2 rows open no dialog, pre-open no tab and send nothing. 1 row opens the dialog.ObjectView.toolbarScriptSelection-12331: its five 0- and 2-row cases now also assert that no dialog opened, beside the toast and the absent request. Every other claim in that suite is unchanged.recordSelectionRefusal-13384(i18n): pins the zh-CN text; every other pack defines both keys with its own text.NODE_OPTIONS=--max-old-space-size=8192 pnpm exec turbo run type-check --concurrency=2:--filter=@object-ui/core: 3/3 tasks--filter=@object-ui/app-shell: 30/30 tasks, includingtsc -p tsconfig.test.jsonpnpm check:i18n-keys,check:i18n-drift,check:i18n-dead-keys: exit 0. Dead-keys is report-only. It lists the two new keys as needs-review, the same class as theiractions.*siblings, which are also reached only through a key table.no-explicit-any; none is added.check:new-line-citations: 0 new.check-changeset-presence: pass.Ablation
Each run used
ablation-replacewith on-disk proof, and was restored togit diff HEADempty:errorKeydropped from the refusalconsoleServerAction.test.tsx, plus all five 0- and 2-row cases ofObjectView.toolbarScriptSelection-12331Acceptance notes
Out of scope, still open: the console
flowHandlerinuseConsoleActionRuntime.tsxcallsresolveServerActionRecordIddirectly and is not a forwarded handler. Atype: 'flow'action with params, launched from a list toolbar with 0 or 2+ rows, still opens its dialog first, and its refusal still reads English. It could read the newerrorKey. That file belongs to objectui#13379's claim, so it was not touched here.Out of scope, still open: the console wrapper's own
newTabUrlfast-path refusal ("This action runs on a single record — no record id available.") is still a hard-coded English string with no pack key.Adds no product-specific widget, dialog, hook or string (see AGENTS.md › Product-neutral renderer)
Session:
https://claude.ai/code/session_01WJozybDmRH8z2c5SD7vJkj🤖 Generated with Claude Code
https://claude.ai/code/session_01WJozybDmRH8z2c5SD7vJkj