Skip to content

Commit 2c881fb

Browse files
fix(spec)!: an inline action refuses confirmText beside a non-empty params, as a registered action does (#29829)
domain:spec#3 · os-support-ai · session_01WGufzfHVbsfKWji5VLrLbG <!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBN6TwTfduBnZH2gwPifJvvc)_ Fixes #29383 Ruling-ref: 6595808462 Clause-②: no (narrowing) Metadata-protocol: narrowed Skills-docs: none — no `skills/**` file teaches an inline action carrying `confirmText` beside `params`. The one rule line about the pair (`skills/objectstack-ui/rules/actions.md` L22, "A registered action is refused it beside a non-empty `params` (put the question on `description`)") stays true and names the remedy this refusal prescribes; the generated `content/docs/references/**` tables that render the `confirmText` describe are regenerated here. **BREAKING** accept-set narrowing at `InlineActionSchema` (`@objectstack/spec`), served where a page's `element:button` `properties.action` is judged: the component-props gate (`component-props-invalid`, `@objectstack/lint`) at every CLI command and at the runtime `page` save door. ## 维护者速读 - 改了什么:页面按钮上内联声明的动作(`element:button` 的 `properties.action`)同时写 `confirmText` 和非空 `params` 时,现在和已注册动作一样被拒收,提示把这句话挪到 `description`、删掉 `confirmText`。 - 为什么改:您在 #29383 裁定 A(「同意」)。内联动作和注册动作走同一个执行器,先弹确认框、再弹参数框,一个决定两个对话框;`description` 已能写在内联动作上,作者有地方挪这句话。 - 风险与回滚:仓内普查 0 处已发运的内联动作带这对组合,只翻转 spec 自己的一条测试 pin;不带 `params` 或 `params: []` 的 `confirmText` 照常通过,批量动作不受影响。已存页面照常加载,下次保存才被拒。回滚即 revert 本 PR。 - 先看:`packages/spec/src/ui/action.zod.ts`(新的共用判定 `declaresConfirmBesideParams` 与 `CONFIRM_BESIDE_PARAMS_REFUSAL`,`InlineActionSchema` 链上新增的那条 `.refine`),`packages/spec/src/ui/action-confirm-params-guard.test.ts` 末尾新的 describe 组,`.changeset/29383-inline-action-confirm-beside-params-refused.md`。 - 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run src/ui/action-confirm-params-guard.test.ts`,预期 16 个测试全过,其中内联组 5 条(2 条拒收、3 条对照)。 - 需要你做的:无,席位落地。 ## What changes - **One predicate, one refusal, two doors.** `packages/spec/src/ui/action.zod.ts` lifts the `ActionSchema` pair rule out of its refine body into a module-level predicate, `declaresConfirmBesideParams(data)` (`confirmText != null` and `params` a non-empty array, unchanged), and a module-level refusal, `CONFIRM_BESIDE_PARAMS_REFUSAL` (the existing message, byte for byte, at path `['confirmText']`). `ActionSchema`'s refinement now reads `return !declaresConfirmBesideParams(data)` with that refusal; `InlineActionSchema` gains one `.refine` with the same two, between its target-required rule and its url-target rule. No second copy of either. - **The comment at the refinement** no longer says the inline pair is "a separate narrowing question, not decided here". It states that `InlineActionSchema` carries the same refusal on its own chain, by ruling record 6595808462 letter A, and why (same runner, so the same two dialogs; its pick carries `description`, so the remedy has a slot). - **The `confirmText` describe** dropped its "On a registered action," qualifier ("pairing this with a non-empty `params` is refused …"), since both doors that render this describe now refuse the pair; a view's `bulkActionDefs` declares its own `confirmText`. The NB comment above the field says so. `content/docs/references/{data/object,kernel/metadata-plugin,ui/action,ui/component}.mdx` are regenerated from it (`check:generated --fix`, which proved only `check:docs` stale). - **Changeset** `.changeset/29383-inline-action-confirm-beside-params-refused.md`: `@objectstack/spec` minor, both declaration lines, the ADR-0087 disposition marker, the narrowing banner, before / now / unchanged / who, and the remedy. ### Before / after at the inline door, on the census case The census case is the one inline carrier the census found, the pin in `action-confirm-params-guard.test.ts`: ```ts InlineActionSchema.safeParse({ type: 'url', target: '/approvals?reject=1', label: 'Reject', confirmText: 'Reject this request?', params: [{ name: 'reason', label: 'Reason', type: 'textarea' }], }); ``` - **Before** (`origin/main` `b3d14150`): `success: true`, pinned by the test "InlineActionSchema is out of reach too — it picks fields, not this refine chain". - **After** (this head): `success: false`, one issue at `['confirmText']`, `code: 'custom'`, and its message equals `ActionSchema`'s own issue for the registered pair (asserted with `toBe`). With `description` in place of `confirmText`, with no `params`, and with `params: []`, the same action parses. - **At the authoring door.** The page component-props gate (`validateComponentProps`, `@objectstack/lint` src against this head's spec `dist`), on a page whose `element:button` carries `{ type: 'api', target: '/api/v1/approvals/reject', params: [reason], confirmText: 'Reject this request?' }`: one finding, `component-props-invalid`, severity `error`, at `pages[0].regions[0].components[0].properties.action.confirmText`, message opening "action.confirmText: An action that declares `confirmText` beside a non-empty `params` shows the user TWO dialogs for one decision". The remedy body (`description`, no `confirmText`), the param-less body and the `params: []` body: no finding. - **`defineStack` does not judge it**, before or after: a component's `properties` is an open bag that the stack parse does not dispatch (measured: an inline `cancelText`, an inline url action with no `target`, and the pair all pass `defineStack` on this head). The changeset names the props gate as the door, not `defineStack`. ### ADR-0087 disposition The changeset body carries the ADR-0087 marker comment with the disposition `adr-0087: not-required (no-migration-prescription)`, the form the repo's narrowing changesets use (223 of the `.changeset/*.md` markers on `origin/main` are `not-required (no-migration-prescription)`; the closest precedents are `26413-now-anchor-extra-key-refused.md` and `26462-diff-missing-version-refused.md`). No `docs/adr/**` path is touched. The reasoning: no key is added, removed or renamed; the census found 0 shipped carriers; and the one rewrite there could be (move `confirmText` into `description`) needs the author to read the sentence, so the refusal names it and nothing converts it. ### Census, re-run at the base At `origin/main` `b3d14150` (report 6580676225 counted at `a6441636`): every file under `examples/`, `packages/**`, `ui/**`, `skills/**`, `templates`, `apps/` carrying `confirmText` (excluding `CHANGELOG.md`), each `confirmText` line read with a ±25-line window that holds an inline marker (`element:button`, `InlineAction`, `properties:`, `action: {`) and a non-empty `params: [`. 16 windows in 5 files, each read by hand: - `packages/spec/src/ui/action-confirm-params-guard.test.ts` — the inline pin above (the one carrier; flipped here) and `ActionSchema` / `BulkActionDefSchema` fixtures. - `packages/lint/src/validate-action-copy-templates.test.ts` L111–137 — registered actions and `action:button` rows, not inline actions. - `packages/spec/src/ui/action-cancel-text.test.ts` L29, `action-copy-template.test.ts` L158, `action-row-update.test.ts` L291–295 — registered actions, a roots helper, and a registered action. The files changed between `a6441636` and `b3d14150` that carry both keys are registered-action metadata (`platform-objects` identity objects, `plugin-approvals`), generated translation bundles, and ui/spec tests none of which parse an inline action with the pair. Cloud fixtures are not in this repository and were not read. **Disposition unchanged: 0 shipped carriers, 1 spec test pin.** ## Tests - `action-confirm-params-guard.test.ts`: the "out of reach" inline pin is removed from the boundary block (renamed "the pair guard is scoped by SCHEMA BOUNDARY — the bulk def keeps the pair") and replaced by a new block, "`confirmText` + non-empty `params` is refused on InlineActionSchema too": the refusal (path, `code`, message equal to `ActionSchema`'s), the locale-map form refused, and three accept controls: `description` with no `confirmText`, `confirmText` with no `params`, `confirmText` with `params: []`. The file header names the inline door. <details><summary>Readings (head `65665759`, under os-verify-lock)</summary> - Targeted: `vitest run src/ui/action-confirm-params-guard.test.ts src/ui/inline-action.test.ts src/ui/action-cancel-text.test.ts src/ui/action-row-update.test.ts src/ui/component.test.ts`: `Test Files 5 passed (5)`, `Tests 459 passed (459)`. - `pnpm --filter @objectstack/spec test && pnpm --filter @objectstack/spec typecheck`: `Test Files 804 passed (804)`, `Tests 23884 passed | 1 todo (23885)`; VERDICT command-exit 0. - Ablation, through `scripts/ablation-replace.mjs` (anchor hit 1 → 0, blob `c1a9cf11` → `2b9084b3`): the inline refine's predicate replaced by `true || …`; `action-confirm-params-guard.test.ts` → `Tests 2 failed | 14 passed (16)` (the two refusal tests); restored, blob equals HEAD, `git diff HEAD` empty. No `dist` in that path (the test imports `./action.zod`). - Lint consumers: `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-action-copy-templates.test.ts src/validate-component-props.test.ts src/validate-component-props-url-target.test.ts src/validate-component-props-row-on-success.test.ts` (after the dogfood closure build): `Test Files 4 passed (4)`, `Tests 207 passed (207)`; VERDICT command-exit 0. The copy-template rule (`validate-action-copy-templates`, its `INLINE_KEYS`) needs no change: it lists copy keys, and this refusal adds none. - `node ui/scripts/check-action-forward-parity.mjs`: exit 0 (`element:button owes 13, forwards 18`; no spec key moved, so nothing new is owed). </details> **Full dogfood battery** (narrowing arm), on head `65665759`, one locked command: `pnpm --workspace-concurrency=2 --filter '@objectstack/dogfood^...' build && pnpm --filter @objectstack/dogfood test` (no `OS_TEST_SHARD`, no path): `Test Files 404 passed | 3 skipped (407)`, `Tests 3326 passed | 28 skipped (3354)`, 0 failures; VERDICT command-exit 0 (44m19s held). ## Acceptance notes - `content/docs/protocol/objectui/actions.mdx` L444 (hand-written) still says "On a registered action, pairing it with a non-empty `params` is *refused*". It stays true but no longer names the inline door; outside this claim's file surface, so left for the docs owner. - `skills/objectstack-ui/rules/actions.md` L22 has the same "a registered action" phrasing. True, and it names the remedy; a one-word widening ("A registered or inline action") would be a `skills/**` (Tier H, token-ratcheted) edit, kept out of this Tier S PR. - The lint copy-template rule's `INLINE_KEYS` (`validate-action-copy-templates.ts`) lists copy keys, not pair rules, so it does not move. ## Verification `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 102 families on this tree (7 paths vs merge base `b3d14150`). <details><summary>102 derived gates: 102 exit 0</summary> - Serial pass on `65665759`: 100 exit 0, 2 exit 3 (built-output prerequisites: spec `check:skill-examples` needs the client SDK `dist`, `check:dual-build-cjs-loads` needs every package's `dist`). `check-plugin-teardown-shape --self-test` needed its pinned fixture commit on the shallow clone (`git fetch origin 621a487`); fetched before the pass, exit 0. - After the full `pnpm turbo run build --concurrency=2` (`Tasks: 118 successful, 118 total`, VERDICT command-exit 0), the 2 were rerun: both exit 0 (`283 prose examples type-check across 3 surface(s)`; the CJS load probe at floors 90/58/520/1). - `check:generated`, `check:authorable-surface`, `check:api-surface`, `check:docs`, `check:liveness`: exit 0. No authorable-surface, api-surface, liveness or dropped-refinements row moves: the refusal adds no key and no export, and `InlineActionSchema` already carried a dropped refinement at every position it is reached from. - `dispatch-gates --ran`: "102 derived famil(ies) accounted for — 102 run, 0 NOT-MEASURED". </details> `origin/main` gained 19 commits after the base (`b0ef25f5` at open); none touches `action.zod.ts` or the guard test, and two regenerate other lines of `content/docs/references/data/object.mdx`. A local `merge-tree` of head and `origin/main` is clean; not merged here, so the dogfood reading stays pinned to this head. `check-changeset-no-major` with this body as the event: exit 0 ("LEVEL AXIS: this PR declares clause-② `no (narrowing)` and metadata-protocol `narrowed`, and no package whose published source … it moves is graded `patch`"). `check-closing-keyword-parity`: the only closing target is `Fixes #29383`. CI families are read from this PR's check-runs. --- _Generated by [Claude Code](https://claude.ai/code/session_01WGufzfHVbsfKWji5VLrLbG)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WGufzfHVbsfKWji5VLrLbG --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c5ddbea commit 2c881fb

7 files changed

Lines changed: 146 additions & 42 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec)!: an inline action (`InlineActionSchema`, a page button's `properties.action`) refuses `confirmText` beside a non-empty `params`, as a registered action (`ActionSchema`) already does, instead of accepting a pair that opens two dialogs for one decision
6+
7+
The console runs an inline action through the registered action's runner: it awaits the confirm dialog, and only then opens the param dialog. So an inline action carrying both keys showed the user a confirm that already read as "the action ran" while nothing had been sent, then a second dialog. `ActionSchema` refused that pair; the inline door accepted it, because until the inline pick carried `description` there was nowhere to move the question. It carries `description` now, so the inline door refuses the pair with the same predicate and the same message as the registered door, at the `confirmText` path.
8+
9+
Clause-②: no (narrowing) — `InlineActionSchema` refuses `confirmText` beside a non-empty `params`, which it accepted before. Nothing is widened.
10+
11+
Metadata-protocol: narrowed (an inline action may no longer declare `confirmText` beside a non-empty `params`)
12+
13+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed or renamed: the inline door already picked both keys and keeps them, and this adds the pair refusal ActionSchema carries. There is no rewrite that keeps what the author meant without reading it (the confirm sentence may or may not suit the param dialog's description line), so the author decides, and the refusal names the remedy. The in-repo census at the base found no shipped inline action carrying the pair. -->
14+
15+
**BREAKING** accept-set narrowing at `InlineActionSchema` (`@objectstack/spec`), served where a page's `element:button` `properties.action` is judged: the component-props gate (`component-props-invalid`, `@objectstack/lint`) at every CLI command (`os validate`, `os compile`, the publish gate) and at the runtime `page` save door. `defineStack` does not parse a component's `properties` bag, so it does not refuse the pair.
16+
17+
- **Before.** `{ type: 'api', target: '/x', confirmText: 'Reject this request?', params: [{ name: 'reason', type: 'textarea' }] }` parsed clean, and the console showed a confirm dialog and then the param dialog.
18+
- **Now.** The same body is refused at `confirmText` with the registered door's message: the pair shows TWO dialogs for one decision; carry the question in `description` and drop `confirmText`.
19+
- **Unchanged.** `confirmText` on an inline action with no `params`, or beside `params: []`, parses as before; so does an inline action with `params` and `description`. A view's `bulkActionDefs` keep the pair, which renders one dialog by that schema's own contract.
20+
- **Who sees it.** An author whose page button declares an inline action with both keys. A stored page keeps loading and serving; its next save is refused, with the remedy in the finding.
21+
22+
**The remedy.** Move the confirm sentence to the inline action's `description`, which the param dialog draws under its title, and drop `confirmText`.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ const result = ApiMethod.parse(data);
495495
| **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. |
496496
| **variant** | `Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'link'>` | optional | Button visual variant for styling (primary = highlighted, danger = destructive, ghost = transparent) |
497497
| **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. |
498-
| **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. |
498+
| **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. 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. |
499499
| **successMessage** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object } \| string \| Record<string, string>` | optional | Success message shown after the action succeeds — 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 `result`, the payload the action's handler answered (`{{ result.email }}`; for an `api` or `script` action the server response), and `record`, the record the action ran against. Formatters come from the template engine's whitelist (`TEMPLATE_FORMATTERS`, `@objectstack/formula`); a hole holds no logic (no CEL, no arithmetic). Example: 'Invitation sent to `{{ result.email }}`'. 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 that declares `outcomeMessages`, the entry named by the response's `outcome` is shown instead, and this message is the fallback. |
500500
| **outcomeMessages** | `Record<string, string \| { dialect: 'template'; source?: string; ast?: any; meta?: object } \| string \| Record<string, string>>` | optional | Success copy per handler outcome, for type:'api' and type:'script' actions: keys are the snake_case `outcome` values the handler returns in its success payload (e.g. archived, already_archived), values the message shown for that outcome. Each message is 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 `result`, the payload the action's handler answered (`{{ result.email }}`), and `record`, the record the action ran against — the roots `successMessage` reads. Formatters come from the template engine's whitelist (`TEMPLATE_FORMATTERS`, `@objectstack/formula`); a hole holds no logic (no CEL, no arithmetic). Example: 'Invitation resent to `{{ result.email }}`'. 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. An outcome with no entry here falls back to `successMessage`, then to the default text. Not allowed beside `resultDialog` (which suppresses the success toast) or `operation: 'update'` (no handler, so no outcome). |
501501
| **errorMessage** | `string \| Record<string, string>` | optional | Error message to show when the action fails (overrides the raw error). |

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ const result = MetadataBulkResultSchema.parse(data);
339339
| **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. |
342-
| **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. |
342+
| **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. 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. |
343343
| **successMessage** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object } \| string \| Record<string, string>` | optional | Success message shown after the action succeeds — 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 `result`, the payload the action's handler answered (`{{ result.email }}`; for an `api` or `script` action the server response), and `record`, the record the action ran against. Formatters come from the template engine's whitelist (`TEMPLATE_FORMATTERS`, `@objectstack/formula`); a hole holds no logic (no CEL, no arithmetic). Example: 'Invitation sent to `{{ result.email }}`'. 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 that declares `outcomeMessages`, the entry named by the response's `outcome` is shown instead, and this message is the fallback. |
344344
| **outcomeMessages** | `Record<string, string \| { dialect: 'template'; source?: string; ast?: any; meta?: object } \| string \| Record<string, string>>` | optional | Success copy per handler outcome, for type:'api' and type:'script' actions: keys are the snake_case `outcome` values the handler returns in its success payload (e.g. archived, already_archived), values the message shown for that outcome. Each message is 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 `result`, the payload the action's handler answered (`{{ result.email }}`), and `record`, the record the action ran against — the roots `successMessage` reads. Formatters come from the template engine's whitelist (`TEMPLATE_FORMATTERS`, `@objectstack/formula`); a hole holds no logic (no CEL, no arithmetic). Example: 'Invitation resent to `{{ result.email }}`'. 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. An outcome with no entry here falls back to `successMessage`, then to the default text. Not allowed beside `resultDialog` (which suppresses the success toast) or `operation: 'update'` (no handler, so no outcome). |
345345
| **errorMessage** | `string \| Record<string, string>` | optional | Error message to show when the action fails (overrides the raw error). |

0 commit comments

Comments
 (0)