Skip to content

fix(plugin-list): a non-grid view's selection bar gates a declared bulk action by its requiredPermissions and visible, through the grid bar's own gate (#30535) - #30580

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-j9fnu9
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-j9fnu9

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread
domain:ui#4 · os-sales · session_01L6bhgWBF1SZ4drR5vh5BSM

Fixes #30535

Clause-②: yes (widening) — @object-ui/plugin-grid now exports useBulkActionItems (with the BulkActionItems and LegacyBulkActionItem types its signature names), the grid selection bar's own gate, so @object-ui/plugin-list reuses it instead of a second copy. Its actionDefs input now reads readonly BulkActionDef[], which accepts every list it accepted before.
Metadata-protocol: none

维护者速读

  • 改了什么:看板、日历、画廊等非表格视图的选择栏,画对象声明的批量操作前先过表格选择栏同一道门:用户不具备 requiredPermissions 的不画;被停用的不画;visible 按每条选中记录求值,至少一条通过才画。bulkActions 里重复的名字只画一个按钮(原来两个按钮同一个 React key)。读不到对象 schema 时的开发态警告,处方句改为点名组件(ListView / ObjectGrid),不再一律写「the grid」。
  • 为什么改:PR fix(plugin-list)!: a non-grid view's selection bar draws only actions the object declares (#30394) #30498 之后非表格选择栏已拿到解析后的操作定义,却只读了标签,于是带权限或 visible 条件的操作对所有人、所有选择都显示;表格那边两道门都有。
  • 风险与回滚:plugin-grid 入口多导出一个 hook 和两个类型,入参放宽为只读数组;表格选择栏与右键菜单行为不变。服务端动作入口本来就拒绝缺权限的调用(实测 403),本改动只是界面不再画出注定被拒的按钮。回滚即 revert 本 PR。
  • 先看:ui/packages/plugin-list/src/ListView.tsx(nonGridBulkFold / nonGridBulkItems / nonGridBulkBar 三段)→ ui/packages/plugin-grid/src/index.tsx(新导出)→ ui/packages/plugin-grid/src/components/BulkActionBar.tsx(只读入参)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/plugin-list/src/__tests__/ListView.nonGridBulkGate-30535.test.tsx,预期 6 条全过;消融门禁时其中 2 条转红,消融去重时 1 条转红。
  • 需要你做的:无,席位落地。

What changes

ListView draws its own selection bar for every view but the grid. Since #30394 it folds bulkActions through plugin-grid's resolveBulkActions and so holds each resolved action definition, requiredPermissions and visible included, but it read only the label. This passes the fold through the grid bar's own gate.

  • plugin-list/src/ListView.tsx. The former nonGridBulkBar memo is split in three. nonGridBulkFold dedupes the names (first occurrence wins; the built-in delete is one button whatever its case) and folds them through resolveBulkActions exactly as before. nonGridBulkItems hands the folded defs to plugin-grid's useBulkActionItems, the hook the grid's BulkActionBar and its right-click RowContextMenu already read: requiredPermissions against the caller's held capabilities (useCapabilityGate, unknown fails open), the activation switch-off (isActionSwitchedOff), and visible once per selected record, offered when at least one passes. nonGridBulkBar then draws, in authored order, delete (its own path, gate and visibleWhen subset, unchanged) and each name the gate offers. The undeclared-name warning reads the fold's undeclared as before.
  • plugin-grid/src/index.tsx. Exports useBulkActionItems and the BulkActionItems / LegacyBulkActionItem types.
  • plugin-grid/src/components/BulkActionBar.tsx. useBulkActionItems's actionDefs reads readonly BulkActionDef[] (the fold's own return type); the hook only filters it. No behaviour change.
  • plugin-grid/src/undeclaredActionNames.ts. The unread prescription says "Give the ListView a data source…" or "Give the ObjectGrid a data source…", from the address's component, instead of "Give the grid…".

Why

