Skip to content

fix(grid,types): 对象声明的批量 action 能真正跑在选中记录上 (#3002) - #3031

Merged
os-zhuang merged 1 commit into
mainfrom
claude/legacy-string-bulkactions-spec-fc612e
Jul 30, 2026
Merged

fix(grid,types): 对象声明的批量 action 能真正跑在选中记录上 (#3002)#3031
os-zhuang merged 1 commit into
mainfrom
claude/legacy-string-bulkactions-spec-fc612e

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#3002.

结论先行:spec 不用改

issue 的前提是「spec 的 locations 枚举里没有 bulk 这一档,对象 action 无法声明『我是一个批量操作』」,所以这是跨仓库的、动手前得先定设计。

查下来这个前提不成立ActionSchema 一直有 bulkEnabled——

/** Bulk Operations */bulkEnabled: z.boolean().optional().describe('Whether this action can be applied to multiple selected records'),

缺的从来不是声明,而是消费方。framework 自己的 property-liveness 审计逐字记着这件事:

bulkEnabled — engine has getBulkActions/executeBulk, but no spec-driven view path calls executeBulk.

所以本 PR 纯 objectui 改动,framework/spec 一行没动。

对 issue 四个设计问题的回答

  1. list_bulk / mass_action 这一档吗? 不加。列表选择栏是唯一「记录被多选」的界面,这正是 bulkEnabled 已经命名的东西;再加一档 location 等于承认 bulkEnabled 表达不了它。locations 保持正交——它安放的是 action 的单条入口,一个 action 可以两者都带(locations: ['list_item'] + bulkEnabled: true = 行菜单点一条、选择栏跑 N 条)。
  2. bulkActionDefs 要不要像 rowActionDefs 一样推导? 要,且推导点放在 ObjectGrid——它是三个列表调用方(app-shell ObjectView、plugin-view ObjectView、plugin-list ListView)的唯一收敛点,和 fix(grid): a legacy string row action runs instead of green-toasting a no-op (#2960) #2996 的选择一致,改一处覆盖三处。
  3. N 条记录的语义谁定? 客户端 fan-out。单条 action 的契约(params / recordIdParam / visible)本来就是逐记录的,服务端「一次收全量 id」需要自己的 spec key + endpoint 契约,那是新功能不是修 bug。
  4. 老的 bulkActions: string[] 保留还是废弃? 保留为 view 级覆盖:名字先拿去和 objectDef.actions 解析,解析得到就提升成真 def;解析不到仍按名字派发(消费方可能用该名字注册过 runner handler)。

改动

① 新增纯函数 resolveBulkActionsresolveLegacyRowActions 的批量孪生),把三路词汇折叠成一份 def 列表:

  • view JSON 里内联写的 bulkActionDefs —— 原样保留,同名永远它赢;
  • objectDef.actionsbulkEnabled: true 的 —— 推导出来,这就是「在对象上声明一个批量 action」的新含义;
  • bulkActions 里的名字 —— 按名字解析到对象 action 后提升成该 def,于是带上 label / icon / visible 谓词 / 确认文案 / 参数(字符串形式一个都带不了)。已在栏上的同名项直接丢弃,不再渲染死副本。

② 执行接进现有 BulkActionDialog(params → confirm → progress → result,issue 里点名要求的)。推导出的 def 带 operation: 'custom' + actionDef(源 action);useBulkExecutor 按这个 key 分流,逐记录经 action runner 派发,行记录挂在 _rowRecord 上——和 list_item 行 action 完全同一个键,所以 recordIdParam 注入行为一模一样。参数和确认由对话框收一次再交给 runner,runner 不会逐条再弹;逐条 toast 静音,对话框的聚合结果是唯一报告;失败的记录进错误列表 / 错误 CSV,而不是被记成成功(那正是 #2960 的批量版)。不带 actionDefcustom def 语义不变(consumer 自己 wire onComplete)。

③ 顺带修BulkActionBar 过去只在没有任何 def 时才渲染 legacy 字符串按钮,于是同时写了两者的 view 会静默丢掉一半按钮。折叠之后两个列表互斥,两边都渲染。

验证

  • 新增测试resolveBulkActions.test.ts(13 条纯函数)、objectBulkActionDispatch.test.tsx(6 条,驱动真实 ObjectGrid + 真实 ActionProvider + 真实对话框,断言 fetch 真的按选中记录数发出、每条带自己的记录、失败被逐条归因)。
  • 受影响包plugin-grid 42 文件 / 342 测试全绿;app-shell + plugin-list + plugin-view 257 文件 / 2325 测试全绿;types + core/actions + react/hooks 35 文件 / 537 测试全绿。
  • type-checktypes / core / plugin-grid / app-shell 及其依赖 32 个任务全过。
  • lint:改动文件 0 error(仓库既有 warning 基线未变)。
  • 未做浏览器实测:这条路径需要后端里存在一个声明了 bulkEnabled 的对象 action,而上面的 DOM 测试已经驱动真实 grid/runner/对话框并断言真实 fetch,覆盖同一条链路。

🤖 Generated with Claude Code

…d records (#3002)
`bulkActions: ['push_down']` dispatched the action NAME in the runner's `type`
slot, so it never ran — and the object had nothing to declare a bulk action
with, since `bulkActionDefs` was passed through from view JSON verbatim rather
than derived from `objectDef.actions`.
No spec change needed: `ActionSchema.bulkEnabled` ("whether this action can be
applied to multiple selected records") is already the declaration; it just had
no consumer. `ObjectGrid` — the single convergence point of the three list
callers — now folds three sources into the selection bar via the new pure
`resolveBulkActions`: inline-authored defs, object actions flagged
`bulkEnabled`, and legacy names resolved against `objectDef.actions`.
A derived def carries the source action under `actionDef`; `useBulkExecutor`
dispatches it through the action runner once per record with the row attached
as `_rowRecord`, reusing BulkActionDialog's params → confirm → progress →
result model so params/confirmation are collected once. Failures are attributed
per record instead of counted as successes.
Also: the bar rendered legacy string buttons only when no defs existed, so a
view mixing both silently lost half its buttons.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredJul 30, 2026 11:45am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)27.9 KB350 KB
Entry fileindex-Bg6eytZM.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.20KB2.97KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.12KB3.41KB
auth (LoginForm.js)17.86KB5.29KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.43KB2.09KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.25KB1.01KB
auth (org-roles.js)6.72KB2.85KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)18.38KB4.49KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)3.65KB1.42KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.25KB0.53KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)457.71KB100.07KB
core (index.js)2.16KB0.78KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)134.67KB34.24KB
fields (index.js)222.07KB54.35KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.46KB0.96KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)5.37KB1.72KB
i18n (useObjectLabel.js)25.17KB5.80KB
i18n (useSafeTranslation.js)3.26KB1.44KB
layout (index.js)38.45KB10.67KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.05KB1.53KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)6.84KB2.42KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.90KB12.35KB
plugin-charts (index.js)60.52KB17.11KB
plugin-chatbot (index.js)180.09KB42.72KB
plugin-dashboard (index.js)111.59KB28.74KB
plugin-designer (index.js)210.56KB42.56KB
plugin-detail (index.js)221.50KB54.15KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)106.35KB25.82KB
plugin-gantt (index.js)162.26KB39.53KB
plugin-grid (index.js)183.96KB48.23KB
plugin-kanban (index.js)47.82KB13.18KB
plugin-list (index.js)102.39KB24.18KB
plugin-map (index.js)16.80KB5.24KB
plugin-markdown (index.js)13.65KB4.67KB
plugin-report (index.js)40.32KB10.53KB
plugin-timeline (index.js)25.75KB7.32KB
plugin-tree (index.js)8.36KB2.81KB
plugin-view (index.js)85.91KB20.99KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)3.47KB1.54KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)0.77KB0.41KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (index.js)2.00KB0.96KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)0.20KB0.18KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.04KB1.93KB
types (system-fields.js)2.39KB1.17KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)1.08KB0.64KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-zhuang
os-zhuang merged commit 4874117 into mainJul 30, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/legacy-string-bulkactions-spec-fc612e branch July 30, 2026 11:50
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…stone (#3002) (#3053)
Follow-up to #3031. `@objectstack/spec` 17.0.0 retired `action.bulkEnabled` in
the #3896 audit close-out (framework#4054), which landed while #3031 was in
flight — the spec source still carried the key when its design was settled.
It is now a retiredKey() tombstone, so it is not merely ignored: defineStack
HARD-REJECTS a config that sets it and the backend refuses to boot. The
derivation branch could never run, and #3031's changeset pointed authors at a
key that breaks their app. Browser verification against a real showcase backend
is what surfaced it.
The tombstone prescribes the surviving path — "declare the action in the view's
`bulkActions` instead" — which is #3031's other half, and the half the
end-to-end run exercised: naming `showcase_mark_done` in the view issued one
POST /api/v1/actions/showcase_task/showcase_mark_done per selected record
(10/10 → done: true, progress: 100 server-side).
Everything downstream of the fold is unchanged. A stale `bulkEnabled: true` is
now inert rather than a second path into the bar.
Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Legacy string bulkActions have nothing to resolve against — the spec has no bulk action location

1 participant

@os-zhuang