You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit f273c9f
Browse filesBrowse the repository at this point in the historyBrowse files
fix(plugin-grid)!: a list's selection bar and row menu draw only actions the object declares (#30169) (#30387)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgMHYZ6ACwf4zMmVF6bRhu81)_
domain:ui#2 · os-bill · session_01Utp6Cb5R6JDdfyvtj3dPR2
Fixes#30169
Clause-②: no (narrowing) — the grid stops drawing a bare `bulkActions`
or legacy `rowActions` name that matches no action the object declares.
`packages/spec/src/ui/bulk-action.zod.ts` already says the member "names
an action the object declares", and lint `action-name-undefined`
(ADR-0049) refuses any other at authoring. Owes a `CONTRACT_REVIEW_TIER`
review before the queue.
Metadata-protocol: narrowed (an undeclared bare `bulkActions` /
`rowActions` name no longer draws)
Skills-docs: none owed — `skills/**` teaches no handler-only bulk or row
action name (searched `skills/` for `bulkActions` / `rowActions`
examples and runner-handler text: no hit); `content/docs/ui/**` already
names only declared actions.
## 维护者速读
-
改了什么:列表的选择栏和行菜单只画对象声明过的操作。视图里写了对象没声明的裸名字(拼错的,或被禁用模块扣下的操作),不再画按钮或菜单项,开发态在控制台打一行警告点名。
- 为什么改:HotCRM 在 18.3.0 上关掉营销模块后,勾选线索仍出现「Create
Campaign」幽灵按钮,点了只弹「不可执行」。按裁决 C,渲染器回到 spec 原文「指对象声明的操作」,不再容忍只靠前端接管的名字。
- 风险与回滚:只动
plugin-grid(两个解析函数、ObjectGrid、行操作列、新增一个警告模块)与其测试、demo、README,不动 spec
和服务端。若有仓外宿主靠前端接管一个未声明的名字,它的按钮会消失(仓内实测无此用法)。回滚即 revert 本 PR。
- 先看:`ui/packages/plugin-grid/src/ObjectGrid.tsx`(`effectiveBulkActions`
不再并入未声明名字、两次警告调用)→
`ui/packages/plugin-grid/src/undeclaredActionNames.ts` → 测试
`objectBulkActionDispatch.test.tsx` 新 describe「a bulk action a disabled
package withholds」。
- 两分钟验证:`pnpm --dir ui exec vitest run
packages/plugin-grid/src/__tests__/objectBulkActionDispatch.test.tsx`,预期全部通过;其中「withheld」用例在旧逻辑下转红(选择栏画出了被扣下的按钮)。
- 需要你做的:无,席位落地。
## What changes
`resolveBulkActions` and `resolveLegacyRowActions` kept a bare name that
matched no action in `objectDef.actions`, and `ObjectGrid` drew it by
humanized name and dispatched it to the runner by name, on the reasoning
that a host might have registered a runner handler under it. That is how
a list whose app has a package disabled kept a ghost button: the server
withholds the package's actions from the served object, so their names
match nothing, and the click toasted `Action "create_campaign" is not
executable`. Ruling 6651753071 (option C) retires the tolerance.
- **`resolveBulkActions` / `resolveLegacyRowActions`.** A name with no
declared match produces no def. It comes back as `undeclared` (renamed
from `unresolved`; neither function is a package export), used only for
the warning below. Both module headers now state the rule and its source
(the spec sentence and `action-name-undefined`), replacing the "that
path must keep working" paragraph.
- **`ObjectGrid`.** The selection bar's string list is now only the
canonical `'delete'` (author-declared or the implicit bulk delete),
unchanged in its routing (`onBulkDelete`, or the runner by name when no
`onBulkDelete` is wired). The row menu no longer receives a by-name
list: `objectGridActionsColumn.tsx` drops the `customRowActions` input,
so the Actions column and the kebab read only resolved defs plus the
canonical Edit / Delete.
- **`undeclaredActionNames.ts` (new).** `describeUndeclaredActionNames`
builds one message per fold naming the key, the block address and every
dropped name; `useUndeclaredActionNamesWarning` logs it once per
distinct list, only when `NODE_ENV` is not `production`, and only once
the object's schema has loaded (or the grid names no object), so a list
still loading its object is never reported. `ObjectGrid` calls it twice,
for `bulkActions` and `rowActions`, in hook order before the early
returns.
- **Demo / README.** `demo/bulk-actions.tsx` swaps `legacy_only_handler`
(a handler-only name) for `not_declared`, drops the handler, and says
the bar draws no button for it. `README.md` replaces `bulkActions:
['delete', 'export']` and `rowActions: ['view', …]` (`export` and `view`
are handler-only) with a declared `deactivate`, and states the rule
under *Row Actions*.
Unchanged: a name that matches a declared action is promoted exactly as
before (label, icon, params, `visible`, `requiredPermissions`);
`bulkActionDefs` are untouched; `delete` keeps its own path
(`keptDeleteAction` / `onBulkDelete`).
**Consumers measured** (this tree, every non-test `bulkActions` /
`batchActions` / `rowActions` list under `examples/`, `ui/apps/`,
`ui/packages/`, `packages/`): `examples/app-showcase` (`task.view.ts`,
`field-zoo.view.ts`) names only actions declared in
`src/ui/actions/index.ts`; `packages/lint` fixtures mirror the showcase.
The one handler-only name was the plugin-grid demo, changed here. The
Console's runner handlers (`useObjectActions.ts`: `create`, `delete`,
`navigate`, `refresh`; `AppContent.tsx`: `crud_success`,
`dialog_cancel`, `navigate_create`, `navigate_edit`) are action TYPES
for other surfaces, not bulk or row names. No fallback is kept for an
out-of-repo host; its remedy is in the changeset's FROM → TO.
## Tests
Pins of the old tolerance, each flipped to the new behaviour with the
trigger named in the test:
- `bulkActionMembers-8071.test.tsx` row 3: `bulkActions: ['approve',
'no_such_action']` draws exactly `bulk-action-approve`, no "No Such
Action", and one `ObjectGrid bulkActions` warning naming
`'no_such_action'`.
- `objectBulkActionDispatch.test.tsx` ("keeps an unresolvable name
alongside the promoted defs" → "draws no button for a name the object
does not declare, even with a runner handler under it"): a handler
registered under `crm_only_handler` neither draws the button nor runs.
- `legacyRowActionDispatch.test.tsx` ("leaves a name with no declared
action to the by-name handler path" → "draws no entry … and warns once
naming it"): the declared `convert_lead` entry draws, `crm_only_handler`
does not, one `ObjectGrid rowActions` warning.
- `resolveBulkActions.test.ts`, `resolveLegacyRowActions.test.ts`: the
by-name cases flip to "no def, reported as undeclared"; a new row pins
that a repeated undeclared row name is reported once.
Trigger test (new top-level describe in
`objectBulkActionDispatch.test.tsx`, the shared harness): the view names
`push_down` and `create_campaign`. **Withheld** (the served object lacks
`create_campaign`): no `bulk-action-create_campaign`, no "Create
Campaign", one warning naming it and not `push_down`. **Served**: the
button reads the action's own label "Add to Campaign", and no warning.
Fixtures that used an undeclared name as a stand-in, repaired (behaviour
under test unchanged):
- `bulkActionsSpecKey.test.tsx`, `rowCrudEffectiveOps.test.tsx`,
`gridPagerSelectionMembers-8071.test.tsx`,
`actionsColumnAndChips-12708.test.tsx`: the object now declares the
named action (`approve` / `reject`, `notify`, `approve`, `archive`).
- `bulkActionRefresh.test.tsx`: the by-name dispatch path these rows pin
(refresh and selection reset after a string action) is now reached by
the canonical `'delete'` with no `onBulkDelete`, the handler registered
under `delete`.
Runs (all `pnpm --dir ui exec vitest run --maxWorkers=2 …` under
`scripts/pm/os-verify-lock.sh`):
- Base `9afcd6a6`, the five resolver / dispatch files before any edit:
`Test Files 5 passed (5)`, `Tests 54 passed (54)` (the old pins green,
encoding the tolerance).
- Whole `packages/plugin-grid/` after the source change, before the test
changes: `Test Files 10 failed | 267 passed (277)`, `Tests 27 failed |
2350 passed (2377)`, the 27 being exactly the pins and fixtures listed
above.
- The 11 changed / harness files after: `Test Files 11 passed (11)`,
`Tests 124 passed (124)`.
- Consumers outside plugin-grid that assert bulk or row action test ids
or author those keys (app-shell 7, plugin-list 5, plugin-view 5,
plugin-detail 2, console 1, ui scripts 1) plus three more plugin-grid
harnesses: `Test Files 14 passed (14)`, `Tests 542 passed (542)` and
`Test Files 10 passed (10)`, `Tests 237 passed (237)`.
Ablation, two legs through `scripts/ablation-replace.mjs` (tests import
`src`, no build involved), each landing verified and restored to the
HEAD blob with `git diff HEAD` empty:
1. `effectiveBulkActions` re-includes the undeclared names (`?
keptDeleteAction` → `? [...undeclaredBulkActions,
...keptDeleteAction]`): `Tests 3 failed | 17 passed (20)` — 8071 row 3,
the withheld trigger, the handler-name row.
2. The warning's `console.warn` disabled: `Tests 3 failed | 24 passed
(27)` — 8071 row 3, the row-menu warning row, the withheld trigger.
Type-check: `pnpm exec turbo run type-check
--filter=@object-ui/plugin-grid --concurrency=2` (`tsc --noEmit && tsc
-p tsconfig.test.json`): `Tasks: 53 successful, 53 total`. Lint:
`eslint` on every touched plugin-grid file, 0 errors (warnings are
pre-existing `no-explicit-any` in the touched files; the new module has
none).
## Acceptance notes
- `ui/packages/plugin-list/src/ListView.tsx` draws its own bulk bar for
non-grid views (`currentView !== 'grid'`, the `bulk-actions-bar` region
mapping `permittedBulkActions`), and it renders every bare name by label
with no resolution against the object's actions. Under this ruling's
rule ("the selection bar draws only actions the object declares") that
bar still draws an undeclared or withheld name. Read-only inference, not
reproduced in a browser; outside this card's file surface, so not
touched.
- `ui/AGENTS.md` #2 (docs-driven) also names the matching page under
`content/docs/frontend/`; that tree is outside the claimed surface.
`content/docs/ui/actions.mdx` and `content/docs/ui/views.mdx` already
show only declared names.
## Verification
<details><summary>Derived gates (dispatch-gates --commands on the final
head 2f621a5): 36 run, all exit 0</summary>
- `node scripts/check-adr-0087-registration.mjs --base origin/main` ::
exit 0
- `node scripts/check-adr-0087-registration.mjs --self-test` :: exit 0
- `node scripts/check-changeset-no-major.mjs --base origin/main` :: exit
0
- `node scripts/check-closing-keyword-parity.mjs` :: exit 0
- `node scripts/check-closing-keyword-parity.mjs --self-test` :: exit 0
- `node scripts/check-comment-mask-corpus.mjs` :: exit 0
- `node scripts/check-empty-changeset.mjs --base origin/main` :: exit 0
- `node scripts/check-empty-changeset.mjs --self-test` :: exit 0
- `node scripts/check-sdui-manifest.mjs` :: exit 0
- `node scripts/check-sdui-manifest.mjs --self-test` :: exit 0
- `node scripts/pm/release-rehearsal-clone.mjs --self-test` :: exit 0
- `node scripts/release-pending-publish.mjs --self-test` :: exit 0
- `pnpm --dir ui exec vitest run scripts/` :: exit 0
- `pnpm --filter @objectstack/spec run check:duration-unit-keys` :: exit
0
- `pnpm check:changeset-gate-self-tests` :: exit 0
- `pnpm check:closed-ceremony-text` :: exit 0
- `pnpm check:cross-package-test-inputs` :: exit 0
- `pnpm check:dispatcher-error-vocabulary` :: exit 0
- `pnpm check:driver-memory-census` :: exit 0
- `pnpm check:engine-double-contract` :: exit 0
- `pnpm check:error-status-conformance` :: exit 0
- `pnpm check:gitlink-declared` :: exit 0
- `pnpm check:nul-bytes` :: exit 0
- `pnpm check:published-files` :: exit 0
- `pnpm check:query-options-erasure` :: exit 0
- `pnpm check:refd-timer-probe` :: exit 0
- `pnpm check:registry-log-declared` :: exit 0
- `pnpm check:rest-log-spy-declared` :: exit 0
- `pnpm check:skill-identifier-liveness` :: exit 0
- `pnpm check:test-source-alias` :: exit 0
- `pnpm check:tier-file-adoption` :: exit 0
- `pnpm check:type-check-coverage` :: exit 0
- `pnpm check:undeclared-dep-imports` :: exit 0
- `pnpm check:watch-hint-literal` :: exit 0
- `pnpm check:where-matcher` :: exit 0
- `pnpm check:type-check-debt` :: exit 0
`pnpm check:type-check-debt` was re-run alone under the verify lock
(`VERDICT command-exit 0`); its first run in the loop exited 3 queued
behind this card's own dogfood hold. `node scripts/pm/dispatch-gates.mjs
--ran`: `36 derived famil(ies) accounted for — 36 run, 0 NOT-MEASURED`.
</details>
<details><summary>Standing ui gates</summary>
- `node scripts/check-sdui-manifest.mjs` (and `--self-test`): in the
derived list above, exit 0.
- `pnpm --dir ui check:new-line-citations`: `VERDICT
new-cross-file-line-citations: 0 new citation(s), enforcement
report-only -> exit 0`.
</details>
Dogfood battery (final head, `(narrowing)` arm): `pnpm --filter
@objectstack/dogfood test` under the verify lock on `899d981b` (closure
built first): `Test Files 419 passed | 3 skipped (422)`, `Tests 3492
passed | 36 skipped (3528)`, `VERDICT command-exit 0`. The final head
`2f621a58` differs from `899d981b` only in the changeset's remedy
paragraph.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CZ1rxZPzain2p5bv5x)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(plugin-grid)!: a list's selection bar and row menu draw only actions the object declares; an undeclared bare `bulkActions` / `rowActions` name draws nothing and warns once in development (#30169)
6
+
7
+
Clause-②: no (narrowing) — the grid stops drawing a bare `bulkActions` or legacy `rowActions` name that matches no action the object declares, which it drew before by humanized name. `packages/spec/src/ui/bulk-action.zod.ts` already says the member "names an action the object declares", and lint `action-name-undefined` (ADR-0049) refuses any other at authoring. Nothing is widened.
8
+
Metadata-protocol: narrowed
9
+
Skills-docs: no move — `skills/**` teaches no handler-only bulk or row action name (searched for `bulkActions` / `rowActions` examples and runner-handler text); `content/docs/ui/**` already names only declared actions in its `bulkActions` / `rowActions` examples.
10
+
11
+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema, export or stored row changes shape: `bulkActions` and `rowActions` keep their `string[]` form and meaning. What narrows is the renderer's draw set, back to the spec's own sentence. Whether an undeclared name was a misspelling, an action still to declare or a handler the host meant to keep is a judgement about the view, so no rewrite derives the fix; the development warning names each name. -->
12
+
13
+
**BREAKING** for a host that registered a runner handler under a bare name the object does not declare (`ActionProvider handlers` / `runner.registerHandler('NAME', …)`) and listed that name in a view's `bulkActions` or `rowActions`.
14
+
15
+
-**Before.**`resolveBulkActions` and `resolveLegacyRowActions` kept a name that matched no declared action, and `ObjectGrid` drew it by humanized name and dispatched it to the runner by name. A name nothing handled toasted "not executable" on click. This is how a list whose app had a package disabled kept a ghost button: the server withholds that package's actions from the served object, so their names matched nothing.
16
+
-**Now.** Such a name gets no selection-bar button and no row-menu entry, and in development the console logs one warning per list naming the key, the block and each name. The resolvers return those names as `undeclared` (renamed from `unresolved`), used only for that warning. The canonical `'delete'` (and the row menu's `'edit'` / `'delete'`) keep their own paths, unchanged.
17
+
-**Unchanged.** A name that matches a declared action is promoted to that action's def exactly as before, under the action's own label, icon, params, `visible` and `requiredPermissions`.
18
+
19
+
**Remedy.** No rewrite is derived, because which fix an undeclared name needs is a judgement about the view: declare the named action on the object, or remove the name from `bulkActions` / `rowActions`. The development warning names each one. An action withheld by a disabled package needs nothing: it draws again when the package is enabled.
0 commit comments