Skip to content

Commit d727348

Browse files
feat(lint,spec)!: refuse a holeless ${record.*} / ${result.*} action copy sentence, the last action-copy warning (#25989)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb1498XergPD9UgXyoeHSEDC)_ Fixes #25709 Clause-②: no (narrowing) Metadata-protocol: narrowed ## What changes The console's action runner no longer fills `${…}` in an action's copy: `ActionRunner.confirmMessage` and `composeSuccessMessage` in `ui/packages/core/src/actions/ActionRunner.ts` return a sentence with no `{{ }}` hole as written (objectui#13476, in the imported `ui/` tree on `main`). So the one action-copy shape the doors still kept a warning, a holeless `${record.*}` `confirmText` or `${result.*}` `successMessage`, is now an error at `objectstack validate`, `compile` and the publish gate, like every other `${…}` token. - `packages/lint/src/validate-action-copy-templates.ts`: `isTransitionalDollar`, its warning branch and the `TRANSITIONAL` suffix are deleted. Every `closedScopeTemplateRefusal` finding is now `refuse(...)`. - `packages/spec/src/ui/action-copy-template.ts`: `ACTION_COPY_DOLLAR_TRANSITION_ROOTS` is retired (and its export in `ui/index.ts`). The module note and the `confirmText` / `successMessage` describes no longer say the shape is a warning; the stale `.objectui-sha` wording now names the `ui/` tree. - `packages/spec/src/automation/flow-text-slot-template.ts`: `dollarOnlyTokenRoots` is retired; the lint branch was its only reader. - Stale wording fixed: `packages/lint/src/authoring-rules.ts`, `validate-expressions.ts` (two comments), the conformance ledger rows `template-action-confirm-text` / `template-action-success-message`. - Generated: `api-surface/ui.json`, `api-surface/automation.json`, `export-origins/ui.json`, `export-origins/automation.json`, and the reference pages the describes feed (`content/docs/references/ui/action.mdx`, `ui/component.mdx`, `data/object.mdx`, `kernel/metadata-plugin.mdx`), all through `check:generated --fix`. Landing point: as the order expected, `packages/spec/src/ui/action-copy-template.ts` and `packages/lint/src/validate-action-copy-templates.ts`. No producer carries the shape (below), so nothing moves on the producer side. The two retired exports are not in `@objectstack/spec@18.1.0` (the latest published version; read from its npm tarball: 0 hits for either name in `dist/`, against hits for `closedScopeTemplateRefusal`, the control). No published version carries them. ## Producer measurement (before the crossing) Every action-copy sentence reachable from each module's exports, walked on the base `38050ced` and judged through the built `@objectstack/lint`: | producer | confirmText | successMessage | errorMessage | with a `${…}` token | holeless `${…}` shape | lint copy findings | |---|---|---|---|---|---|---| | `examples/app-crm` | 0 | 2 | 2 | 0 | 0 | 0 | | `examples/app-todo` | 1 | 8 | 0 | 0 | 0 | 0 | | `examples/app-showcase` | 6 | 24 | 0 | 0 | 0 | 0 | | `examples/app-multi-package` | 0 | 0 | 0 | 0 | 0 | 0 | | `@objectstack/platform-objects` | 68 | 256 | 16 | 0 | 0 | n/a | | `@objectstack/plugin-approvals` | 0 | 9 | 0 | 0 | 0 | n/a | **Count: 0 sentences of the shape in `examples/**`**, and 0 in the platform packages. Control: a planted `Delete ${record.name}?` `confirmText` on each example stack draws the finding (a warning before this change). A text scan of `examples`, `packages/platform-objects`, `packages/plugins` and `packages/services` for a `confirmText` / `successMessage` line carrying `${` also answers 0. (The platform-objects counts include translation-map copies the deep walk reaches, so they are an upper bound on sentences, not a declaration count.) HotCRM was not re-measured here. ## The pending #25680 release note is corrected in this PR `.changeset/25680-action-copy-refusal.md` is still pending (not yet released). Two of its sentences become false with this change, and they would ship in the same release: "**Still a warning.** …" (the holeless shape) and "`@objectstack/spec` adds two exports: `ACTION_COPY_DOLLAR_TRANSITION_ROOTS` … `dollarOnlyTokenRoots`". This PR rewrites exactly those two sentences (plus "all but one" in the summary sentence) and adds its own changeset, `.changeset/25709-action-copy-holeless-dollar-refusal.md`. This is the DELIBERATE CORRECTION class `check-empty-changeset.mjs` names, so `Check Changeset` stays red on purpose (route 0 of its message). **Confirmation requested in writing on this PR** that the rewrite of the #25680 note is wanted. `skip-changeset` is not applied. ## Pins flipped (load-bearing) - `packages/lint/src/validate-action-copy-templates.test.ts`: the holeless `${result.email}` / `${record.name}` case now asserts `severity: 'error'`, its location, its `{{ }}` prescription and no "A warning, not a refusal"; the `action:group` / inline-action case asserts four errors; the publish-gate cases assert one error on the error channel and an empty advisory channel for a page write and an action write. - `packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts` (c): through the real `saveMetaItem`, the holeless `${record.*}` page save now answers `422 INVALID_METADATA` with one `expression-invalid` error prescribing `{{ record.name }}`, and nothing is stored. - Spec: the `ACTION_COPY_DOLLAR_TRANSITION_ROOTS` and `dollarOnlyTokenRoots` unit pins are deleted with their subjects; the export snapshot (`api-surface/`) pins their absence. - Repo-wide sweep for the same meaning ("still fills", "transitional", "A warning, not a refusal, for now", the two symbol names): every hit outside `ui/` and released text is in this diff. ## Evidence Final head `a3b0809d`. - **Ablation** (committed fix first; `scripts/ablation-replace.mjs` with a trap restore): the refusal branch was mutated back to a warning for a holeless `${…}` sentence. Lint unit file: 6 failed / 11 passed (the flipped pins). Rebuilt `@objectstack/lint`, `ablation-dist-preflight` confirmed the marker in 4 built files, then the protocol gate file: 1 failed / 210 passed (pin (c)). Restore: blob equals HEAD, `git diff HEAD` empty, rebuilt, `--absent` preflight clean. - Tests: `@objectstack/lint` 163 files / 7335 tests passed; `@objectstack/spec` 722 files / 21881 passed (1 todo); `protocol.runtime-authoring-gate.test.ts` 211/211; `expression-conformance.test.ts` 8/8 on `a3b0809d`. Typecheck: `@objectstack/lint`, `@objectstack/spec`, `@objectstack/metadata-protocol` exit 0. - Derived gates (`dispatch-gates.mjs --commands`, 113 families, `--ran` reconciliation: 113 derived, 113 run): green except `check-empty-changeset` (the declared correction above, exit 1); `check:type-check-debt` and `check:dual-build-cjs-loads` exited 3, PREREQUISITE NOT MET (no lock turn for the re-measure's closure build; unbuilt packages outside this closure): NOT MEASURED locally, CI's `Lint & Repo Gates` runs both. `check:skill-examples`, `check:dts-closure` and `check-plugin-teardown-shape --self-test` first read a dist mid-rebuild or a shallow clone and were re-run green. `check:doc-authoring` found a tracker id in the ledger string; fixed in `a3b0809d` and re-run green. `check-platform-checklist --changed-from origin/main`: OK, 0 anchors lost. `check:generated`: all 14 artifacts up to date. - **Dogfood battery** (owed by the `(narrowing)` arm): `pnpm --filter @objectstack/dogfood test` under the verify lock, launched at `5a6ed48a`; the ledger string edit of `a3b0809d` landed on disk 43 s into the run, so `expression-conformance.test.ts` was re-run on `a3b0809d` (8/8). VERDICT command-exit 1: Test Files 1 failed / 295 passed / 3 skipped; Tests 1 failed / 2522 passed / 18 skipped. The one failure is `test/authz-probe-blind-spot.test.ts` "packages/runtime/src/domains/i18n.ts — every positive control is still present in THAT file" (control `handleI18nRequest` expected 3, read 4). This diff touches nothing under `packages/runtime` or that test, so the failure does not come from it (inferred from the file list; the test was not re-run on the base `38050ced`). ## Acceptance notes - Contract review at CONTRACT_REVIEW_TIER is owed (narrowing). - `outcomeMessages` and `ActionSchema.errorMessage` stay outside the family, unchanged. --- _Generated by [Claude Code](https://claude.ai/code/session_0148U1UfHrMrKAyJbiSs1ZYr)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3518664 commit d727348

21 files changed

Lines changed: 115 additions & 178 deletions

‎.changeset/25680-action-copy-refusal.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Clause-②: no (narrowing: an action's copy sentence that is blank, holes an unb
99
Metadata-protocol: narrowed
1010
Ruling-ref: `6365736632` (on #24814, the refusal half); triage `6386348015`
1111

12-
An action's person-facing copy is an ADR-0032 `template` slot over the roots the console's runner binds (#24814): `confirmText` over `record` (and `params` beside static values), `successMessage` over `result` and `record`, `errorMessage` over none. That covers a page action row (`action:button`, `action:icon`, an `action:group` / `action:menu` member), a page component's inline `properties.action`, and a registered action (`stack.actions[]`, an object's `actions[]`). `validateStackExpressions`, the rule all three doors run, judges each sentence through `closedScopeTemplateRefusal`, then compiles it as a template. Its findings were warnings. They are now errors, all but one. This supersedes the warning-only behaviour #24814 described earlier in this release line; a refused finding no longer ends with "A warning, not a refusal".
12+
An action's person-facing copy is an ADR-0032 `template` slot over the roots the console's runner binds (#24814): `confirmText` over `record` (and `params` beside static values), `successMessage` over `result` and `record`, `errorMessage` over none. That covers a page action row (`action:button`, `action:icon`, an `action:group` / `action:menu` member), a page component's inline `properties.action`, and a registered action (`stack.actions[]`, an object's `actions[]`). `validateStackExpressions`, the rule all three doors run, judges each sentence through `closedScopeTemplateRefusal`, then compiles it as a template. Its findings were warnings. They are now errors. This supersedes the warning-only behaviour #24814 described earlier in this release line; a refused finding no longer ends with "A warning, not a refusal".
1313

1414
**BREAKING — what moves for consumers.**
1515

@@ -18,15 +18,15 @@ An action's person-facing copy is an ADR-0032 `template` slot over the roots the
1818

1919
**Fix (one line).** Write the hole the finding prescribes (`Delete ${record.name}?` → `Delete {{ record.name }}?`), name a root the key binds, double a single-brace token, move logic out of the hole, or remove a blank key so the runner's default shows.
2020

21-
**Still a warning.** A `${…}` token the console's runner still fills in its transitional reading: in a sentence with no `{{ }}` hole and no other token, `${record.*}` in a `confirmText` and `${result.*}` in a `successMessage` (`ACTION_COPY_DOLLAR_TRANSITION_ROOTS`, measured at objectui `ActionRunner` at the pinned `.objectui-sha`, which fills them there). It is named with its `{{ }}` spelling and ends "A warning, not a refusal, for now". Refusing it waits for the runner to retire that reading.
21+
**The holeless `${…}` sentence too.** `${record.*}` in a `confirmText` and `${result.*}` in a `successMessage`, in a sentence with no `{{ }}` hole and no other token, is refused as well: the console's runner shows it as written. Its own entry in this release says so (#25709).
2222

2323
**Unchanged.**
2424

2525
- The parse. Each copy key accepts what it accepted, a blank string included (the slot's blank-string arm stays). The boot load and the read path judge a stored row with this parse, so a stored page or action carrying a refused sentence keeps loading and serving until it is next saved, and that save is judged. Saving as a draft is still allowed; publishing the draft is judged.
2626
- `ActionSchema.errorMessage` and `outcomeMessages` are not in this family and draw nothing.
2727
- A sentence with no finding draws nothing at any door, in any form (the bare string, the `{ dialect: 'template', source }` envelope, an inline locale map).
2828
- `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still turns a refusal into a logged write.
29-
- No schema shape moves. `@objectstack/spec` adds two exports: `ACTION_COPY_DOLLAR_TRANSITION_ROOTS` (`@objectstack/spec/ui`) and `dollarOnlyTokenRoots` (`@objectstack/spec/automation`). The three copy describes now say the texts are refused.
29+
- No schema shape moves, and `@objectstack/spec` adds no export. The three copy describes now say the texts are refused.
3030
- Measured before crossing, through the built `@objectstack/lint` at `6021b5e837`: 0 in-repo producers carry such a sentence. `examples/app-crm`, `app-showcase` (23 registered `successMessage`s, 29 pages), `app-todo` (2 `confirmText`, 16 `successMessage`) and `app-multi-package`, `@objectstack/platform-objects` (12 `confirmText`, 48 `successMessage`) and `@objectstack/plugin-approvals` (9 `successMessage`) draw 0 findings, against 1 for a planted unbound hole. HotCRM at `a4b3560` declares 1 action `confirmText` and 19 action `successMessage`s; judged through the same built pass, all 20 draw 0 findings, against an error for a planted `{count}`.
3131

3232
<!-- adr-0087: not-required (no-migration-prescription) a refusal at `objectstack validate`, `compile` and the publish gate of an action copy sentence (`confirmText`, `successMessage`, a page row's `errorMessage`) that is blank, names an unbound root, carries a single-brace or raw-shown `${…}` token, or holds logic in a hole: no authorable key, spelling or stored shape moves, the parse is unchanged, and no stored row is read, rewritten or converted. A stored row whose sentence is refused keeps loading until it is next saved, and the repair is the author's rewrite of the sentence (which root a hole should name is the author's intent), which no ledger entry can derive. The other categories are closed on facts: the packages publish (not unpublished); no ADR-0087 id covers this verdict (not already-registered); and the change is a build and door verdict, not a declaration (not runtime-interface-only or type-surface-only). -->
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/lint': minor
3+
'@objectstack/spec': minor
4+
---
5+
6+
feat(lint)!: `objectstack validate`, `compile` and the publish gate refuse a holeless `${record.*}` `confirmText` and a holeless `${result.*}` `successMessage` on an action, the one action-copy finding that was still a warning (#25709)
7+
8+
Clause-②: no (narrowing: an action's `confirmText` / `successMessage` sentence with no `{{ }}` hole whose tokens are `${record.*}` / `${result.*}` is refused at `objectstack validate`, `compile` and the publish gate instead of saving with a warning)
9+
Metadata-protocol: narrowed
10+
11+
The console's action runner no longer fills `${…}` in an action's copy: a sentence with no `{{ }}` hole is shown as written, `${…}` included (objectui#13476, in the `ui/` tree). The doors kept one `${…}` shape a warning while the runner still filled it: `Delete ${record.name}?` in a `confirmText`, `Invitation sent to ${result.email}` in a `successMessage`. The person reading the dialog or toast now sees that token raw, so the doors refuse it like every other `${…}` token.
12+
13+
**BREAKING — what moves for consumers.**
14+
15+
- FROM: a warning at `objectstack validate` and `compile`, and an entry on the publish gate's `advisories` channel while the action, object or page saved. TO: an error. Both commands fail, and an active `action`, `object` or `page` write answers `422 INVALID_METADATA` with an `expression-invalid` issue located at the key. The item is not stored. The finding no longer ends with "A warning, not a refusal, for now".
16+
- `ACTION_COPY_DOLLAR_TRANSITION_ROOTS` (`@objectstack/spec/ui`) and `dollarOnlyTokenRoots` (`@objectstack/spec/automation`), which reached `main` after `@objectstack/spec@18.1.0` and were never published, are withdrawn before this release. The `confirmText` and `successMessage` describes no longer say the shape is a warning.
17+
18+
**Fix (one line).** Write the hole the finding prescribes: `Delete ${record.name}?` → `Delete {{ record.name }}?`, `Invitation sent to ${result.email}` → `Invitation sent to {{ result.email }}`.
19+
20+
**Unchanged.** The parse: each copy key accepts what it accepted, so a stored row with such a sentence keeps loading until its next save, and that save is judged. Saving as a draft is still allowed. `outcomeMessages` and `ActionSchema.errorMessage` are not in this family. `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still turns a refusal into a logged write.
21+
22+
**Measured before crossing**, by walking every action-copy sentence reachable from each module's exports on this branch's base `38050ced` and through the built `@objectstack/lint`: `examples/app-crm` (2 `successMessage`, 2 `errorMessage`), `app-todo` (1 `confirmText`, 8 `successMessage`), `app-showcase` (6 `confirmText`, 24 `successMessage`), `app-multi-package` (none), `@objectstack/platform-objects` and `@objectstack/plugin-approvals` carry 0 sentences with a `${…}` token and 0 of the holeless shape, and the four example stacks draw 0 copy findings. A planted `Delete ${record.name}?` `confirmText` on each example stack draws the finding.
23+
24+
<!-- adr-0087: not-required (no-migration-prescription) a refusal at `objectstack validate`, `compile` and the publish gate of an action copy sentence with no `{{ }}` hole whose tokens are `${record.*}` (in a `confirmText`) or `${result.*}` (in a `successMessage`), which saved with a warning: no authorable key, spelling or stored shape moves, the parse is unchanged, and no stored row is read, rewritten or converted. A stored row whose sentence is refused keeps loading until it is next saved, and the repair is the author's rewrite of the sentence (which root a hole should name is the author's intent), which no ledger entry can derive. The two dropped exports never shipped in a published version. The other categories are closed on facts: the packages publish (not unpublished); no ADR-0087 id covers this verdict (not already-registered); and the change is a build and door verdict, not a declaration (not runtime-interface-only or type-surface-only). -->

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -492,8 +492,8 @@ const result = ApiMethod.parse(data);
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`). |
493493
| **variant** | `Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'link'>` | optional | Button visual variant for styling (primary = highlighted, danger = destructive, ghost = transparent) |
494494
| **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. |
495-
| **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. A `${record.*}` token in a sentence with no `{{ }}` hole, which the console's runner still fills, is named as a warning instead. 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. |
496-
| **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. A `${result.*}` token in a sentence with no `{{ }}` hole, which the console's runner still fills, is named as a warning instead. 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. |
495+
| **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. |
496+
| **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. |
497497
| **outcomeMessages** | `Record<string, 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 may interpolate $`{result.*}`, the scope `onSuccess.navigate` declares. 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). |
498498
| **errorMessage** | `string \| Record<string, string>` | optional | Error message to show when the action fails (overrides the raw error). |
499499
| **refreshAfter** | `boolean` | optional (default: `false`) | Refresh view after execution |

0 commit comments

Comments
 (0)