Skip to content

Commit 76bf6c5

Browse files
feat(spec): an action's confirm and param dialogs can name their dismiss button — cancelText (#28859)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9Ut7hw65Gi64QMYVAVzsgLi)_ Part of #28519 Clause-②: yes (widening) Metadata-protocol: added Skills-docs: follow-up owed — `skills/objectstack-ui/rules/actions.md` (the action key list teaches `submitText` beside `confirmText`) and `skills/objectstack-i18n/SKILL.md` (the `_actions.{action_name}` key row). Both are governed (Tier H), so they ride a separate PR rather than this one; neither turns false here, they only lack the new key. ## 维护者速读 - 改了什么:动作(action)新增可选键 `cancelText`,给动作弹出的确认框或参数框的「取消」按钮起名字;可写纯字符串或多语言映射,翻译路径 `objects.对象._actions.动作.cancelText`;空字符串、或在不弹框的动作上写它,解析时直接拒收。 - 为什么改:动词本身就是「取消……」的动作(如邀请列表的「Cancel Invitation」)弹确认框时,两个按钮是「Cancel」和「Cancel Invitation」,管理员分不清哪个是保留邀请。这次先落 spec 半边,控制台画按钮、`sys_invitation` 声明「Keep invitation」留给后续卡(与 `submitText` 当初的拆法相同)。 - 风险与回滚:纯放宽,不写这个键的动作解析结果完全不变;目前没有界面读它,liveness 标 `planned` 并让 `os validate` 提醒作者「按钮还显示 Cancel」。回滚即撤掉本 PR。 - 先看:`packages/spec/src/ui/action.zod.ts`(`actionCancelText` 与新加的第三条 refine:没有 `confirmText` 也没有非空 `params` 时拒收);`packages/spec/src/system/i18n-resolver.ts` 的 `translateAction` 叠加条件;`packages/lint/src/validate-translation-references.ts` 的 `checkActionCancelText`。 - 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run src/ui/action-cancel-text.test.ts`,应看到 20 条全过。 - 需要你做的:确认新公开键名 `cancelText`(席位已在项目里问你);其余无,席位落地。 ## What changes - **`ActionSchema.cancelText`** (`@objectstack/spec`, `@objectstack/spec/ui`): an optional `I18nLabel`, the label of the dismiss button on the dialog an action opens before it runs, in place of the console's default "Cancel". Two dialogs carry that button and an action opens at most one: the confirm dialog of a param-less action that declares `confirmText`, and the param dialog of an action with a non-empty `params` (beside that dialog's `submitText`). - A blank string (`''` or whitespace only) is refused at parse, through `NON_BLANK_STRING`, so the published JSON Schema carries the rule too. - A new refinement refuses the key on an action that declares neither `confirmText` nor a non-empty `params`: no dialog opens, so the label would parse clean and never show (ADR-0078). It mirrors `submitText`'s no-`params` refusal, widened to the two hosts this button has. - `cancelLabel` is an alias that renames to `cancelText`, on the action and in a translation bundle. It is the spelling of the same button on the console's `form` and `alert-dialog` renderers, the `*Label` twin of `submitLabel`. - `InlineActionSchema` does not pick the key, as it does not pick `submitText`: no inline host forwards either. - **Translation slot.** `objects.OBJECT._actions.ACTION.cancelText` and `globalActions.ACTION.cancelText` (`translation.zod.ts`). `translateAction` overlays it for an action that opens either dialog, judged on the AUTHORED `confirmText` / `params`, and never gives a button to an action that opens none. - **Lint.** `validateTranslationReferences` reports a `cancelText` translation under an action that declares neither `confirmText` nor `params` as `translation-target-unknown` at `error` (`checkActionCancelText`), at both addresses. - **Ledgers.** `liveness/action.json` gains a `planned` row with `authorWarn` (nothing draws the key yet), and `state-counts/action.md` is regenerated (planned 2 → 3). The metadata-form reconciliation ledger gains an `omit` row (no Studio control is offered until the console draws the key). `authorable-surface/ui.json` lists `ui/Action:cancelText`. `SPEC_ACTION_KEYS` in `@object-ui/core` lists it. - **Docs.** A `cancelText` section in `content/docs/protocol/objectui/actions.mdx` beside `submitText`'s, the interface listing line, and the `content/docs/ui/translations.mdx` action row. The `content/docs/references/**` pages are regenerated. - The `submitText` docblock now names `actionCancelText` as its pair. `object-form`'s own `submitText` / `cancelText` describes in `component.zod.ts` are left as they are: `submitText`'s precedent added no back-reference there either. ### Forced test edits - `packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts`: the served `action` key count 51 → 52, and the sample names `cancelText`. - `packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`: the new `omit` row for `cancelText` (a schema key the action form does not offer must be ledgered). - `packages/lint/src/validate-translation-references.test.ts`: the action-entry coverage table classifies every key the translation shape declares, so `cancelText` gets a `reference-checked` row (host `noPreRunDialog`), and the real-instance leg carries it. - `ui/packages/core/src/actions/actionKeys.ts`: the `SPEC_ACTION_KEYS` inventory, pinned against the spec's declared keys by `actionKeys.pin.test.ts`. ## Why #28519: on Setup → Invitations, the row menu's **Cancel Invitation** opens a confirm whose two buttons read **Cancel** and **Cancel Invitation**. `ActionSchema` has the param dialog's submit label (`submitText`) but no key for the dismiss button. This PR is the spec half (Wanted 1). Wanted 2 (the console's `ActionConfirmDialog` / `ActionParamDialog` draw it) and Wanted 3 (`sys_invitation.cancel_invitation` declares `cancelText: 'Keep invitation'` with its translations) are the ui / platform-objects half, split the way `submitText` was split. ## Tests - `packages/spec/src/ui/action-cancel-text.test.ts` (new, 20 tests): both I18nLabel forms on both host dialogs, optional, the non-I18nLabel shapes `label` refuses, the blank refusal on both hosts, the no-dialog refusal (no `params` and `params: []`, string and locale map) with its control, the `cancelLabel` rename, not on `InlineActionSchema`, the translation slot at both addresses with the bundle alias, and `translateAction` on the confirm host, the param host, `globalActions`, the locale fallback, an action that authors none, and an action that opens no dialog. - `packages/lint/src/validate-translation-references.test.ts`: a `cancelText` describe (errors at both addresses under an action that opens no dialog; the controls: accepted under a confirm host and under a param host), plus the coverage rows above. ## Acceptance notes - **Implementation choice, raised for the seat:** the no-dialog refusal is not in the card's Wanted 1 text. It is the `submitText` precedent's second rule carried over, because without it `cancelText` on a click-to-run action would parse clean and never show. It judges `confirmText != null` (the declaration), not the console's truthiness test: a blank `confirmText` already fails `os validate`, `compile` and the publish gate, so the refusal does not add a second message for it. - **Default stays the console's.** Absent, both dialogs keep their own "Cancel" (`actionConfirm.cancel` / `actionDialog.cancel`). The console's confirm dialog already draws `options.cancelText` over its default; what it lacks is the runner handing the action's key to it (`ActionRunner.confirmOptions` passes `title` / `confirmText` / `destructive` only, and `ActionDef` does not declare the key). That is the follow-up's first file. - **Migrations checked, nothing owed:** `git grep -c "cancelLabel\|cancelText" -- packages/spec/src/migrations packages/spec/src/conversions` answers 0 files (control: `submitLabel` hits 4 files, 17 lines). `element:form`'s retired keys are six and none is a cancel label (`retired-keys/18.ui__ElementFormProps__*`), and the `translation-component-submit-label-retired` D3 entry concerns the component copy key `submitLabel` alone. `cancelText` moves no existing key, so no conversion or semantic entry is owed. - **Stale sentence noted, not fixed here:** `content/docs/protocol/objectui/actions.mdx`'s `submitText` section still ends "The console does not draw it yet", which #28392 made false (the liveness row is `live`). Not this card's defect class; carrier: the console follow-up of this card, which edits the same file to flip `cancelText`'s matching sentence. ## Verification All readings on head `6e2b3ddb` (this branch with `origin/main` `3e62a22c` merged in) unless a line says otherwise; every heavy run went through `os-verify-lock.sh`, whose `VERDICT command-exit` line is quoted. <details><summary>Package suites and type checks (the packages this diff touches)</summary> - `pnpm --filter @objectstack/spec test`: Test Files 789 passed (789), Tests 23311 passed, 1 todo. VERDICT command-exit 0. - `pnpm --filter @objectstack/lint test`: Test Files 189 passed (189), Tests 7829 passed (7829). VERDICT command-exit 0. - `pnpm --filter @objectstack/metadata-protocol test`, re-run after the `main` merge (main moved `metadata-protocol/src`): Test Files 297 passed, 4 skipped (301), Tests 22093 passed, 26 skipped. VERDICT command-exit 0. - `pnpm --filter @object-ui/core test`: Test Files 228 passed (228), Tests 4226 passed, 27 skipped. VERDICT command-exit 0. - `typecheck` for `@objectstack/spec`, `@objectstack/lint`, `@objectstack/metadata-protocol` (spec's includes `check:test-typecheck`): all Done, VERDICT command-exit 0; `@object-ui/core` `type-check`: VERDICT command-exit 0. - `eslint --no-inline-config` over the eight changed `.ts` files under `packages/`: exit 0; `ui` eslint over `ui/packages/core/src/actions/actionKeys.ts`: exit 0. - `pnpm --filter @objectstack/spec check:generated --fix`: regenerated exactly `content/docs/references/**` and `liveness/state-counts/`, then re-checked both green; `check:api-surface` green against a dist that run built (no export moved). </details> <details><summary>Ablations (one per changed tested behaviour; each landed on disk through ablation-replace and restored to the HEAD blob)</summary> - `action.zod.ts`, the no-dialog refinement's `data.cancelText != null` → `false`: `action-cancel-text.test.ts` 2 failed | 18 passed (the two no-dialog refusals). Restored: blob == HEAD. - `validate-translation-references.ts`, `checkActionCancelText`'s `confirmText` guard → bare `return`: `validate-translation-references.test.ts` 2 failed | 231 passed (the `cancelText` error leg and the coverage table's reference-checked leg). Restored. - `i18n-resolver.ts`, `const cancelText = opensDialog` → `true`: `action-cancel-text.test.ts` 1 failed | 19 passed ("gives no button to an action that opens no dialog"). Restored. - Reverse verification of the cross-package pin: `'cancelText'` deleted from `SPEC_ACTION_KEYS` → `actionKeys.pin.test.ts` 1 failed | 18 passed, `missing: ['cancelText']`, so the pin reads the rebuilt spec. Restored. </details> <details><summary>Derived gate families (dispatch-gates)</summary> - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `6e2b3ddb`: 109 commands, change set 21 paths vs merge base `3e62a22c4`, 525 changed lines (+513 / -12). - All 109 exit 0. `check-plugin-teardown-shape.mjs --self-test` first exited 3 (its pinned fixture commit is outside the shallow clone); after `git fetch --depth=1 origin 621a487…` it reports 48 cases pass, exit 0. - `dispatch-gates --ran`: "109 derived famil(ies) accounted for — 109 run, 0 NOT-MEASURED (a DERIVED zero)". - `check-changeset-no-major.mjs --base origin/main --event` (this body, offline): exit 0, `@objectstack/spec: minor` for `Clause-②: yes (widening)` + `Metadata-protocol: added`. - Local-only families: `check:skill-frame-freshness` exit 0; `check:dev-prereqs` exit 1 as a box precondition (34 `ui/` workspace dists not built in this container; the `packages/**` build is 78/78 tasks), not a reading about this diff. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [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. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmNhdF5oapRdMKNBctQ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 69c9f7c commit 76bf6c5

21 files changed

Lines changed: 513 additions & 12 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): an action's confirm and param dialogs can name their dismiss button — `cancelText`
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: added
9+
10+
`ActionSchema` gains one optional key, `cancelText`: the label of the dismiss button on the dialog an action opens before it runs. The button says "Cancel" today for every action. That reads badly when the action's own verb is "Cancel …" or "Close …": a "Cancel Invitation" confirm whose two buttons read "Cancel" and "Cancel Invitation" leaves the user unable to tell which one keeps the invitation. With the key, the action names what dismissing keeps. An action that does not declare it parses exactly as before.
11+
12+
```ts
13+
defineAction({
14+
name: 'cancel_invitation',
15+
label: 'Cancel Invitation',
16+
objectName: 'sys_invitation',
17+
type: 'api',
18+
target: '/api/v1/auth/organization/cancel-invitation',
19+
confirmText: 'Cancel this invitation? The recipient will no longer be able to accept it.',
20+
cancelText: 'Keep invitation',
21+
});
22+
```
23+
24+
- **An `I18nLabel`.** A plain string or an inline locale map (`{ en, 'zh-CN' }`). A blank string is refused at parse: `''` or whitespace only. Omit the key to keep "Cancel". `cancelLabel` (the same button's spelling on the console's `form` and `alert-dialog` renderers) is refused with a rename to `cancelText`, on the action and in a translation bundle.
25+
- **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, naming the key.
26+
- **Translated like `submitText`.** The slot is `objects.OBJECT._actions.ACTION.cancelText` (`globalActions.ACTION.cancelText` for an action with no `objectName`). `translateAction` overlays it for an action that opens either dialog, and never gives a button to an action that opens none.
27+
- **The name.** `cancelText` is the protocol's existing word for a dismiss button's copy: the `object-form` component's `cancelText`, beside its `submitText`.
28+
29+
Nothing draws the key yet. The console's confirm and param dialogs read it in a follow-up change. Until then the liveness ledger marks it `planned` with `authorWarn`, so `os validate` warns an author who declares it that the button still says "Cancel".
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@object-ui/core': patch
3+
---
4+
5+
`SPEC_ACTION_KEYS` lists `cancelText`, the key `@objectstack/spec` adds to `ActionSchema` for the dismiss button label of an action's confirm and param dialogs. An action that carries it is not counted as having an unknown key, and the inventory pin stays green.
6+
7+
Clause-②: no
8+
Metadata-protocol: none
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
`validateTranslationReferences` judges an action translation's new `cancelText` key (`@objectstack/spec`'s dismiss label of an action's confirm and param dialogs). Under an action that declares neither `confirmText` nor `params`, at `objects.OBJECT._actions.ACTION.cancelText` or `globalActions.ACTION.cancelText`, it reports `translation-target-unknown` at `error`: such an action opens no dialog, so nothing reads the label. Under an action that declares either the key is accepted.
6+
7+
Clause-②: no
8+
Metadata-protocol: none

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ interface Action {
231231
body?: HookBody; // Inline logic for type: 'script'
232232
params?: ActionParam[]; // Inputs collected before execution
233233
submitText?: I18nLabel; // The param dialog's submit button verb (default "Confirm")
234+
cancelText?: I18nLabel; // The confirm or param dialog's dismiss button label (default "Cancel")
234235
method?: 'POST' | 'PATCH' | 'PUT' | 'DELETE'; // for type: 'api'
235236
236237
// UX
@@ -539,6 +540,26 @@ params:
539540
- `submitLabel` and `confirmLabel` (the button's spelling on a view's `bulkActionDefs`) are refused with a rename to `submitText`.
540541
- 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.
541542

543+
### Naming the dismiss button: `cancelText`
544+
545+
The dialog an action opens before it runs, its confirm dialog or its param dialog, has a dismiss button that says **Cancel**. That reads badly when the action's own verb is "Cancel …" or "Close …": a **Cancel Invitation** confirm whose two buttons read **Cancel** and **Cancel Invitation** leaves the user unable to tell which one keeps the invitation. `cancelText` names what dismissing keeps.
546+
547+
```yaml
548+
name: cancel_invitation
549+
label: Cancel Invitation
550+
objectName: sys_invitation
551+
type: api
552+
target: /api/v1/auth/organization/cancel-invitation
553+
confirmText: Cancel this invitation? The recipient will no longer be able to accept it.
554+
cancelText: Keep invitation
555+
```
556+
557+
- It is an `I18nLabel`: a plain string, translated at `objects.<object>._actions.<action>.cancelText` (`globalActions.<action>.cancelText` for an action with no `objectName`), or an inline locale map.
558+
- **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`).
559+
- A blank string (`''` or whitespace only) is refused at parse. Omit the key to keep **Cancel**.
560+
- `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.
562+
542563
## AI Tool Exposure (ADR-0011)
543564

544565
An action can be exposed as an AI-callable tool. Exposure is **opt-in and default off** — set `ai.exposed: true`, which then **requires** `ai.description` (≥ 40 chars, the LLM-facing contract).

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +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. |
494495
| **variant** | `Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'link'>` | optional | Button visual variant for styling (primary = highlighted, danger = destructive, ghost = transparent) |
495496
| **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. |
496497
| **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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +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. |
339340
| **variant** | `Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'link'>` | optional | Button visual variant for styling (primary = highlighted, danger = destructive, ghost = transparent) |
340341
| **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. |
341342
| **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)