The card's first measurement, the run door: POST /api/v1/actions/OBJECT/ACTION refuses a declared action whose requiredPermissions the caller lacks with 403 before anything dispatches (packages/runtime/src/domains/actions.ts, actionPermissionError). The existing pins say so on this head: pnpm --filter @objectstack/runtime exec vitest run src/http-dispatcher.test.ts -t 'ADR-0066 D4 action requiredPermissions gate' gives Tests 5 passed | 246 skipped. So the gap here is the bar drawing a button the door will refuse, not a door that lets the call through.

Tests

Acceptance notes

  • What the bar dispatches is unchanged and out of scope (the card's "Not in scope", the fix(plugin-grid)!: a list's selection bar and row menu draw only actions the object declares (#30169) #30387 review): it still calls the host's onBulkAction(name, selectedRows) with the whole selection, where the grid runs a per-record-visible def over the passing subset only. The bar now draws such an action only when one selected record passes; which records the host receives is left to that ruling.
  • The console's app-shell wires no onBulkAction on this bar (ObjectView.relayRungCensus-7559.test.ts records it as host-runtime, delete only), so in the console a non-delete button here still has no host behind it; unchanged by this PR.

Verification

All on 9eda24eb unless stated. Closure: pnpm exec turbo run build --filter='@objectstack/runtime^...' --filter='@object-ui/plugin-list^...' --filter='@object-ui/plugin-grid^...' --concurrency=2 (53 tasks, lock VERDICT command-exit 0), then pnpm --filter @object-ui/plugin-grid build after the readonly change.

Package suites and type-check
  • pnpm exec vitest run --maxWorkers=2 packages/plugin-list/ packages/plugin-grid/ (from ui/, on 5f1f79b6; 9eda24eb adds only the readonly parameter type and the changeset level): Test Files 469 passed (469), Tests 4218 passed (4218).
  • On 9eda24eb: the new test plus BulkActionBar.test.tsx, bulkActionCapabilityGate.test.tsx, bulk-action-visible.test.tsx: Tests 32 passed (32), VERDICT command-exit 0.
  • pnpm --filter @object-ui/plugin-grid --filter @object-ui/plugin-list run type-check: plugin-list first red on 5f1f79b6 (TS4104: the fold's readonly BulkActionDef[] into the hook's mutable parameter), read through the rebuilt plugin-grid .d.ts; both Done on 9eda24eb after pnpm --filter @object-ui/plugin-grid build.
  • pnpm --dir ui run check:spec-symbols :: exit 0.
Ablations (three, each on a tested behaviour, via scripts/ablation-replace.mjs)

The test imports ListView and plugin-grid from src (ui vitest aliases @object-ui/* to src), so no dist leg.

  1. Gate: anchor const labels = new Map(nonGridBulkItems.defs.map( replaced by const labels = new Map(nonGridBulkFold.defs.map( in ListView.tsx (anchor 1 → 0, blob 2675189af304 → 7ed74bffb459): Tests 2 failed | 4 passed (6) (the lacking-capability case and the no-passing-record case). Restored: blob == HEAD, git diff HEAD empty.
  2. Dedupe: anchor if (seen.has(key)) return false; deleted (blob 2675189af304 → 4a39ec23f849): Tests 1 failed | 5 passed (6) (the repeated-name case). Restored, git diff HEAD empty.
  3. Wording: the prescription's component interpolation replaced by Give the grid a data source in undeclaredActionNames.ts (blob baf3f36dd5d4 → 3602646c8b8c): Tests 1 failed | 5 passed (6). Restored, git diff HEAD empty.
Derived gates (dispatch-gates --commands, 35 families)
  • Derived set identical on 5f1f79b6 and 9eda24eb (35 families).
  • --ran with an exit code on every line: see the os-dev-report on the card for the final-head reconciliation.
  • check-changeset-no-major fed this body's declaration lines as the event: exit 1 with the changeset at minor (Metadata-protocol: none takes patch), exit 0 at patch (5c534bc9).
  • Families CI runs are read from this PR's check-runs.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint is CI's.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English, except the 维护者速读 section the fleet requires.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgWBF1SZ4drR5vh5BSM


Generated by Claude Code

…tion by the grid bar's own rule

ListView's selection bar over kanban, calendar, gallery and the other
non-grid views now passes the actions resolveBulkActions folds through
plugin-grid's useBulkActionItems, the gate the grid's bar and its
right-click menu read: requiredPermissions against the caller's held
capabilities, the activation switch-off, and visible evaluated per
selected record. A repeated bulkActions name draws one button. The
unread-schema warning names its component instead of "the grid".

Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01L6bhgWBF1SZ4drR5vh5BSM
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-sales · session_01L6bhgVfPeFZkN1FP93imKU

维护者速读

  • 改了什么:看板、日历、画廊等非表格视图的选择栏,画对象声明的批量操作前先过表格选择栏同一道门:用户不具备 requiredPermissions 的不画,被停用的不画,visible 按每条选中记录求值、至少一条通过才画。bulkActions 里重复的名字只画一个按钮(原来两个按钮共用一个 React key)。读不到对象 schema 时的开发态警告改为点名组件(ListView / ObjectGrid),不再一律写「the grid」。
  • 为什么改:List views other than the grid: the selection bar draws a declared bulk action without its requiredPermissions or visible gate #30535。PR fix(plugin-list)!: a non-grid view's selection bar draws only actions the object declares (#30394) #30498 之后非表格选择栏已拿到解析后的操作定义,却只读了标签,于是带权限或 visible 条件的操作对所有人、所有选择都显示;表格选择栏两道门都有。
  • 风险与回滚:服务器的动作入口本来就以 403 拒绝缺权限的调用(http-dispatcher.test.ts 的 ADR-0066 D4 钉在本 head 上 5 例绿),本 PR 只是不再画出注定被拒的按钮,不放宽也不收紧任何权限。@object-ui/plugin-grid 新增导出 useBulkActionItems 与类型 BulkActionItems、LegacyBulkActionItem,其 actionDefs 入参放宽为只读数组(Clause-② yes,加宽);两包 changeset 都是 patch。表格选择栏与右键菜单行为不变。回滚即撤本 PR。
  • 先看:ui/packages/plugin-list/src/ListView.tsx 的 nonGridBulkFold、nonGridBulkItems、nonGridBulkBar 三段;然后 ui/packages/plugin-grid/src/index.tsx 的新导出。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/plugin-list/src/__tests__/ListView.nonGridBulkGate-30535.test.tsx,预期 6 passed。
  • 需要你做的:无,席位落地。
  • 席位意见:可合。

席位核对 — ACCEPT · head 9eda24e

  • 形态:7 文件 +224/−13(ListView.tsx +32/−7、plugin-grid index.tsx +6/−4、BulkActionBar.tsx +2/−1、undeclaredActionNames.ts +1/−1;新测试 +160、undeclaredActionNames.test.ts +10;changeset +13,@object-ui/plugin-list 与 @object-ui/plugin-grid 各 patch)。check-governed-merges --pr 30580:NOT governed(0/7),237 行 ≤ 3000。Fixes #30535,draft,目标 main,标题无 !。
  • 复核面:TIER_DEFAULT(声明面:Clause-②: yes (widening),@object-ui/plugin-grid 入口新增 useBulkActionItems 与类型 BulkActionItems、LegacyBulkActionItem,三者原已在 components/BulkActionBar.tsx 导出、只是入口没转出;useBulkActionItems 的 actionDefs 由 BulkActionDef[] 放宽为 readonly BulkActionDef[],原来接受的输入照收;没有 packages/spec 路径、没有受管路径、没有收窄;Metadata-protocol: none),由本席在此写出。认领 6662594075 的这一行已由修正 6663695278 改为 yes,并补记分支 claude/project-thread-j9fnu9。
  • 席位逐行读:nonGridBulkFold 先去重(先出现的拼写留下;内建 delete 不论大小写算一个),再照旧交给 resolveBulkActions,返回 names、defs、undeclared。nonGridBulkItems 把 defs 交给 useBulkActionItems,与表格 BulkActionBar 和右键 RowContextMenu 读的是同一个 hook:mayInvoke(requiredPermissions)(能力未知时放行,与表格相同)、!isActionSwitchedOff、isBulkDefOffered 按每条选中记录求 visible;actions: undefined,所以 legacy 一支为空,不会多画按钮。nonGridBulkBar 按作者顺序画 delete(它自己的路径、门与 visibleWhen 子集,没动)和过了门的名字;undeclared 警告仍读折叠结果,不因门而多报。hook 在组件顶层无条件调用。BulkActionBar.tsx 只改入参类型(hook 只做 filter),undeclaredActionNames.ts 只把处方句里的「the grid」换成 describeComponent(address.component)。派发的内容不变:非 delete 按钮仍以整个选择调用宿主的 onBulkAction,见范围外去向。
  • 前提实测:dev 报告 6663667463。动作入口先量过:缺 requiredPermissions 的调用在派发前 403(ADR-0066 D4 钉 5 passed),所以缺口是界面画了注定被拒的按钮,不是安全缺口。新测试 6 例(缺能力不画、具备则画、能力未知放行、visible 全不过不画与混合选择画、重复名去重无 key 冲突)加处方句 2 例。消融三腿:门(labels 改读未过门的 nonGridBulkFold.defs)2 红 4 绿;去重(删 seen.has 一行)1 红;处方句退回「the grid」1 红;各自还原后 blob 与 HEAD 相同。plugin-list/ 与 plugin-grid/ 469 文件 4218 例绿;本 head 上新测试与 BulkActionBar、能力门、visible 三份旧测试 32 例绿;plugin-list type-check 在 5f1f79b6 上先红(TS4104,只读数组进可变入参),9eda24eb 放宽后两包 type-check 都过;派生门禁 35 条全部 exit 0,0 NOT-MEASURED;check:spec-symbols exit 0。
  • 门禁 check-runs:head 上 55 个(按名取最新一次),44 success,11 skipped,0 红;check-expected-skips --pr 30580:11 个跳过全在 main 的名册里(exit 0)。Build & E2E UI、Live E2E UI、Lint UI、Test UI 16 片与汇总、Type Check UI 与各 Type Check 均绿,Check Changeset 与 PR-body guards success。mergeable_state clean。
  • 接受的偏差:plugin-grid 的入口多了三个导出名,是复用表格那道门而不抄第二份所必需,认领已修正为 Clause-② yes。分支是线程指定的 claude/project-thread-*。
  • 范围外去向:非表格选择栏的非 delete 按钮仍以整个选择调用 onBulkAction,而表格对按记录 visible 的操作只传通过的子集:派发归 fix(plugin-grid)!: a list's selection bar and row menu draw only actions the object declares (#30169) #30387 的裁定,卡上「Not in scope」已写明,不立卡。控制台的 app-shell 没给这条选择栏接 onBulkAction(ObjectView.relayRungCensus-7559.test.ts 记作 host-runtime,只有 delete),所以控制台里这里的非 delete 按钮仍没有宿主承接:本 PR 前后相同,是已登记的现状,不立卡。
  • 落地条款:中继 pr_ready + automerge_enable。合并后由 Fixes 关 List views other than the grid: the selection bar draws a declared bulk action without its requiredPermissions or visible gate #30535,席位同一笔摘掉 pm:dispatched 并留落地评论。

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 16:56
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 6c30357 Nov 5, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-j9fnu9 branch November 5, 2026 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

List views other than the grid: the selection bar draws a declared bulk action without its requiredPermissions or visible gate

1 participant