fix(plugin-grid): 行「更多操作」触发器改为数「真正会渲染的项」,空则不渲染 - #3761
Merged
Conversation
… declarations The object list's row overflow trigger was guarded on whether row-action HANDLERS were wired and how many actions were DECLARED ((canEdit && onEdit) || (canDelete && onDelete) || menuDefs.length > 0 || rowActions.length > 0), while the menu's items were filtered a second time - per item, per record - against `visibleWhen` / `visible`. A row whose every item was predicate-suppressed therefore kept a trigger that opened an empty menu, which reads as a broken page: sys_approval_request declares its list_item actions (approve / reject / recall) gated for approvers, so an admin in the "all" view failed every one of them row by row and every row grew a 128x10 empty box with zero [role=menu] children (objectui#3562). The trigger is now decided by the items that will actually render for that row, and the decision is per ROW because the predicates are per record: within one grid a row that keeps an action keeps its trigger while a row with nothing left renders none. The guard and all three item renderers - the menu item, the built-in Edit/Delete item and the inline `variant: primary` button - read the SAME visibility rule (isBuiltinRowActionVisible / isCustomRowActionVisible via planRowActionMenu), so the trigger and its contents cannot drift apart. The capability gate was already folded into this guard for exactly this reason (framework#3923); the per-record predicates were not. Which items render is unchanged: the `visible: false` truthy gate is preserved verbatim (objectui#3758), and the maxInlineActions slice still runs on the declared primaries so a suppressed primary does not promote the next one into its slot. The actions column is table-level and untouched, so a row with nothing to offer renders an empty cell and every row keeps the same cell count. Fixes objectui#3562 Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
This was referenced Aug 8, 2026
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 8, 2026 12:36
This was referenced Aug 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3562
孪生半件(plugin-grid),关闭本单。data-table 半件是已合并的 PR #3756(
Part of,不关单)——两者是同一裁定、同一形状在两个渲染路径上的落地。本 PR 修的是报告人那一屏:console 对象列表的行菜单。落点(已按
origin/main@e473b6c29复核,行号以内容为锚)packages/plugin-grid/src/components/RowActionMenu.tsx修改前的守卫:数的是**「handler 是否接了」+「声明了几条」**;而项一级的过滤在后面各自跑第二遍——
RowActionMenuItem(自定义动作的visible)、BuiltinRowActionItem(visibleWhen)、RowActionInlineButton(inline primary 的visible)。于是「handler 都在、声明都在,但这一行的每一项都被谓词压掉」这个状态可达:触发器活过守卫,菜单里什么都不渲染。sys_approval_request在list_item声明的 approve / reject / recall 等动作,visible是写给审批人的;admin 在「全部」视图里逐行都不满足 →hasMenu为真、0 项 —— 正是报告实测的data-state=open/z-index: 50/ 128×10 /[role=menu]children=== 0。本组件一条分隔线都没有,全压掉恰好是 0,与报告逐字吻合(data-table 那条路径在两个 handler 同时在场时会无条件渲染一条分隔线,children 至少为 1 —— 这也是 PR #3756 判定落点不在 data-table 的旁证之一)。一处早已存在的同类先例:ADR-0066 D4 的能力门(framework#3923)当初就是因为同样的理由被折进
hasMenu的(注释原文:applied ONCE to the whole declared set so the inline CTA, the overflow menu andhasMenuall agree)。按记录求值的谓词没有被一并折进去,本 PR 补上这一半。packages/plugin-grid/src/__tests__/rowCrudEffectiveOps.test.tsx的辅助函数甚至已经把期望行为写在注释里了(RowActionMenu renders the "⋮" only when at least one entry survives its gates)——在此之前那句话只对能力门成立。修复
守卫改为数真正会渲染的菜单项,零则整个触发器不渲染。
isBuiltinRowActionVisible/isCustomRowActionVisible,planRowActionMenu与三个item 渲染器都读它们。项与触发器因此在结构上无法各说各话(不是靠两处写法碰巧一致)。与 PR fix(data-table): 行「更多操作」触发器改为数「真正会渲染的项」,空则不渲染 #3756 的 data-table 实现保持结构平行(同样的函数名、同样的 plan 形状),不跨包 import。:219的 inline primary 路径:它本来就在谓词失败时return null,所以行为不变;变的是它现在读的是同一个共享函数,而不是自己那份useRowPredicate+ 真值判断。裁定点名要覆盖这条路径,覆盖方式是接入单一来源而不是新增拦截。evalRowActionVisibility走@object-ui/core的evalRowPredicate(fallback: false+warnOnError+fields,与useRowPredicate同参),因为动作条数是变量,一项一个useRowPredicate会把 hook 数量绑到rowActionDefs.length上。布尔短路照抄本包bulkEligibility里写明的理由(objectui#3492:布尔交给引擎会 fault 并 fail-closed,visible: true曾因此把按钮对所有人藏起来)。visibleWhen/visible是按记录求值的,同一张表第一行可能保留 Edit、第二行一项不剩。RowActionMenu本来就由_actions列的cell逐行实例化,所以决策天然落在这里,无需像 data-table 那样新抽 per-row 组件。plan.menuCount而不是plan.count:本表面还有 inline 按钮路径,而 inline 按钮不参与触发器的决策(一行只有一个存活 primary 时,历来就是「有 inline、无 ⋮」)。命名上把这件事写死,避免下一个读者误加。刻意不改的两处(都在裁定第 4 条内)
visible: false真值门逐字保留:!def.visible把false读成「未设门」,于是该项渲染并计数——正是两个 item 组件历来的行为。重新裁决它会改变哪些项渲染,越出本单,已由 行动作声明visible: false仍然渲染该菜单项——可见性门用真值判断(#3492 在行菜单上的未收口部分) #3758 单独记录。测试里正向钉住了这一条。maxInlineActions的 slice 仍跑在「声明的 primaries」上:不按存活重算。重算会把下一个 primary 提进被压掉那个的槽位,即改变某一项渲染在哪里;本单只管触发器是否渲染。测试里也钉住了。(顺带发现的槽位浪费问题已单独立单,见下。)列对齐
操作列(
_actions)是ObjectGrid在表级注入的(hasActions || hasRowActions),表头与每行的单元格都不由本次改动决定;本 PR 只改单元格内部的内容。没有动作可显示的行渲染一个空单元格,与「完全没接 handler 的表」历来的形态一致(与 PR #3756 同一约定)。测试里用真实 ObjectGrid 钉住了混合行(一行有触发器、一行没有)的td数量一致。测试
packages/plugin-grid/src/components/__tests__/RowActionMenu.emptyGuard.test.tsx(新增,23 例):[role=menu]不存在、单元格内无按钮;visible压掉(approve / reject / recall 三条,即sys_approval_request的形状)→ 无触发器;其中一条存活 → 触发器保留;rowActions没有谓词可失败,因此永远保住触发器(刻意保留);:219inline primary:谓词失败 → 既无 inline 按钮也无触发器;同一 def 在通过的行上照常 inline;被压掉的 primary 不把下一个 primary 提进槽位;td数量相同;planRowActionMenu纯函数 11 例:计数、只掉被压的那一项、同一表格两行不同结论、只有 handler 没有对象裁定不计数、存活动作按声明序、inline 被过滤但不计入menuCount、legacy 计数、disabledWhen的项仍然算数、谓词报错 fail-closed(不产生幽灵项/幽灵触发器)、visible: false真值门正向钉住。既有的行菜单测试(
RowActionMenu.test.tsx/RowActionMenu.capabilityGate.test.tsx/rowCrudEffectiveOps/column-features/legacyRowActionDispatch)未改动即全绿。正向(仓库根,flock 串行,
--maxWorkers=2,NODE_OPTIONS=--max-old-space-size=4096):反向验证(先写下预测,后运行)
预测(原文存档于运行前):只把守卫那一行换回旧式声明计数(共享可见性函数、plan、item 渲染全部保留),则 4 红 19 绿——
renders NO trigger when every built-in item is predicate-suppressed(canEdit && onEdit为真,旧守卫复活空触发器)suppresses the trigger when every CUSTOM action is invisible for the row(menuDefs.length === 3 > 0)every row suppressed → no triggers…(0 → 2)mixed rows keep identical td counts(canEdit是表级的,旧守卫给两行都发触发器:1 → 2)并明确预测不会红的两条:inline 套件里那两条(单个 primary 占掉 inline 槽 →
menuDefs为空且无 handler → 旧守卫同样为 false)。它们钉的是:219这条路径,结构上无法察觉守卫被换回去——照模板想当然写成「全红」是不对的。11 条纯函数用例不经过守卫,同样保持绿。实测:
4 红 19 绿,与事先写下的预测逐条一致(含"哪两条不会红"的预测)。
其余门
pnpm --filter @object-ui/plugin-grid type-check→ 0(tsc --noEmit+ typetests 均通过)pnpm --filter @object-ui/plugin-grid lint→ 0 error(563 warning 为全包既有基线)node scripts/check-control-bytes.mjs→ OK;另对改动文件手工扫过控制字符类(grep -naP的\x00-\x08\x0b\x0c\x0e-\x1f),无命中node scripts/check-changeset-no-major.mjs→ OK(changeset 为patch)消费半径清查(不是只扫本包)
全仓 grep
RowActionMenu与row-action-trigger:源码消费者只有plugin-grid/src/ObjectGrid.tsx(_actions列的 cell)与包 barrelindex.tsx的再导出;app-shell/src/views/ObjectView.tsx只在注释里提到它,无 import。跨包无任何 import。e2e live 三个 spec(action-modal/list-row-action-cel/screen-flow)都以「先等到触发器、点开、断言存在的项」为形态——list-row-action-cel显式依赖 "Edit" is always present,即menuCount >= 1,不受影响。备注(评审可留意)
三个新的
export function与 PR #3756 一样与组件同文件(保持结构平行、且「守卫和项读同一个函数」这件事对下一个读者是自证的),代价是react-refresh/only-export-components由 1 条变 4 条 warning。该文件因既有的formatActionLabel导出本来就已经放弃了 fast refresh,故无新增能力损失;若评审倾向拆出独立模块(本包已有bulkEligibility.ts这样的纯逻辑模块先例),我照改。顺带发现、未在本 PR 修、已单独立单:
maxInlineActions的槽位分配跑在声明序上,因此被visible压掉的 primary 会白占一个 inline 槽,把本该 inline 的下一个 primary 挤进溢出菜单(详见 issue 正文)。改它属于「某一项渲染在哪里」,越出本单裁定第 4 条。Generated by Claude Code