Skip to content

Commit 3cc4fe5

Browse files
feat(components): the four action:* blocks publish the spec keys their block path honours (objectui#11168 slice 1) (#11185)
Refs #11168 Clause-②: yes (narrowing) — breaking for an author who wrote the retired `action:group.name`, an object at `action:group.actions` or `action:menu.actions`, or `md` at `action:group.size`: each is now refused by the manifest, the intrinsics and the page validator, as the spec already refuses it. The widening beside it is the spec keys the four blocks now publish as inputs. objectui's fixed group ships the break as `minor`, stated in the changeset. The PR waits as a draft for the director seat's contract review. Slice 1 of objectui#11168: the four `action:*` blocks. Every guard row this card owns on these blocks was measured through the real `SchemaRenderer`, the renderer's own forward and the real `ActionRunner`, against the installed `@objectstack/spec` 17.5.0 rows, and decided under objectui#11111 decision 3 = B (record `5902351047`): declare what the block path honours, refuse or retire what it does not. The PR stays a draft for the director seat's contract review. ## What moved - **Declared** (published as `inputs`): on `action:button` and `action:icon`, `visible`, `disabled`, `params`, `description`, `openIn`, `method`, `bodyExtra`, `bodyShape`, `operation`, `patch`, `confirmText`, `successMessage`, `errorMessage`, `refreshAfter`, `locations`, `toast`, `resultDialog`, `onSuccess`, `objectName`, plus `recordIdField` on `action:button`; on `action:group`, `location` and `visible`; on `action:menu`, `size` and `visible`. - **Retired**: `action:group.name`. - **Narrowed** (refused arms): `action:group.actions` and `action:menu.actions` from the `object` kind to a list of objects (`type: 'array'`, `of: 'object'`); `action:group.size` from `sm`/`md`/`lg` to `default`/`sm`/`lg`/`icon`. - **Member pins**: `action:group.actions`, `action:menu.actions`, and the ten array/object-armed keys each leaf block now publishes. - **Held unpublished**, still booked to this card with the measurement below: `endpoint` on `action:button` and `action:icon`, `undoable` on `action:button`. The renderers keep forwarding both, so nothing changes at runtime. - `public-block-binding-reach.test.tsx` derives its population from public blocks that publish `objectName`. The two action leaves now do, and on them the key names the object an action is dispatched against, not a data binding. Both are listed and ledgered in `NO_DATA_REACH` with that reason, which still asserts that the block fetches nothing under the name. - One changeset (`@object-ui/components`: minor) and one paragraph in `content/docs/guide/layout.md`. ## Per-key verdicts Read sites are cited by symbol and quoted expression. "Pin" names the row in `packages/components/src/renderers/action/__tests__/action-button-icon-inputs-11168.test.tsx` (leaf blocks) or `action-group-menu-inputs-11168.test.tsx` (containers). Every pin row carries a control in which the key is absent or holds the other value. ### `action:button` and `action:icon` (one row per key, both blocks unless noted) | Key | Read site on the block path | Verdict | Evidence | |---|---|---|---| | `visible` | `ActionButtonRenderer` / `ActionIconRenderer`: `useCondition(toPredicateInput(schema.visible), recordData …)` then `hasDeclaredVisibilityGate(schema.visible) && !isVisible`; and the `SchemaRenderer` node gate `evaluateVisibilityPredicate(newSchema.visible, 'visible')` | declared, `boolean`/`string`/`object` | holds/fails pair for a boolean, a bare CEL string and the envelope | | `disabled` | `useCondition(toPredicateInput(schema.disabled), recordData)` feeding `disabled={hostDisabled \|\| (hasDeclaredVisibilityGate(schema.disabled) ? … )}` | declared, `boolean`/`string`/`object` | holds/fails pair per arm, control on screen in both rows | | `params` | forward `Array.isArray(schema.params) ? { actionParams: schema.params … }`; `ActionRunner.execute` hands the list to `this.paramCollectionHandler(paramDefs, action)` | declared, `array` (the list to collect; static values stay on `properties.params`, and a node-level object is ignored under objectui#10289 ruling A) | the collection handler gets the list whole and in order; a collected value reaches the executor under its member `name` | | `description` | forward `description: (schema as any).description`; the console `paramCollectionHandler` reads `action?.description`; on `action:icon` also the tooltip `schema.label \|\| schema.description` | declared, `string` | the collection handler's action carries it; control `undefined` | | `openIn` | `executeUrl`: `const openIn = source.openIn ?? action.openIn` | declared, enum `self`/`new-tab` | `new-tab` gives `newTab: true`; control navigates in place | | `endpoint` | `executeAPI`: `action.api \|\| action.endpoint \|\| action.target`; the console `apiHandler` reads `const target = action.target \|\| action.name` and never `endpoint` | **held** | probe: on a runner with no `api` handler the request goes to the endpoint; the census below finds no console read | | `method` | `executeAPI`: `method = action.method \|\| 'POST'`; console `(action.method \|\| 'POST').toUpperCase()` | declared, `string` | request verb `PUT`; control `POST` | | `bodyExtra` | `buildApiRequestBody`: `const bodyExtra = asRecord(action.bodyExtra)`; console `Object.assign(body, …)` | declared, `object` | merged last over a static value of the same name; control without it | | `bodyShape` | `buildApiRequestBody`: `const shape = action.bodyShape`, then `{ [wrap]: base ?? {}, ...bodyExtra }` | declared, `enum` (`flat`) or `object` | `{ wrap: 'data' }` nests; `flat` and absent send the same flat body | | `operation` | `isUpdateOperationAction`: `action.operation === 'update'`, then `executeUpdateOperation` | declared, enum `update` | `patch` reaches the `script` dispatch; control without `operation` merges nothing | | `patch` | `executeUpdateOperation`: `const params = { ...patch, ...collected }` | declared, `object` | merged under supplied values; a supplied value of the same name wins | | `confirmText` | `ActionRunner.execute`: `if (action.confirmText)` then the confirm handler | declared, `string` | asked first, a refusal stops the action; control asks nothing | | `successMessage` | `handlePostExecution`: `dyn \|\| action.successMessage \|\| …` | declared, `string` | success toast text; control is the runner's default | | `errorMessage` | `handlePostExecution`: `action.errorMessage \|\| result.error` | declared, `string` | replaces the raw error; control shows the raw error | | `refreshAfter` | `handlePostExecution`: `if (action.refreshAfter && result.success) result.reload = true` | declared, `boolean` | the provider's result carries `reload: true`; control falsy | | `undoable` (button) | `executeUpdateOperation`: `if (action.undoable && rowRecord && writtenFields.length > 0)`; console runtime `action.undoable && obj && recId && rowRecord`; `RecordDetailView` `apiHandler` `action.undoable && isThisRecord && pageRecord` | **held** | probe: the def the block forwards gets `undo: null`; the same def plus a host `_rowRecord` stash gets an undo | | `recordIdField` (button) | `resolveServerActionRecordId`: `recordId = selected[0]?.[recordIdField]` | declared, `string` | with one selected row the POST carries its `code`; control sends `id` | | `locations` | `resolveServerActionRecordId` and the console `flowHandler`: `isRecordScopedAction(action)` | declared, `array` of `string` | a record-scoped list refuses to run with no record in scope (error toast, no request); control `list_toolbar` runs | | `toast` | `handlePostExecution`: `const showToast = action.toast ?? { … }` and `action.toast?.duration` | declared, `object` | `showOnSuccess: false`, `showOnError: false` and `duration` each asserted on the toast handler | | `resultDialog` | `handlePostExecution`: `this.resultDialogHandler(action.resultDialog!, result.data, action)` | declared, `object` | the dialog handler gets the spec whole with the data, and no success toast | | `onSuccess` | `handlePostExecution`: `readOnSuccessNavigation(action.onSuccess)`, then `navigateOnSuccess` | declared, `object` | `navigate` interpolated from the result, `openIn: newTab` opens a tab; control navigates nowhere | | `objectName` | `createServerActionHandler`: `action.objectName \|\| resolveObject?.(action, context) \|\| 'global'`; console handlers `action.objectName \|\| objApiName` | declared, `string` | the route becomes `/api/v1/actions/contact/…`; control uses the page object | `undoable` and `recordIdField` are not on `action:icon`'s spec row and its renderer does not forward them; the delivery probe shows `undefined` for both there, which is the probe's own lit zero. ### `action:group` | Key | Read site | Verdict | Evidence | |---|---|---|---| | `location` | `ActionGroupRenderer`: `declaredActions.filter(a => actionRendersAt(a, schema.location))` | declared, enum (`ACTION_LOCATIONS`) | only the matching member renders; control renders both | | `visible` | `useCondition(toPredicateInput(schema.visible), recordData)` and `if (schema.visible && !isVisible) return null`; the `SchemaRenderer` node gate | declared, `boolean`/`string`/`object` | holds/fails pair per arm | | `name` | none: `schema.name` is never read; inline mode only spreads leftover props onto the wrapping div element | **retired** | spec refuses it by name (`unrecognized_keys` naming `name`); the same members render identically with and without it, in both display modes | | `actions` (arm) | `const declaredActions: UIActionSchema[] = schema.actions \|\| []`, then `.filter` and `.map` | **narrowed**: `object` to `array` of `object` | spec refuses the object kind (`invalid_type` at `actions`) and accepts a list; member pin | | `size` (arm) | dropdown `schema.size === 'md' ? 'default' : (schema.size \|\| 'default')`; inline `action.size === 'md' ? 'default' : (action.size \|\| size \|\| 'sm')` | **narrowed** to `default`/`sm`/`lg`/`icon` | spec refuses `md` (`invalid_value` at `size`); sizes asserted on inline members and the trigger | ### `action:menu` | Key | Read site | Verdict | Evidence | |---|---|---|---| | `size` | `ActionMenuRenderer`: `const size = schema.size \|\| 'icon'` | declared, enum `default`/`sm`/`lg`/`icon` | trigger `lg`; control icon-sized | | `visible` | `useCondition(toPredicateInput(schema.visible), recordData, { throwOnError: true, … })` and `if (schema.visible && !isVisible) return null` | declared, `boolean`/`string`/`object` | holds/fails pair per arm | | `actions` (arm) | `const actions: UIActionSchema[] = schema.actions \|\| []`, then `.map` | **narrowed**: `object` to `array` of `object` | spec refuses the object kind and accepts a list; member pin | ## Ledger arithmetic (`registry-inputs-spec-parity.test.ts`) | `OBJECTUI_11111_LEDGER_CAPS` | before | struck here | after | |---|---|---|---| | `unjudgedBlocks` | 4 | 0 | 4 (the four lazy blocks, later slices) | | `offSpecInputs` | 1 | 1 (`action:group.name`) | 0 | | `unpublishedKeys` | 57 | 43 (button 20, icon 19, group 2, menu 2) | 14 (this card 3, objectui#8652 2, objectui#8649 9) | | `refusedArms` | 5 | 3 (the three `action:*` arms) | 2 (`element:definition-list`, `object-form`) | | `memberPins` | 6 | 2 (`action:group.actions`, `action:menu.actions`) | 4 (the `element:*` bookings) | - Owner-count pin: objectui#11168 62 to 13 (49 struck); objectui#8652 2 and objectui#8649 9 unchanged, and none of their rows is touched. - `MEMBER_PIN_EXEMPTION_CEILING` 7 to 5; `OFF_SPEC_EXEMPTIONS` is pinned empty again. - `MEMBER_PINS` gains 24 entries: the ten leaf keys on each of `action:button` and `action:icon` (one claim per key, written once and spelled per block) and `visible` / `actions` on each container. - objectui#8649 moves the same `unpublishedKeys` cap line and owner-count pin in parallel. Whichever PR lands second merges `main` and re-derives those two lines. ## Held keys Each still carries its booked entry (reason text now states the measurement), so the guard keeps it visible. - **`endpoint`** (`action:button`, `action:icon`). The block forwards it and the runner's built-in `api` executor reads it (probe: the request goes to the endpoint). The console registers its own `api` handler, and that handler reads `target`, never `endpoint`. On the console, an `api` action carrying `endpoint` therefore never calls it. The spec's own `ActionSchema` alias table already maps `endpoint` to `target`. Publishing it would advertise a key the product drops. - **`undoable`** (`action:button`). The block forwards it. The runner's `operation: update` path, which is the spec's declared form of an update action, offers Undo only with a `_rowRecord` stash that hosts write and this block never does (probe: `undo: null`; the same def plus a stash gives an undo). The console runtime reads it under the same guard. Only the record page's own `api` handler honours it, and only for a logical target on that page. The options and a recommendation are in the report on the card. ## Instruments - **Delivery probe** (one-time, not committed): each forwarded key on a node through the real `SchemaRenderer` inside a real `ActionProvider`, with the button clicked and the value read where it arrives. All 20 forwarded `action:button` keys arrived intact, and all 18 of `action:icon`'s. The probe's lit zero is icon's `undoable` and `recordIdField`, which arrive as `undefined`. `visible` and `disabled` are consumed by the renderer itself and are measured by the pin rows instead. - **Checker census**: `runtimeReadKeys` / `forwardedKeys` from `scripts/check-action-forward-parity.mjs` over its four runtime consumers, with `target` and `label` as lit controls and a nonsense key as the zero. `locations` read `-` on all four consumers. That is the census's documented blind spot (a read behind a helper): `isRecordScopedAction` in `serverActionHandler.ts` reads it, and the pin measures it directly. - **Pins**: `action-button-icon-inputs-11168.test.tsx` (47 tests) and `action-group-menu-inputs-11168.test.tsx` (20 tests). Every row mounts through `SchemaRenderer`. Where the runner hands a key to a host handler, the handler is the real `createServerActionHandler` from `@object-ui/core` (for `objectName`, `locations`, `recordIdField`), or a recording double. Refusal rows assert the spec issue's `code` and path, not a bare `success: false`. ## Tests (commands from the repo root; verdicts read from vitest's own summary) - Components suite, 341 files in three batches under the verify lock: `packages/components/src/renderers/ ui/ notifications/` 151 passed (1466 tests, 17 skipped); `src/__tests__` halves 94 passed plus 1 skipped, and 95 passed. All exit 0. - Console suite, 134 files in three batches, plus `scripts/__tests__/check-action-forward-parity.test.ts` and `packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx`: 45, 47 and 44 files passed. All exit 0. - At the final head `05d9cfdfa`: `registry-inputs-spec-parity`, `public-block-binding-reach`, everything under `packages/components/src/renderers/action/`, `action-group.test.tsx` and the forward-parity script tests: 33 files, 775 tests passed. - Type check: `@object-ui/components` `type-check` (`tsc --noEmit && tsc -p tsconfig.test.json`) and `@object-ui/console` `type-check` both exit 0, after building each dependency closure. `--listFilesOnly` shows both pin files in the components test project, and the guard and reach test in the console project. - Lint, declared narrowing: `eslint --format json` on the eight touched TypeScript files gives 0 errors. The warnings are pre-existing `no-explicit-any` and `only-export-components` on untouched lines; the diff adds no `any`. `eslint.config.js` sets no `parserOptions.project` or `projectService`, so linting is not type-aware and the diff cannot move a verdict on an untouched file. Whole-package `eslint .` is CI's. ## Ablations (each predicted before running; `ablation-replace` with anchor and blob proof, restore proven blob == HEAD with `git diff HEAD` empty) - **A. Unpublish `confirmText` on `action:button`.** Predicted: the published-input pin and the guard's reverse direction go red; the behaviour row stays green. Observed: exactly 2 red, `every declared key is a published input …` ("action:button does not publish confirmText") and `action:button publishes every top-level key its spec props schema declares`; 275 green. Anchor x1 to x0; blob `fce9b14a0a47` to `97074e6ff9e1`. - **B. Re-admit `md` on `action:group.size`.** Predicted: the size pin and the guard's arm direction go red. Observed: exactly 2 red, `` `size` publishes exactly the sizes the spec accepts … `` and `action:group declares no arm the spec refuses outright`; 248 green. Blob `26b31ec9a251` to `01c4266dad38`. - **C. Drop `confirmText` from `action:button`'s forward.** Predicted: only `action:button`'s behaviour row goes red, and `check:action-forward-parity` goes red. Observed: 1 of 47 red (`` `confirmText` is asked first … `` for `action:button`), and the gate reports "does not forward 1 key the runtime reads: `confirmText`". Blob `fce9b14a0a47` to `09421448fe69`. ## Gates (at `05d9cfdfa`, exit codes captured before any pipe) - Exit 0: `check-changeset-presence`, `check-changeset-no-major`, `check-changeset-fixed`, `check-changeset-overwrite`, `check-changeset-claims` (report-only), `check-pending-changeset-literals`, `check:control-bytes`, `check:new-line-citations` (0 new), `check:spec-symbols`, `check:action-forward-parity`, `check:doc-snippets` (679 of 679 blocks, against a built closure), `check:doc-types`, `check:prompt-keys`, `check:handler-key-reads`, `check:installed-pin-claims`, `check:test-path-roots`, `check:phantom-deps`, `check:unreferenced-sources`, `check:doc-example-readers`, `check:i18n-keys`, `check:element-data-source-declaration`, `check:esm-specifiers`, `check:side-effects-array`, `check:self-import`, `check:component-surface-parity` (report-only; no `inputs` finding on the four blocks). - `check-governed-queue-guard --test` on the ten changed paths: NOT GOVERNED. - Control-byte self-scan of every changed file: no hit. - NOT MEASURED: `check-spec-range-floors` (reason: it needs a whole-workspace build, and the diff touches no `package.json`, which is the gate's only input). `check:sdui-registration-pins` (reason: it needs a console bundle, and the diff touches no `sideEffects` array). Repo-wide `pnpm type-check`, `pnpm test` and `pnpm lint` are CI's. ## Acceptance notes (observations, not filed) - `scripts/check-action-forward-parity.mjs` says in its header that `locations` "is read only off the AUTHORED action … never off the forwarded def". `isRecordScopedAction` in `packages/core/src/actions/serverActionHandler.ts` and the console `flowHandler` read it off the forwarded def, and `RUNTIME_CONSUMERS` does not list that file. The gate is one-directional and every surface forwards the key, so this is dormant. Carrier: none. - The same gate's `JUSTIFIED` entries for `undoable` on `action:icon` / `action:group` / `action:menu` reason only from the console runtime's `rowRecord` guard. `RecordDetailView`'s `apiHandler` reads `action.undoable && isThisRecord && pageRecord` with no stash. Unmeasured beyond that reading. Carrier: none. - The `action-forward-parity.test.tsx` header calls `recordIdField` inert on these renderers. This slice measures it honoured on the `script` path with a single selection. Carrier: none. - `packages/types/src/zod/public-blocks.zod.ts` still says the `action:*` blocks have "no spec row yet"; 17.5.0 carries them. Carrier: objectui#10872. - `@object-ui/types` exports an `ActionGroup` interface whose required `name` is the key this slice retires. Nothing imports it. Carrier: none. ## Base merge `main` at `39e625de2` is merged into this branch as `80503f5bf`, a merge commit with no rebase and no force-push, after PR #11184 (objectui#8649) landed as `647908686`. The one conflict was in `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`, in three hunks. Every figure was re-derived by counting the merged ledger's booked entries per ledger and per owner. The same census first reproduced each parent's own pinned figures as a control. The ledger table above is the slice's arithmetic at `05d9cfdfa`; these are the figures at the merged head. - `OBJECTUI_11111_LEDGER_CAPS`: `unjudgedBlocks` 4, `offSpecInputs` 0, `unpublishedKeys` 5 (objectui#11168 3, objectui#8652 2, objectui#8649 0), `refusedArms` 2, `memberPins` 4. `MEMBER_PIN_EXEMPTION_CEILING` stays 5, because main did not touch that ledger. - Owner-count pin: objectui#11168 13, objectui#8652 2, objectui#8649 0. objectui#8649 stays in `OBJECTUI_11111_OWNERS` at 0. That is the convention its own landing used, and the file's header leaves removing the scaffold to the last owner card to land. - The `UNPUBLISHED_EXEMPTIONS` booking docblock takes main's wording as landed. Both PRs' struck rows stay struck. The net diff against `main` is still the same ten files. | Run at `80503f5bf` | Exit | Verdict | |---|---|---| | guard, `public-block-binding-reach`, `packages/components/src/renderers/action/` (both `-11168` pins), `action-group.test.tsx`, forward-parity script tests | 0 | 33 files, 775 tests passed | | `@object-ui/components` `type-check` | 0 | `tsc --noEmit && tsc -p tsconfig.test.json` | | `@object-ui/console` `type-check` | 0 | `tsc --noEmit && tsc -b tsconfig.node.json --force` | | `check:action-forward-parity` | 0 | 5 surfaces, 41 runtime-read keys, 22 justified omissions, 0 known gaps | | `check-changeset-presence`, `-no-major`, `-fixed`, `-overwrite`, `-claims` (report-only) | 0 each | 1 changeset declared for 8 source files of 2 released packages | | `check:control-bytes` | 0 | OK | | `check:new-line-citations` | 0 | 0 new | Ablation: `unpublishedKeys` set back to 14, this branch's pre-merge figure. Predicted and observed: exactly 1 red of the guard's 230 tests, `the objectui#11111 ledger is capped at exactly the entries it lists` ("expected 5 to be 14"). Anchor x1 to x0, blob `b26db645d11a` to `f60e7157b89e`; restored to blob == HEAD with `git diff HEAD` empty. The head moved, so a new contract-review record is owed before the queue. Session `https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm` --- _Generated by [Claude Code](https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9419df1 commit 3cc4fe5

10 files changed

Lines changed: 1384 additions & 82 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
'@object-ui/components': minor
3+
---
4+
5+
The four `action:*` blocks now publish the `@objectstack/spec` keys their
6+
renderers honour, and stop publishing what the spec refuses (objectui#11168,
7+
slice 1). Each key was decided by measuring it through `SchemaRenderer` and the
8+
action runner, per the objectui#11111 ruling: declare what the block path
9+
honours, refuse or retire what it does not.
10+
11+
Newly published, so the SDUI manifest, the JSX intrinsics and the page
12+
validator accept them instead of reporting `unknown-prop`:
13+
14+
- `action:button` and `action:icon`: `visible`, `disabled`, `params`,
15+
`description`, `openIn`, `method`, `bodyExtra`, `bodyShape`, `operation`,
16+
`patch`, `confirmText`, `successMessage`, `errorMessage`, `refreshAfter`,
17+
`locations`, `toast`, `resultDialog`, `onSuccess` and `objectName`; and
18+
`recordIdField` on `action:button`. `params` is published as the list of
19+
parameters to collect; static execution values stay under
20+
`properties.params`.
21+
- `action:group`: `location` and `visible`.
22+
- `action:menu`: `size` and `visible`.
23+
24+
Retired and narrowed (breaking for an author who wrote them, which the spec
25+
already refuses):
26+
27+
- `action:group` no longer publishes `name`. Nothing reads a group-level
28+
`name`; each member action's own `name` identifies it.
29+
- `actions` on `action:group` and `action:menu` is published as a list of action
30+
objects, not an `object`. Both blocks read the key as a list, so an object
31+
there could not render.
32+
- `action:group.size` publishes `default`, `sm`, `lg` and `icon`, and no longer
33+
`md`. A stored `md` still renders as before.
34+
35+
Still unpublished, pending a decision on objectui#11168: `endpoint` on
36+
`action:button` and `action:icon`, and `undoable` on `action:button`. The
37+
renderers keep forwarding both, so nothing changes at runtime.

‎apps/console/src/__tests__/public-block-binding-reach.test.tsx‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,19 @@ const NO_DATA_REACH: Readonly<Record<string, string>> = {
117117
// titles the panel.
118118
'record:related_list':
119119
'needs the parent record id from RecordContext before it may fetch; declines to fetch without one (objectstack#4413 ledger)',
120+
// objectui#11168 slice 1 published the spec key `objectName` on the two
121+
// action leaves, and on these blocks the key is not a data binding at all:
122+
// it names the object the ACTION acts on. The block draws a button and
123+
// fetches nothing; the key takes effect when the action is dispatched, where
124+
// the server-action route becomes `/api/v1/actions/{objectName}/{action}`
125+
// instead of the page's object. That dispatch read is pinned in
126+
// `packages/components/src/renderers/action/__tests__/action-button-icon-inputs-11168.test.tsx`;
127+
// what this ledger asserts is the other half — that the block still asks the
128+
// data layer for NOTHING under that name.
129+
'action:button':
130+
'objectName names the object the action is dispatched against (the server-action route), not a data binding; the block renders a button and fetches nothing (objectui#11168)',
131+
'action:icon':
132+
'objectName names the object the action is dispatched against (the server-action route), not a data binding; the block renders an icon button and fetches nothing (objectui#11168)',
120133

121134
// `list-view` and `embeddable-form` were the other two entries here for
122135
// exactly one release of this file. Neither registration bridged the
@@ -178,6 +191,11 @@ const EXPECTED_CANDIDATES = [
178191
// an `objectName`, so it joins this derived population — additive, and the
179192
// probe below is what decides whether the binding actually reaches.
180193
'object-tree',
194+
// objectui#11168 slice 1 published `objectName` on the two action leaves, so
195+
// they join the population too; both are ledgered in NO_DATA_REACH, because
196+
// on an action block the key is a dispatch target rather than a binding.
197+
'action:button',
198+
'action:icon',
181199
];
182200

183201
/**

‎apps/console/src/__tests__/registry-inputs-spec-parity.test.ts‎

Lines changed: 113 additions & 71 deletions
Large diffs are not rendered by default.

‎content/docs/guide/layout.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,15 @@ The handler name goes in `actionType`, which `action:button` forwards to the act
585585
runner as the action's type; the runner dispatches to the handler registered under it.
586586
Same spelling as [Record Edit Modes](./record-edit-modes.md).
587587

588+
The rest of the action is authored on the same node. `action:button` and `action:icon`
589+
evaluate `visible` and `disabled` themselves (a boolean, a CEL string, or a
590+
`{ dialect, source }` envelope), and forward the action-definition keys to the runner:
591+
`params`, `confirmText`, `successMessage`, `errorMessage`, `toast`, `bodyExtra`,
592+
`bodyShape`, `operation` / `patch`, `onSuccess`, `objectName` and the others each
593+
registration's `inputs` lists, with a `description` per key. For an `api` action, write
594+
the request URL in `target`: `endpoint` is not published, because the console's `api`
595+
handler never reads it.
596+
588597
## Responsive Behavior
589598

590599
The shell has exactly **one** layout breakpoint, at **768px** — Tailwind's `md`, and

0 commit comments

Comments
 (0)