Skip to content

Commit 462b4f9

Browse files
feat(console): an action's confirm and param dialogs name their dismiss button (cancelText drawn in place of Cancel) (#28983)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbCvT3geLANnf5WgDVV6kTX7)_ Fixes #28879 Refs #28519 Clause-②: yes (widening) Metadata-protocol: none File surface: the card's Wanted 1-4 list (claim 6568809663), plus two mechanical consequences the seat added in amendment 6568954960 (`ui/packages/types/src/ui-action.ts`, and the one `RecordDetailView` `useObjectLabel()` destructure line outside `paramCollectionHandler`). ## 维护者速读 - 改了什么:动作弹出的确认框和参数框,其「取消」按钮现在显示动作自己声明的 `cancelText`;没声明时仍是「Cancel」。翻译包里的 `_actions.ACTION.cancelText` 优先,其次是作者写的多语言文本,最后是作者写的字符串。设置 → 邀请里的「Cancel Invitation」确认框,取消按钮现在写「Keep invitation」(中/日/西已翻译)。Studio 的动作表单可以填这个键,`os i18n extract` 会为带 `confirmText` 或参数的动作生成它的翻译位。 - 为什么改:`cancelText` 已在 #28859 写进 spec,但控制台一直画「Cancel」;动作本身就叫「取消……」时,两个按钮分不清哪个是保留。 - 风险与回滚:只改按钮文字,确认与取消的逻辑不变;没写 `cancelText` 的动作完全不变。回滚即 revert 本 PR。 - 先看:`ui/packages/core/src/actions/ActionRunner.ts` 的 `confirmOptions`(把 `cancelText` 交给确认框);`ui/packages/react/src/hooks/useActionTextLocalizer.ts` 新增的 `cancelText` 解析;`packages/spec/liveness/action.json` 的 `cancelText` 行。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/core/src/actions/__tests__/ActionRunner.confirmCancelText-28879.test.ts packages/app-shell/src/views/ActionConfirmDialog.cancelText-28879.test.tsx packages/app-shell/src/views/ActionParamDialog.cancelText-28879.test.tsx`,预期 3 个文件全绿。 - 需要你做的:无,席位落地。 ## What changes **The confirm path.** `ActionDef` declares `cancelText?: SpecActionInput['cancelText']` (derived) and `ACTION_DEF_KEYS` lists it. `useActionTextLocalizer` resolves it only when the action declares one: the authored `I18nLabel` is collapsed to the active language, then offered as the fallback to `useObjectLabel().actionCancelText`, so the bundle's `objects.OBJECT._actions.ACTION.cancelText` (`globalActions.ACTION.cancelText` for an object-less action) wins. `ActionRunner#confirmOptions` hands the resolved non-empty string to the confirm handler as `options.cancelText`, which `ActionConfirmDialog` already draws over `actionConfirm.cancel`. The runner has no language, so it passes on a string only; a surface that skips the localizer keeps the host's "Cancel". **The param path.** `ParamDialogState` gains `cancelText?: string`. Both param-collection handlers (`useConsoleActionRuntime#paramCollectionHandler`, `RecordDetailView#paramCollectionHandler`) resolve it exactly as they resolve `submitText`, and `ActionParamDialog` draws it on the dismiss button, `actionDialog.cancel` when absent or empty. **The resolver.** `@object-ui/i18n` `useObjectLabel().actionCancelText`, the sibling of `actionSubmitText` on the same `actionSuffixes` node rule (a bound action never reads `globalActions`). The member-count pin in `useObjectLabel-identity-5564.test.tsx` goes from 29 to 30. **Types and forwarding.** `UIActionSchema.cancelText?: SpecAction['cancelText']` (amendment 6568954960). `action:button`, `action:icon`, `action:group` and `action:menu` forward `cancelText` to the runner; `action-forward-precedence.test.tsx` lists it after `submitText`. **`os i18n extract`** (`pushActionCancelText`). Scaffolds `cancelText` under an action's translation root for an action that declares `confirmText` or a non-empty `params`, the population `translateAction` overlays it onto, and no slot for any other action. **First consumer.** `sys_invitation`'s `cancel_invitation` declares `cancelText: 'Keep invitation'`. The platform-objects bundles regenerate (`node scripts/check-i18n-bundles.mjs --write`); zh-CN 保留邀请, ja-JP 招待を保持, es-ES Conservar invitación are hand-translated, as is the new Studio action-form row. **Spec rows.** `liveness/action.json` `cancelText` flips `planned` → `live` with the read points above and drops `authorWarn` / `authorHint`; `state-counts/action.md` regenerates. The `action` / `cancelText` omit row leaves `metadata-form-zod-reconciliation.test.ts`, and `action.form.ts` gains a plain `cancelText` row ("Cancel button label"), not gated, because the parse refuses the key on an action that opens neither dialog. "(declared ahead of its console reader: no renderer shows it yet)" leaves the `ActionSchema.cancelText` describe and "no renderer shows it yet" leaves the translation slot's; `content/docs/references/**` regenerates. In `actions.mdx` both "The console does not draw it yet" sentences flip, the `cancelText` one and the stale `submitText` one. ## Why `ActionSchema.cancelText` landed spec-first in #28859 with no reader, so an author who wrote it still saw "Cancel". On Setup → Invitations, **Cancel Invitation** asked with two buttons reading **Cancel** and **Cancel Invitation**. ## Tests - `ActionRunner.confirmCancelText-28879.test.ts` (5): the bag carries `cancelText` beside the label; a label-less action still hands it on; control, no key without it; `''` hands nothing; dismissing stops the run. - `ActionConfirmDialog.cancelText-28879.test.tsx` (3) and `ActionParamDialog.cancelText-28879.test.tsx` (4): the label is drawn, the fallback is Cancel (also for `''` on the param dialog), and the labelled button dismisses. - `useActionTextLocalizer.cancelText-28879.test.tsx` (6): authored string, bundle first, inline map collapsed, the global node for an object-less action, nothing for an undeclared key even with a bundle entry, a nameless action's map collapsed. - `useConsoleActionRuntime.cancelText-28879.test.tsx` (5) and `RecordDetailView.paramCancelText-28879.test.tsx` (5), one per param reader. - `useObjectLabel-actionCancelText-28879.test.tsx` (5): the object node, the global node, no global read for a bound action (with a control), the miss answers. - `RecordDetailView.paramRuntimeParity-6431.test.tsx`: `cancelText` joins `DIALOG_READS`, and the two runtimes write the same value. - `packages/cli/test/i18n-extract-action-cancel-text.test.ts` (7): top-level, global and inline actions; the seed-less slot; no slot without `confirmText` or `params`; bundle seed and coverage gating; one fixture through `ActionSchema` → extract → `ObjectTranslationDataSchema`. - Sibling pins that mock `useObjectLabel` partially gain an `actionCancelText` passthrough (seven `useConsoleActionRuntime.*` files), and `object-lifecycle-panel-echo-decisions.test.ts` counts the new action-form row (698 → 699). ## Acceptance notes - Two lines outside the claim's first file surface, both added by amendment 6568954960: `ui/packages/types/src/ui-action.ts` (the renderers' `schema.cancelText` does not type-check without it), and in `RecordDetailView.tsx` the `useObjectLabel()` destructure gains `actionCancelText` (the handler cannot call the hook). Nothing else in `RecordDetailView.tsx` moves. - `cancelText` is not interpolated: it is a button label, like `submitText`. - Wanted 5 (inline actions) and the two skills files are out of scope, as the card says. - `metadata-admin/MetadataTypeActions.tsx`, a third `ActionParamDialog` consumer (Setup metadata-type actions), passes neither `submitText` nor `cancelText`, so its buttons keep Confirm / Cancel. The card names the two business-action handlers only. Noted, not filed. ## Verification Readings at head `f566aa2e` (after merging `origin/main` at `00d0668d`) unless a line says otherwise. <details><summary>Derived gates: 112 derived, 111 run exit 0, 1 NOT MEASURED</summary> - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 112 families at `e389f7f7` (the pre-merge head; the merge brought nothing that overlaps the diff). Each ran with stdin closed and its exit captured before any pipe. - `dispatch-gates.mjs --ran`: 112 accounted, 111 run, 1 NOT-MEASURED: `pnpm check:dual-build-cjs-loads` exit 3 (PREREQUISITE NOT MET, needs a whole-workspace build), left to CI. - `check:skill-examples` and `check:i18n-coverage` first exited 3 on unbuilt prerequisites; after building `client-react` and the i18n-coverage closure both exit 0 (`check-i18n-coverage: OK (14 config(s), 426 baselined untranslated string(s), none new)`). - At `f566aa2e`: `node scripts/check-sdui-manifest.mjs` exit 0; `node scripts/check-i18n-bundles.mjs` exit 0; `pnpm --filter @objectstack/spec check:generated` "All 14 generated artifacts are up to date" after a spec rebuild; ui `check:new-line-citations` 0 new; `check-nul-bytes` exit 0. </details> <details><summary>Tests</summary> - ui, at `f566aa2e`: the 18 new or touched test files plus `RecordDetailView.childWriteRefreshesParent-12244.test.tsx` (the one overlap with main) → 19 files, 100 passed. - ui, at `e389f7f7`: `packages/core/src/actions/`, `packages/i18n/`, `packages/components/src/renderers/action/`, every `useActionTextLocalizer*`, `useConsoleActionRuntime*`, `ActionParamDialog*`, `ActionConfirmDialog*` and `RecordDetailView*` test → 330 files, 3779 passed, 13 skipped. `packages/types/` → 386 files, 10456 passed. `pnpm --dir ui exec vitest run scripts/` → 169 passed, 2 skipped, exit 0 (includes `check:action-forward-parity`). - ui type-check (turbo, `@object-ui/app-shell`, `components`, `core`, `i18n`, `react`, `types`) → 70/70 tasks, exit 0. - spec: `vitest run src/ui/ src/system/` → 238 files, 8155 passed; `scripts/liveness/` → 12 files, 359 passed; `typecheck` exit 0. - cli: `vitest run --project unit test/i18n-extract` → 12 files, 96 passed; `typecheck` exit 0. The integration tier is declared to CI. - platform-objects: `test` → 108 files, 1503 passed; `typecheck` exit 0. - Ablation (`scripts/ablation-replace.mjs`, committed first): deleting the `cancelText` spread in `confirmOptions` reds `ActionRunner.confirmCancelText-28879` (2 failed | 3 passed); replacing `state.cancelText || t('actionDialog.cancel')` with `t('actionDialog.cancel')` reds `ActionParamDialog.cancelText-28879` (2 failed | 2 passed). Both restored, blob == HEAD, `git diff HEAD` empty. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (Only a describe, a form row, a liveness row and a test ledger row change there.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` is CI's run. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 section is the Chinese summary the repo asks for). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QsR2jQtTxhjJ5tQUg79qSg --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jQtTxhjJ5tQUg79qSg)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c708de6 commit 462b4f9

