Skip to content

Commit d3b2aaf

Browse files
fix(plugin-list)!: a non-grid view's selection bar draws only actions the object declares (#30394) (#30498)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuEHQyuPNhMoPYnP8JdznESK)_ domain:ui#5 · os-sales · session_01L6bhgVzvtWN68zb2r9z2zy Fixes #30394 Clause-②: yes (narrowing) — `@object-ui/plugin-grid` now exports `resolveBulkActions` and `useUndeclaredActionNamesWarning` (with the `ActionLabelResolver`, `NamedActionDef`, `UndeclaredActionNameKey`, `UndeclaredActionNamesAddress` and `UndeclaredActionNamesBasis` types), so `@object-ui/plugin-list` reuses the grid's fold and warning instead of a second copy. The warning's address gains an optional `component`, defaulting to `ObjectGrid`, so the grid's message is byte-identical. This PR both widens (those exports) and narrows (the non-grid bar no longer draws an undeclared bare name), so the arm is `(narrowing)`: seat patch round on the Check Changeset red, after claim amendment 6658495307 had written `(widening)`. Owes a `TIER_DEFAULT` review for the export widening and a `CONTRACT_REVIEW_TIER` review for the narrowing below. Metadata-protocol: narrowed (an undeclared bare `bulkActions` name no longer draws in a non-grid view, matching #30387 on the grid) Skills-docs: none owed — `skills/**` teaches no handler-only bulk action name (searched `skills/` for `bulkActions` examples); `content/docs/ui/**` already names only declared actions. ## 维护者速读 - 改了什么:看板、日历、画廊等非表格视图的选择栏,改成和表格同一条规则:视图 `bulkActions` 里的裸名字,只有对象声明了同名操作才画按钮,用声明的标签;删除照旧走自己的路径;没声明的名字不画,开发态控制台打一行警告点名。 - 为什么改:#30169 裁决 C 后表格已不画未声明的操作(PR #30387),但切到看板等视图,勾选记录后仍会出现幽灵按钮(比如被禁用模块扣下的「Create Campaign」),点了只弹「不可执行」。 - 风险与回滚:plugin-list 新增对 plugin-grid 的 peer 依赖(控制台 app-shell 本来就同时装两包);plugin-grid 入口多两个导出,警告消息多一个可选的组件名字段,表格输出不变。若仓外宿主靠前端接管一个未声明的名字,非表格视图里它的按钮会消失。回滚即 revert 本 PR。 - 先看:`ui/packages/plugin-list/src/ListView.tsx`(`nonGridBulkBar` 折叠与选择栏渲染)→ `ui/packages/plugin-grid/src/undeclaredActionNames.ts`(可选 `component`)→ `ui/packages/plugin-grid/src/index.tsx`(两个新导出)。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/plugin-list/src/__tests__/ListView.nonGridBulkDeclared-30394.test.tsx`,预期 4 条全过;去掉折叠(消融)时其中 3 条转红。 - 需要你做的:无,席位落地。 ## What changes ListView draws its own selection bar for every view but the grid (kanban, calendar, gallery, timeline, …). That bar mapped each `bulkActions` member straight to a button labelled by the humanized name, declared or not. #30169 (ruling C, comment 6651753071) made the grid draw only actions the object declares; this applies the same rule to the other bar, through the grid's own code. - **`ListView.tsx`.** A new `nonGridBulkBar` memo folds `permittedBulkActions` (the existing delete gate is untouched) through plugin-grid's `resolveBulkActions`, with the canonical `delete` held out of the fold exactly as `ObjectGrid` holds it out. The bar draws, in authored order, `delete` (its own path, gate and per-record `visibleWhen` subset, unchanged) and each name the served object declares, under the declared label resolved through the app's i18n bundle (the grid's `localizeLabel`). Any other name draws nothing. A bar with nothing left to draw is not rendered. `useUndeclaredActionNamesWarning` names the undrawn names once in development, only while a non-grid view is on screen (the grid warns for itself) and only once the object schema read has settled; with no schema in hand (no `getObjectSchema`, or the read failed) the names are reported as unread, never as undeclared, the grid's reading. - **`plugin-grid/src/undeclaredActionNames.ts`.** `UndeclaredActionNamesAddress` gains an optional `component` that heads the message; omitted, it is `ObjectGrid`, so every grid message is byte-identical. ListView passes `ListView` and the view on screen as `blockType`: `[ObjectUI] ListView bulkActions: kanban (objectName: 'lead') — 1 name matches no action the object declares, …`. - **`plugin-grid/src/index.tsx`.** Exports `resolveBulkActions`, `useUndeclaredActionNamesWarning` and the types their signatures name. - **`plugin-list/package.json`.** Declares `@object-ui/plugin-grid` as a peer (`workspace:^`) and dev (`workspace:*`) dependency, following its `@object-ui/plugin-detail` precedent. No cycle: plugin-grid does not depend on plugin-list. plugin-list's build externalizes `@object-ui/*`, so its dist does not bundle the grid. `pnpm-lock.yaml` moves by the three lines that declaration adds. - **Fixtures.** `ListView.bulkDeleteVisibleWhen.test.tsx` and `ListView.permissions.test.tsx` used `archive` as their non-delete control without the object declaring it; both fixtures now declare `{ name: 'archive', label: 'Archive' }`, so the control still draws (moved pins, assertions unchanged). ## Why Same defect as #30169, one more surface: after PR #30387 a list shown as a grid hides a name the object does not declare (for example an action a disabled package withholds), but the same list switched to kanban or gallery still drew a button for it, and pressing it toasted "not executable". The card asks for the grid's resolver to be reused rather than copied; plugin-list could not reach it (no dependency, not exported), measured and reported to the seat before plugin-grid was touched. ## Tests - New `ui/packages/plugin-list/src/__tests__/ListView.nonGridBulkDeclared-30394.test.tsx` (gallery view, selection stubbed the way `ListView.bulkDeleteVisibleWhen.test.tsx` does): a declared name draws under its declared label and dispatches by name, `delete` draws in authored order, an undeclared name draws nothing and is named in one `ListView` warning; all-undeclared draws no bar; no `getObjectSchema` reports the names as unread; all-declared warns nothing. - `ui/packages/plugin-grid/src/__tests__/undeclaredActionNames.test.ts`: the message heads with `ObjectGrid` by default and with the address's `component` when given. ## Acceptance notes - Importing the plugin-grid entry (`sideEffects: true`, it registers `object-grid` on import) did not disturb plugin-list's mocked registrations: the full plugin-list suite is green (below), which was the measured risk in the claim amendment's plan B. - The non-grid bar still dispatches through the host's `onBulkAction(name, rows)`; promoted defs' params, confirm text and `visible` are not consulted on this bar (they never were). Unchanged by this PR. ## Verification Heads `38d91dbe` and `4d680aaa` change only the changeset over `fbecf332`: the Clause-② arm (`(widening)` → `(narrowing)`), then the summary's `!`; on it `check-changeset-no-major` (fed this body as the event) exits 0 where the `(widening)` body exits 1 with the level-axis contradiction, and `check-adr-0087-registration`, `check-empty-changeset`, `check:changeset-fixed` exit 0 on `4d680aaa`. The `(narrowing)` arm owes the full dogfood battery on the final head; its reading goes to the seat. Everything below is on `fbecf332` unless stated; the build closure was `pnpm exec turbo run build --filter='@object-ui/plugin-list^...'` (53/53 tasks). <details><summary>Package suites and type-check</summary> - `pnpm --filter @object-ui/plugin-list test` on `85a03128` (the fix commit; `fbecf332` changes only the changeset marker and the new test's spy typing): `Test Files 190 passed (190)`, `Tests 1817 passed (1817)`, lock `VERDICT command-exit 0`. - `pnpm --filter @object-ui/plugin-grid test`: `Test Files 278 passed (278)`, `Tests 2389 passed (2389)`, `VERDICT command-exit 0`. - `pnpm exec vitest run packages/plugin-list/src/__tests__/ListView.nonGridBulkDeclared-30394.test.tsx`: `Tests 4 passed (4)`. - `pnpm --filter @object-ui/plugin-grid --filter @object-ui/plugin-list run type-check`: plugin-grid Done; plugin-list green after typing the new test's spy (`fbecf332`). - `pnpm --dir ui run check:spec-symbols` :: exit 0 (`1807 files scanned against 5743 spec export names; ... 0 untriaged collisions`). </details> <details><summary>Ablation (one, on the tested behaviour)</summary> `node scripts/ablation-replace.mjs --file ui/packages/plugin-list/src/ListView.tsx --anchor ".filter((a) => a.toLowerCase() === 'delete' || labels.has(a))" --replacement ".filter(() => true /* ABLATION-30394 */)" -- …vitest run …nonGridBulkDeclared-30394.test.tsx` - Mutation landed: anchor 1 → 0, blob `c5b3c47fb594` → `5ddccdf4efbb`. - Mutated run: `Tests 3 failed | 1 passed (4)`; the passing one is the all-declared control, which the mutation cannot move. - Restored: blob after restore `c5b3c47fb594` == HEAD, `git diff HEAD` empty. </details> <details><summary>Derived gates (dispatch-gates --commands, 57 families)</summary> - 57 derived, 57 run, 0 NOT-MEASURED (`--ran` with an exit code on every line: "a DERIVED zero"). - Two needed a second run: `node scripts/check-adr-0087-registration.mjs --base origin/main` :: exit 1 on `85a03128` (the `narrowed` kind requires a disposition), exit 0 on `fbecf332` after the changeset gained `not-required (no-migration-prescription)`; `pnpm check:dual-build-cjs-loads` :: exit 3 (PREREQUISITE NOT MET, dists missing) then exit 0 (`116 published require entry point(s) across 71 package(s) load`). - Changeset families re-run on `fbecf332`: `check-changeset-no-major` 0, `check-empty-changeset` 0, `check:changeset-fixed` 0, `check-closing-keyword-parity` 0. - Every other family exit 0. Families CI runs are read from this PR's check-runs. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` is CI's. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: n/a. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English, except the 维护者速读 section the fleet requires. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01L6bhgVzvtWN68zb2r9z2zy)_ <!-- signed: domain:ui#5 · os-sales · session_01L6bhgVzvtWN68zb2r9z2zy --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e3948ae commit d3b2aaf

10 files changed

Lines changed: 294 additions & 12 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@object-ui/plugin-list': minor
3+
'@object-ui/plugin-grid': minor
4+
---
5+
6+
fix(plugin-list)!: a list's selection bar over a non-grid view (kanban, calendar, gallery, …) draws only actions the object declares, by the grid's own rule; an undeclared bare `bulkActions` name draws nothing and warns once in development (#30394)
7+
8+
Clause-②: yes (narrowing) — `@object-ui/plugin-grid` exports `resolveBulkActions` and `useUndeclaredActionNamesWarning` (with the `ActionLabelResolver`, `NamedActionDef`, `UndeclaredActionNameKey`, `UndeclaredActionNamesAddress` and `UndeclaredActionNamesBasis` types) so `@object-ui/plugin-list` reuses the grid's fold and warning instead of a second copy; the warning's address gains an optional `component`, defaulting to `ObjectGrid`, so the grid's message is byte-identical.
9+
Metadata-protocol: narrowed
10+
Skills-docs: no move — `skills/**` teaches no handler-only bulk action name (searched for `bulkActions` examples); `content/docs/ui/**` already names only declared actions.
11+
12+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema, export or stored row changes shape: `bulkActions` keeps its `string[]` form and meaning, and plugin-grid's new exports add, never retire. What narrows is the non-grid bar's draw set, back to the spec's own sentence (a bare name names an action the object declares). Whether an undeclared name was a misspelling or an action still to declare is a judgement about the view, so no rewrite derives the fix; the development warning names each name. -->
13+
14+
- **Before.** The bar ListView draws itself for every view but the grid mapped each `bulkActions` member straight to a button, labelled by the humanized name, whether or not the object declared it. A name the object does not declare (a misspelling, or an action a disabled package withholds from the served object) showed a button that toasted "not executable" on click. The grid stopped drawing such names in #30169.
15+
- **Now.** The bar folds the names through the grid's `resolveBulkActions`: a name the object declares draws under that action's label (through the app's i18n bundle), the built-in `delete` keeps its own path and gate, and any other name draws nothing. In development the console logs one warning per list naming the key, the view and each name (`[ObjectUI] ListView bulkActions: kanban (objectName: …) — …`), the grid's own message. A bar with nothing left to draw is not shown.
16+
- **Remedy.** Declare the named action on the object, or remove the name from `bulkActions`. An action withheld by a disabled package needs nothing: it draws again when the package is enabled.

‎pnpm-lock.yaml‎

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎ui/packages/plugin-grid/src/__tests__/undeclaredActionNames.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ describe('describeUndeclaredActionNames', () => {
5959
expect(line).not.toContain('matches no action');
6060
});
6161

62+
it('heads the message with ObjectGrid unless the address names another component (objectstack#30394)', () => {
63+
const grid = describeUndeclaredActionNames('bulkActions', ['create_campaign'], ADDRESS);
64+
expect(grid?.startsWith('[ObjectUI] ObjectGrid bulkActions: object-grid (')).toBe(true);
65+
const list = describeUndeclaredActionNames('bulkActions', ['create_campaign'], {
66+
component: 'ListView', blockType: 'kanban', objectName: 'lead',
67+
});
68+
expect(list?.startsWith("[ObjectUI] ListView bulkActions: kanban (objectName: 'lead')")).toBe(true);
69+
expect(list).toContain('the selection bar draws no button');
70+
});
71+
6272
it('says nothing for no names', () => {
6373
expect(describeUndeclaredActionNames('bulkActions', [], ADDRESS)).toBeNull();
6474
});

‎ui/packages/plugin-grid/src/index.tsx‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ export { useGroupedData } from './useGroupedData';
3232
export { GroupRow } from './GroupRow';
3333
export { RowActionMenu, formatActionLabel } from './components/RowActionMenu';
3434
export { BulkActionBar } from './components/BulkActionBar';
35+
// The bare-name fold and its development warning, for the one other surface
36+
// that draws a selection bar of its own: plugin-list's ListView over a
37+
// non-grid view (objectstack#30394). One rule, one copy.
38+
export { resolveBulkActions } from './resolveBulkActions';
39+
export { useUndeclaredActionNamesWarning } from './undeclaredActionNames';
3540
export { useCellClipboard } from './useCellClipboard';
3641
export { useGradientColor } from './useGradientColor';
3742
export { useGroupReorder } from './useGroupReorder';
@@ -55,6 +60,12 @@ export type { GroupEntry, UseGroupedDataResult, AggregationType, GroupAggregatio
5560
export type { GroupRowProps } from './GroupRow';
5661
export type { RowActionMenuProps } from './components/RowActionMenu';
5762
export type { BulkActionBarProps } from './components/BulkActionBar';
63+
export type { ActionLabelResolver, NamedActionDef } from './resolveBulkActions';
64+
export type {
65+
UndeclaredActionNameKey,
66+
UndeclaredActionNamesAddress,
67+
UndeclaredActionNamesBasis,
68+
} from './undeclaredActionNames';
5869
export type { CellRange, UseCellClipboardOptions, UseCellClipboardResult } from './useCellClipboard';
5970
export type { GradientStop, UseGradientColorOptions } from './useGradientColor';
6071
export type { UseGroupReorderOptions, UseGroupReorderResult } from './useGroupReorder';

‎ui/packages/plugin-grid/src/undeclaredActionNames.ts‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,16 @@ export type UndeclaredActionNameKey = 'bulkActions' | 'rowActions';
4646

4747
/** Where the offending block lives, for the first line of the message. */
4848
export interface UndeclaredActionNamesAddress {
49-
/** The schema node's `type`: `object-grid`, or the `view:grid` alias. */
49+
/**
50+
* The component whose bar or menu left the names undrawn, for the message's
51+
* head. Omitted: `ObjectGrid`. `@object-ui/plugin-list`'s ListView passes
52+
* `ListView` for the selection bar it draws itself over a non-grid view.
53+
*/
54+
component?: unknown;
55+
/**
56+
* The schema node's `type`: `object-grid`, or the `view:grid` alias; for
57+
* ListView, the view on screen (`kanban`, `calendar`, …).
58+
*/
5059
blockType?: unknown;
5160
/** The object the grid queries, when it names one. */
5261
objectName?: unknown;
@@ -67,6 +76,10 @@ const SURFACE: Record<UndeclaredActionNameKey, string> = {
6776
rowActions: 'the row menu draws no entry',
6877
};
6978

79+
function describeComponent(component: unknown): string {
80+
return typeof component === 'string' && component.length > 0 ? component : 'ObjectGrid';
81+
}
82+
7083
function describeAddress({ blockType, objectName, label }: UndeclaredActionNamesAddress): string {
7184
const block = typeof blockType === 'string' && blockType.length > 0 ? blockType : 'object-grid';
7285
const parts: string[] = [];
@@ -92,13 +105,13 @@ export function describeUndeclaredActionNames(
92105
const rule = ` A bare \`${key}\` name must name an action declared on the object.`;
93106
if (basis === 'unread') {
94107
const count = names.length === 1 ? '1 name' : `${names.length} names`;
95-
return `[ObjectUI] ObjectGrid ${key}: ${describeAddress(address)} — the object's schema could not `
108+
return `[ObjectUI] ${describeComponent(address.component)} ${key}: ${describeAddress(address)} — the object's schema could not `
96109
+ `be read (no getObjectSchema on the data source, or the read failed), so its declared actions `
97110
+ `are unknown and ${SURFACE[key]} for ${count}: ${listed}.\n`
98111
+ `${rule} Give the grid a data source that answers getObjectSchema, or check why the read failed.`;
99112
}
100113
const count = names.length === 1 ? '1 name matches' : `${names.length} names match`;
101-
return `[ObjectUI] ObjectGrid ${key}: ${describeAddress(address)} — ${count} no action the object `
114+
return `[ObjectUI] ${describeComponent(address.component)} ${key}: ${describeAddress(address)} — ${count} no action the object `
102115
+ `declares, so ${SURFACE[key]} for ${them}: ${listed}.\n`
103116
+ `${rule} Declare the action on the `
104117
+ 'object, or remove the name. An action that a disabled package withholds is left out of the '
@@ -141,16 +154,17 @@ export function useUndeclaredActionNamesWarning(input: {
141154
// Keyed on the names' VALUES, never on the array the fold returns, which is
142155
// a fresh one on every render.
143156
const namesKey = ready ? JSON.stringify(input.names) : '';
144-
const { blockType, objectName, label } = input.address;
157+
const { component, blockType, objectName, label } = input.address;
145158
useEffect(() => {
146159
if (!namesKey || !isDevelopment()) return;
147160
const message = describeUndeclaredActionNames(key, JSON.parse(namesKey) as string[], {
161+
component,
148162
blockType,
149163
objectName,
150164
label,
151165
}, basis);
152166
if (!message || warnedMessages.has(message)) return;
153167
warnedMessages.add(message);
154168
console.warn(message);
155-
}, [key, namesKey, basis, blockType, objectName, label]);
169+
}, [key, namesKey, basis, component, blockType, objectName, label]);
156170
}

‎ui/packages/plugin-list/package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"@object-ui/mobile": "workspace:^",
4949
"@object-ui/permissions": "workspace:^",
5050
"@object-ui/plugin-detail": "workspace:^",
51+
"@object-ui/plugin-grid": "workspace:^",
5152
"@object-ui/react": "workspace:^",
5253
"@object-ui/types": "workspace:^",
5354
"@objectstack/spec": "workspace:^",
@@ -62,6 +63,7 @@
6263
"@object-ui/mobile": "workspace:*",
6364
"@object-ui/permissions": "workspace:*",
6465
"@object-ui/plugin-detail": "workspace:*",
66+
"@object-ui/plugin-grid": "workspace:*",
6567
"@object-ui/react": "workspace:*",
6668
"@object-ui/test-support": "workspace:*",
6769
"@object-ui/types": "workspace:*",

‎ui/packages/plugin-list/src/ListView.tsx‎

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import { platformProvisionsStorage, resolveInjectedSystemColumns, resolveSearchF
5454
import { SystemFieldName } from '@objectstack/spec/system';
5555
import { usePermissions } from '@object-ui/permissions';
5656
import { RecordDetailPanel } from '@object-ui/plugin-detail';
57+
import { resolveBulkActions, useUndeclaredActionNamesWarning } from '@object-ui/plugin-grid';
5758

5859
/**
5960
* The `case 'map'` branch below builds an `object-map` schema by flattening
@@ -2543,6 +2544,51 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
25432544
return declared.filter((a: unknown) => String(a).toLowerCase() !== 'delete');
25442545
}, [schema.bulkActions, bulkDeleteVerdict]);
25452546

2547+
/**
2548+
* [objectstack#30394] What the non-grid bar DRAWS out of `permittedBulkActions`:
2549+
* the grid's own rule (objectstack#30169, ruling C), folded by the grid's own
2550+
* `resolveBulkActions` so the two bars cannot drift.
2551+
*
2552+
* A bare name draws only when the object declares an action of that name,
2553+
* labelled from that declaration (through the app's i18n bundle, as the grid
2554+
* labels it). The canonical `delete` keeps its own path: it is not an object
2555+
* action but the built-in bulk delete, gated above by `bulkDeleteVerdict`,
2556+
* so it is held out of the fold exactly as the grid holds it out. Any other
2557+
* name draws nothing; `undeclared` exists only for the development warning
2558+
* below. Before this fold every member drew a button, so a name the object
2559+
* never declared, an action a disabled package withholds included, showed a
2560+
* button that only toasted "not executable" when pressed.
2561+
*/
2562+
const nonGridBulkBar = React.useMemo(() => {
2563+
const names = (permittedBulkActions ?? []).filter((a: unknown) => typeof a === 'string' && a !== '') as string[];
2564+
const { defs, undeclared } = resolveBulkActions({
2565+
bulkActions: names.filter((a) => a.toLowerCase() !== 'delete'),
2566+
objectActions: objectDef?.actions,
2567+
localizeLabel: (actionName, fallback) =>
2568+
schema.objectName && typeof resolveActionLabel === 'function'
2569+
? resolveActionLabel(schema.objectName, actionName, fallback)
2570+
: fallback,
2571+
});
2572+
const labels = new Map(defs.map((def) => [def.name, def.label]));
2573+
// Authored order, the built-in `delete` where the author put it.
2574+
const drawn = names
2575+
.filter((a) => a.toLowerCase() === 'delete' || labels.has(a))
2576+
.map((name) => ({ name, label: labels.get(name) }));
2577+
return { drawn, undeclared };
2578+
}, [permittedBulkActions, objectDef, schema.objectName, resolveActionLabel]);
2579+
// The grid says its own (ObjectGrid warns for the names it leaves undrawn),
2580+
// so this one speaks only while a non-grid view is on screen, and only once
2581+
// the object's schema read has settled. A list with an object but no schema
2582+
// in hand (no `getObjectSchema`, or the read failed) reports the names as
2583+
// unread, never as undeclared: the grid's own reading.
2584+
useUndeclaredActionNamesWarning({
2585+
key: 'bulkActions',
2586+
names: nonGridBulkBar.undeclared,
2587+
ready: objectDefLoaded && currentView !== 'grid',
2588+
basis: schema.objectName && objectDef == null ? 'unread' : 'declared',
2589+
address: { component: 'ListView', blockType: currentView, objectName: schema.objectName, label: schema.label },
2590+
});
2591+
25462592
/**
25472593
* [objectui#4420] The PER-RECORD half of the same key, for the same bar.
25482594
*
@@ -7360,7 +7406,7 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
73607406
)}
73617407

73627408
{/* Bulk Actions Bar — skip for grid view since ObjectGrid renders its own BulkActionBar */}
7363-
{permittedBulkActions && permittedBulkActions.length > 0 && selectedRows.length > 0 && currentView !== 'grid' && (
7409+
{nonGridBulkBar.drawn.length > 0 && selectedRows.length > 0 && currentView !== 'grid' && (
73647410
<div
73657411
className="border-t border-primary/30 px-4 py-2 flex items-center gap-2 text-xs bg-primary/10 text-foreground shrink-0 shadow-sm"
73667412
role="region"
@@ -7372,8 +7418,8 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
73727418
{selectedRows.length} {selectedRows.length === 1 ? 'item' : 'items'} selected
73737419
</span>
73747420
<div className="flex items-center gap-1.5 ml-3">
7375-
{permittedBulkActions.map((action: any) => {
7376-
const actionStr = String(action).toLowerCase();
7421+
{nonGridBulkBar.drawn.map(({ name: action, label }) => {
7422+
const actionStr = action.toLowerCase();
73777423
const isDestructive = actionStr.includes('delete') || actionStr.includes('remove') || actionStr.includes('destroy');
73787424
const Icon = isDestructive ? Trash2 : null;
73797425
// [objectui#4420] The built-in `delete` runs over the ALLOWED
@@ -7396,7 +7442,7 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
73967442
data-testid={`bulk-action-${action}`}
73977443
>
73987444
{Icon && <Icon className="h-3 w-3" />}
7399-
{tActionLabel(action)}
7445+
{label ?? tActionLabel(action)}
74007446
</Button>
74017447
);
74027448
})}
@@ -7410,7 +7456,7 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
74107456
the dialog's slot: one name for one fact, whichever surface says
74117457
it. */}
74127458
{bulkDeleteEligibility.skipped > 0
7413-
&& permittedBulkActions.some((a: any) => String(a).toLowerCase() === 'delete') && (
7459+
&& nonGridBulkBar.drawn.some(({ name }) => name.toLowerCase() === 'delete') && (
74147460
<span
74157461
className="text-muted-foreground ml-3"
74167462
data-testid="bulk-skipped-notice"

‎ui/packages/plugin-list/src/__tests__/ListView.bulkDeleteVisibleWhen.test.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ function makeDataSource(userActionsDelete?: unknown) {
8080
name: { type: 'text' },
8181
status: { type: 'text' },
8282
},
83+
// `archive` is declared: the non-grid bar draws a bare `bulkActions`
84+
// name only when the object declares it (objectstack#30394).
85+
actions: [{ name: 'archive', label: 'Archive' }],
8386
...(userActionsDelete === undefined ? {} : { userActions: { delete: userActionsDelete } }),
8487
}),
8588
};

0 commit comments

Comments
 (0)