54 files changed

Lines changed: 1171 additions & 54 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/components': patch
4+
'@object-ui/core': patch
5+
'@object-ui/i18n': patch
6+
'@object-ui/react': patch
7+
'@object-ui/types': patch
8+
'@objectstack/cli': patch
9+
'@objectstack/platform-objects': patch
10+
'@objectstack/spec': patch
11+
---
12+
13+
feat(console): an action's confirm and param dialogs name their dismiss button — `cancelText` is drawn in place of "Cancel"
14+
15+
Clause-②: yes (widening)
16+
Metadata-protocol: none
17+
18+
`@object-ui/types` `UIActionSchema` and `@object-ui/core` `ActionDef` gain the optional member `cancelText`, typed by derivation from the spec's `ActionSchema`; `@object-ui/i18n` `useObjectLabel()` gains the resolver `actionCancelText`. Nothing is narrowed.
19+
20+
- **The console.** The dismiss button of the dialog an action opens before it runs draws the action's `cancelText`: on the confirm dialog of an action with `confirmText`, and on the param dialog of an action with `params`. Both paths resolve it the same way: the translation bundle's `objects.OBJECT._actions.ACTION.cancelText` (`globalActions.ACTION.cancelText` for an object-less action) first, then the authored inline locale map collapsed to the active language, then the authored string. An action that names no label keeps "Cancel". `action:button`, `action:icon`, `action:group` and `action:menu` forward the key to the runner, which hands it to the confirm dialog.
21+
- **Setup → Invitations.** **Cancel Invitation** now asks with **Keep invitation** beside its confirm button, translated in every shipped locale.
22+
- **Studio.** The action form offers the key ("Cancel button label"), translated in every shipped locale.
23+
- **`os i18n extract`.** Scaffolds `objects.OBJECT._actions.ACTION.cancelText` (or `globalActions.ACTION.cancelText`) for an action that declares `confirmText` or a non-empty `params`, the actions `translateAction` overlays it onto, and no slot for any other action.
24+
- **Spec.** The liveness ledger row for `ActionSchema.cancelText` is `live`, with its author warning dropped; the key's and the translation slot's descriptions no longer say no renderer shows it.

‎content/docs/protocol/objectui/actions.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ params:
538538
- It is not `confirmText`. `confirmText` is the message of the confirm dialog a param-less action opens; `submitText` is the button of the param dialog an action with params opens instead.
539539
- A blank string (`''` or whitespace only) is refused at parse. Omit the key to keep **Confirm**.
540540
- `submitLabel` and `confirmLabel` (the button's spelling on a view's `bulkActionDefs`) are refused with a rename to `submitText`.
541-
- The console does not draw it yet: until its param dialog reads the key, the button says **Confirm**, and `os validate` warns an author who declares it.
541+
- The console draws it on the param dialog's submit button. Without it, the button says **Confirm**, and while a file param uploads it says **Uploading…** either way.
542542

543543
### Naming the dismiss button: `cancelText`
544544

@@ -558,7 +558,7 @@ cancelText: Keep invitation
558558
- **Either pre-run dialog.** It labels the dismiss button of the confirm dialog a param-less action with `confirmText` opens, and of the param dialog an action with a non-empty `params` opens (beside that dialog's `submitText`). An action that declares neither opens no dialog, so `cancelText` there is refused at parse, and a translation of it under such an action is reported by `os validate` (`translation-target-unknown`).
559559
- A blank string (`''` or whitespace only) is refused at parse. Omit the key to keep **Cancel**.
560560
- `cancelLabel` (the button's spelling on the console's `form` and `alert-dialog` renderers) is refused with a rename to `cancelText`.
561-
- The console does not draw it yet: until its confirm and param dialogs read the key, the button says **Cancel**, and `os validate` warns an author who declares it.
561+
- The console draws it on the dismiss button of both dialogs, from the translation bundle first and then the authored text. Without it, the button says **Cancel**. `sys_invitation`'s **Cancel Invitation** action declares **Keep invitation**.
562562

563563
## AI Tool Exposure (ADR-0011)
564564

‎content/docs/references/data/object.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ const result = ApiMethod.parse(data);
491491
| **execution** | `Enum<'perRecord' \| 'aggregate'>` | optional | The bulk dispatch contract this action's BODY is written for, in `bulkActionDefs`' own vocabulary: 'perRecord' = one dispatch per selected row carrying that row's `recordId` (the view's `bulkActions: ['<name>']` bare-string form); 'aggregate' = ONE dispatch for the whole selection carrying every id in `params._selectedIds` (a `bulkActionDefs` entry with `execution: 'aggregate'`). Optional with NO default — omit it only when the body genuinely serves both. A list view wiring a declared action under the other contract is refused by `@objectstack/lint` (`action-dispatch-contract-mismatch`). |
492492
| **params** | `{ name?: string; field?: string; objectOverride?: string; label?: string \| Record<string, string>; … }[]` | optional | Input parameters required from user — an ActionParam[] DEFINITION array, never a payload map (a static request body goes in `bodyExtra`). A param name (its `name`, or the `field` a field-backed param reads) may repeat only for per-gate variants of ONE input (declarations that differ in `visible` / `requiresFeature`, e.g. one optional and one required); the params check treats the name as `required` only when EVERY declaration of it requires it, and judges its value by the FIRST declaration's shape (`type`, `multiple`, `options`), so keep that shape identical across them. |
493493
| **submitText** | `string \| Record<string, string>` | optional | Label of the submit button in the action's param dialog, the verb the button performs (e.g. "Send", "Log Call"), in place of the default "Confirm" — the default-language string, or an inline locale map; translated at `objects.OBJECT._actions.ACTION.submitText` (`globalActions.ACTION.submitText` for an object-less action). Only on an action with a non-empty `params`: without one no param dialog opens, so it is refused. Not `confirmText`, which is the message of a param-less action's confirm dialog. A blank label is refused. |
494-
| **cancelText** | `string \| Record<string, string>` | optional | Label of the dismiss button on the dialog the action opens before it runs — the confirm dialog of a param-less action that declares `confirmText`, or the param dialog of an action with a non-empty `params` — in place of the default "Cancel" (declared ahead of its console reader: no renderer shows it yet). Name what dismissing keeps where the action's own verb is "Cancel …" or "Close …" (e.g. "Keep invitation" beside "Cancel Invitation"). The default-language string, or an inline locale map; translated at `objects.OBJECT._actions.ACTION.cancelText` (`globalActions.ACTION.cancelText` for an object-less action). Only on an action that opens one of those dialogs: with neither `confirmText` nor a non-empty `params` no dialog opens, so it is refused. The dismiss twin of `submitText`, as on `object-form`. A blank label is refused. |
494+
| **cancelText** | `string \| Record<string, string>` | optional | Label of the dismiss button on the dialog the action opens before it runs — the confirm dialog of a param-less action that declares `confirmText`, or the param dialog of an action with a non-empty `params` — in place of the default "Cancel". Name what dismissing keeps where the action's own verb is "Cancel …" or "Close …" (e.g. "Keep invitation" beside "Cancel Invitation"). The default-language string, or an inline locale map; translated at `objects.OBJECT._actions.ACTION.cancelText` (`globalActions.ACTION.cancelText` for an object-less action). Only on an action that opens one of those dialogs: with neither `confirmText` nor a non-empty `params` no dialog opens, so it is refused. The dismiss twin of `submitText`, as on `object-form`. A blank label is refused. |
495495
| **variant** | `Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'link'>` | optional | Button visual variant for styling (primary = highlighted, danger = destructive, ghost = transparent) |
496496
| **order** | `number` | optional | Sort order within a location group (lower = higher). Promotes/demotes an action toward the record_header primary button; stable, so actions without `order` keep their registration order. |
497497
| **confirmText** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object } \| string \| Record<string, string>` | optional | Confirmation message before execution — a `template` slot (ADR-0032): a bare string is shorthand for `{ dialect: 'template', source }`, and text with no hole is shown as written. Holes are `{{ path }}` or `{{ path \| formatter[:arg] }}` over `record`, the record the action runs against (the clicked row on a list, else the page's record), and — only when the action's `params` is an object of static values — `params` (`{{ params.tier }}`); an input list binds nothing, since its values are collected after the confirmation. Formatters come from the template engine's whitelist (`TEMPLATE_FORMATTERS`, `@objectstack/formula`); a hole holds no logic (no CEL, no arithmetic). Example: 'Delete `{{ record.name }}`?'. A hole over any other root, a `${…}` or single-brace `{x}` token (each named with its `{{ }}` spelling), a hole holding logic or an unknown formatter, and a blank sentence are named and refused at `objectstack validate`, `compile` and the publish gate; the text still parses, so a stored one loads until its next save. An inline locale map (`{ en, "zh-CN" }`) is accepted too, each value such a sentence. On a registered action, pairing this with a non-empty `params` is refused — that opens a second dialog for one decision; put the question on `description` instead. Correct on a param-LESS action, where the confirm is the only dialog there is. |

‎content/docs/references/kernel/metadata-plugin.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ const result = MetadataBulkResultSchema.parse(data);
336336
| **execution** | `Enum<'perRecord' \| 'aggregate'>` | optional | The bulk dispatch contract this action's BODY is written for, in `bulkActionDefs`' own vocabulary: 'perRecord' = one dispatch per selected row carrying that row's `recordId` (the view's `bulkActions: ['<name>']` bare-string form); 'aggregate' = ONE dispatch for the whole selection carrying every id in `params._selectedIds` (a `bulkActionDefs` entry with `execution: 'aggregate'`). Optional with NO default — omit it only when the body genuinely serves both. A list view wiring a declared action under the other contract is refused by `@objectstack/lint` (`action-dispatch-contract-mismatch`). |
337337
| **params** | `{ name?: string; field?: string; objectOverride?: string; label?: string \| Record<string, string>; … }[]` | optional | Input parameters required from user — an ActionParam[] DEFINITION array, never a payload map (a static request body goes in `bodyExtra`). A param name (its `name`, or the `field` a field-backed param reads) may repeat only for per-gate variants of ONE input (declarations that differ in `visible` / `requiresFeature`, e.g. one optional and one required); the params check treats the name as `required` only when EVERY declaration of it requires it, and judges its value by the FIRST declaration's shape (`type`, `multiple`, `options`), so keep that shape identical across them. |
338338
| **submitText** | `string \| Record<string, string>` | optional | Label of the submit button in the action's param dialog, the verb the button performs (e.g. "Send", "Log Call"), in place of the default "Confirm" — the default-language string, or an inline locale map; translated at `objects.OBJECT._actions.ACTION.submitText` (`globalActions.ACTION.submitText` for an object-less action). Only on an action with a non-empty `params`: without one no param dialog opens, so it is refused. Not `confirmText`, which is the message of a param-less action's confirm dialog. A blank label is refused. |
339-
| **cancelText** | `string \| Record<string, string>` | optional | Label of the dismiss button on the dialog the action opens before it runs — the confirm dialog of a param-less action that declares `confirmText`, or the param dialog of an action with a non-empty `params` — in place of the default "Cancel" (declared ahead of its console reader: no renderer shows it yet). Name what dismissing keeps where the action's own verb is "Cancel …" or "Close …" (e.g. "Keep invitation" beside "Cancel Invitation"). The default-language string, or an inline locale map; translated at `objects.OBJECT._actions.ACTION.cancelText` (`globalActions.ACTION.cancelText` for an object-less action). Only on an action that opens one of those dialogs: with neither `confirmText` nor a non-empty `params` no dialog opens, so it is refused. The dismiss twin of `submitText`, as on `object-form`. A blank label is refused. |
339+
| **cancelText** | `string \| Record<string, string>` | optional | Label of the dismiss button on the dialog the action opens before it runs — the confirm dialog of a param-less action that declares `confirmText`, or the param dialog of an action with a non-empty `params` — in place of the default "Cancel". Name what dismissing keeps where the action's own verb is "Cancel …" or "Close …" (e.g. "Keep invitation" beside "Cancel Invitation"). The default-language string, or an inline locale map; translated at `objects.OBJECT._actions.ACTION.cancelText` (`globalActions.ACTION.cancelText` for an object-less action). Only on an action that opens one of those dialogs: with neither `confirmText` nor a non-empty `params` no dialog opens, so it is refused. The dismiss twin of `submitText`, as on `object-form`. A blank label is refused. |
340340
| **variant** | `Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'link'>` | optional | Button visual variant for styling (primary = highlighted, danger = destructive, ghost = transparent) |
341341
| **order** | `number` | optional | Sort order within a location group (lower = higher). Promotes/demotes an action toward the record_header primary button; stable, so actions without `order` keep their registration order. |
342342
| **confirmText** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object } \| string \| Record<string, string>` | optional | Confirmation message before execution — a `template` slot (ADR-0032): a bare string is shorthand for `{ dialect: 'template', source }`, and text with no hole is shown as written. Holes are `{{ path }}` or `{{ path \| formatter[:arg] }}` over `record`, the record the action runs against (the clicked row on a list, else the page's record), and — only when the action's `params` is an object of static values — `params` (`{{ params.tier }}`); an input list binds nothing, since its values are collected after the confirmation. Formatters come from the template engine's whitelist (`TEMPLATE_FORMATTERS`, `@objectstack/formula`); a hole holds no logic (no CEL, no arithmetic). Example: 'Delete `{{ record.name }}`?'. A hole over any other root, a `${…}` or single-brace `{x}` token (each named with its `{{ }}` spelling), a hole holding logic or an unknown formatter, and a blank sentence are named and refused at `objectstack validate`, `compile` and the publish gate; the text still parses, so a stored one loads until its next save. An inline locale map (`{ en, "zh-CN" }`) is accepted too, each value such a sentence. On a registered action, pairing this with a non-empty `params` is refused — that opens a second dialog for one decision; put the question on `description` instead. Correct on a param-LESS action, where the confirm is the only dialog there is. |

0 commit comments

Comments
 (0)