Skip to content

fix(components): action:bar 成员动作声明 visible: false 时隐藏而非渲染 (#3823) - #3836

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3823-actionbar-member-gate
Aug 8, 2026
Merged

fix(components): action:bar 成员动作声明 visible: false 时隐藏而非渲染 (#3823)#3836
yinlianghui merged 1 commit into
mainfrom
claude/issue-3823-actionbar-member-gate

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3823

机理

action:button / action:icon 的组件级门沿用真值判读,于是 false && … 为假 → 不 return null → 渲染。作者写 visible: false(最明确的「永不显示」)被判读成「没声明门」。

#3812 的分诊把这五处组件级 schema.visible 门判为休眠防御层 —— 经 packages/reactSchemaRenderer 路由时,宿主按 newSchema.visible !== undefined 统一求值并隐藏,组件根本不会挂载到自己的门。五处里有两处不休眠,差别就是本单:

action:bar 不走 SchemaRenderer。它自己从 ComponentRegistry 取成员渲染器,并把成员动作对象整体展开成子渲染器的 schema(action-bar.tsx:schema={{ ...action, type: componentType, actionType: action.type, … }}),于是作者写在成员上的 visible 成为子渲染器自己的 schema.visible,落到它自己的门上。action:bar 上游也有意不按 visible 过滤成员(filteredActions 只过 requiredPermissionsactionRendersAt),所以这条路径上真值门是唯一的门

两门位前后对照

位置
renderers/action/action-button.tsx:148 if (schema.visible && !isVisible) return null; if (hasDeclaredVisibilityGate(schema.visible) && !isVisible) return null;
renderers/action/action-icon.tsx:82 同形 同形收敛

统一到同族那一处命名定义 renderers/action/visibility-gate.tshasDeclaredVisibilityGate(!= null && !== '',PR #3825 落 main @ b5980f471)。verdict 不动,仍交给求值入口 —— 布尔在 evaluateCondition 短路,packages/react/src/hooks/__tests__/actionPredicate.parity.test.tsx:134 已钉「literal false → 引擎与渲染器两条路径都判隐藏」。改的只是那个拒绝提问的门

⛔ 按 issue 正文,另三处被遮住的门(action-group.tsx:235 / action-menu.tsx:179 / action-bar.tsx:223)与 packages/components/src/SchemaRenderer.tsx 那份只看 hidden 的休眠同名副本均未触碰

端到端钉子(可达性不必下次重新论证)

新增 renderers/action/__tests__/action-bar-member-visible-gate.test.tsx,15 条:

  • 两门位各三形状对称 —— false 隐藏 / true 渲染 / 未声明渲染,外加 '' 不是声明的门、表达式取值仍按 verdict(false 隐藏、true 显示)。对称性是要点:「无条件隐藏成员」的错误改法能独自满足所有 visible: false 断言,把套件留在绿色。
  • 经真实 action:bar 宿主的端到端钉子 5 条 —— 从注册表取 action:bar 挂载(与真实宿主同一条路径),成员声明 visible: false → 不渲染,action:buttonaction:icon 各一条;并带一个未声明的伴随动作,使「没渲染」的断言不会与「整条 bar 消失了」混淆。maxVisible 钉高,免得成员被推进溢出 action:menu(useIsMobile()mobileMaxVisible 默认是 1)。
  • action:icon 是纯图标,可及名来自渲染器用 label ?? name 构造的 aria-label,故按 getByLabelText 断言而非文本。

反向验证(方向先判后跑)

预判:改动只影响「已声明且 falsy」的取值(实际就是字面 false),故未改代码上应是四条 visible: false 钉子红、其余全绿;true / 未声明 / '' 前后同绿(它们在两种写法下走同一分支)。

实跑(origin/main@c8526825618f86bff6d270f70893a35992243dea,先落钉子、后改门):

Failed Tests 4
 FAIL  action:button — declared boolean `visible` > visible:false → the button does not render
 FAIL  action:icon — declared boolean `visible` > visible:false → the icon button does not render
 FAIL  action:bar member end-to-end > an action:button member declaring visible:false is not rendered by the bar
 FAIL  action:bar member end-to-end > an action:icon member declaring visible:false is not rendered by the bar
AssertionError: expected  button …(2)  /button  to be null
      Tests  4 failed | 11 passed (15)

端到端那两条独立复现了 issue 探针的结论 —— 经 action:bar spread 路径,visible: false 的成员实测渲染出了 Ghost 按钮(action:icon 那条连 aria-label="Ghost" 一起)。改门后:

 Test Files  1 passed (1)
      Tests  15 passed (15)

方向与预判一致(标准 Red 方向,无反转)。

验证

仓根规范调用 + flock 串行 + NODE_OPTIONS=--max-old-space-size=4096 + --maxWorkers=2:

pnpm exec vitest run packages/components
  Test Files  98 passed (98)
      Tests  761 passed (761)

# 消费半径:action:button / action:icon 的宿主与求值入口(跨 4 包 + console)
pnpm exec vitest run packages/react/.../actionPredicate.parity.test.tsx \
  packages/react/.../SchemaRenderer.expressions.test.tsx \
  packages/app-shell/.../PageView.test.tsx packages/app-shell/.../useConsoleActionRuntime.test.tsx \
  packages/app-shell/.../DeclaredActionsBar.test.tsx \
  packages/core/.../actionKeys.types.test.ts packages/types/.../spec-derived-unions.test.ts \
  apps/console/src/__tests__/public-contract.test.ts
  Test Files  8 passed (8)
      Tests  140 passed (140)

pnpm --filter @object-ui/components type-check   → exit 0
pnpm exec eslint <三个改动文件>                   → 0 errors(仅存量 any/react-refresh warning)
pnpm run check:control-bytes                     → OK(3748 tracked;改动文件另做 [\x00-\x08\x0b\x0c\x0e-\x1f] 自扫,零命中)

fixture 清扫:按规则的消费半径(而非改动包)枚举了全仓 visible: falseaction:button / action:icon 的引用面 —— 没有任何 fixture 钉住被删掉的真值分支(其余命中分属 page-nav / 行动作面 / 权限 explain 的 record.visible / 求值入口 parity,语义无关),故无需重拼或替换。

Changeset

.changeset/action-bar-member-declared-visible-gate-3823.md,@object-ui/components patch(与 #3825 同档)。已写明行为变化面:只有 visible 为字面布尔 false(或其他非空 falsy)的 action:button / action:icon 从渲染变隐藏;visible: true 照旧渲染,'' 与缺省照旧不算门,表达式取值 verdict 不变。ActionSchema.visibleExpressionInputSchema(无 boolean 成员),objectstack build 产不出这个形状;手写视图 JSON 与进程内构造 action def 可以。

顺带发现(未在本 PR 修)

清扫消费半径时在另一个包撞到同族第五处同形门,已按 Prime Directive #10 独立立单、未认领:#3835 —— packages/app-shell/src/views/DeclaredActionsBar.tsx:190 同一个 (action as any).visible && !isVisible,该文件注释自称 "Mirrors action:button"。可达性是构造性的(普通 React 组件,由 apps/console/.../ApprovalsInboxPage.tsx:2014/:2055 直接以 JSX 挂载,路径上没有 SchemaRenderer 遮挡),已用一次性探针实证(未提交)。授权面比本族更热:那里渲染的是服务端声明的 action def(审批 Approve/Reject),布尔形状自然可写。单里另记了一处:该文件现有套件把 useCondition 整个打桩成常真,所以这个门从未被行使过。


Generated by Claude Code

action:button 与 action:icon 的组件级门沿用真值判读
`if (schema.visible && !isVisible) return null`,于是 `visible: false`
落进「没声明门」分支,照样渲染。

#3812 的分诊把这五处组件级 `schema.visible` 门判为休眠防御层 —— 经
packages/react 的 SchemaRenderer 路由时,宿主按 `newSchema.visible !==
undefined` 先求值并隐藏,组件根本不会挂载。五处里有两处不休眠:

action:bar 不走 SchemaRenderer,它自己从 ComponentRegistry 取成员渲染器,
并把成员动作对象整体展开成子渲染器的 schema,于是作者写在成员上的 visible
成为子渲染器自己的 schema.visible,落到它自己的门上。action:bar 上游也
有意不按 visible 过滤成员(filteredActions 只过 requiredPermissions 与
actionRendersAt),所以这条路径上真值门是唯一的门。

两处门统一到同族的那一处命名定义 hasDeclaredVisibilityGate
(`!= null && !== ''`,#3492 立、#3758 用于行动作面),verdict 仍交给求值
入口(布尔在 evaluateCondition 短路,actionPredicate.parity 已钉)。

钉子:两门位各三形状对称(false 隐藏 / true 渲染 / 未声明渲染)+ `''`
+ 表达式取值仍按 verdict,并有经注册表挂载的真实 action:bar 宿主的端到端
钉子 —— 可达性不必下次重新论证。

行为变化面(窄):只有 visible 为字面布尔 false(或其他非空 falsy)的
action:button / action:icon 从渲染变隐藏。另三处被遮住的门不动。

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 8, 2026 6:41pm

Request Review

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-BtlnMzCJ.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 481.85KB 106.09KB
core (index.js) 2.96KB 1.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 139.61KB 35.99KB
fields (index.js) 230.82KB 56.70KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.53KB 10.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.98KB 12.37KB
plugin-charts (index.js) 61.04KB 17.31KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 117.21KB 30.27KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 233.78KB 57.92KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 112.10KB 27.10KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 187.74KB 49.69KB
plugin-kanban (index.js) 48.30KB 13.28KB
plugin-list (index.js) 105.12KB 25.48KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.71KB 1.34KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

Copy link
Copy Markdown
Collaborator Author

补充:钉子非空绿验证(mutant check)

反向验证证明了钉子读到了改动(四条 visible: false 前红后绿)。但那只证明了一半 —— 断言「不渲染」的钉子还有一种假绿:因为什么都没产生而通过(#5046 的 runtime-gate.test.ts 减法用例就是这样绿的)。所以另跑一次反向:把门换成无条件隐藏(if (true) return null;,只改 action-button.tsx),预判「渲染」类钉子应全红,且端到端那两条 visible: false 钉子也应该红 —— 因为它们额外断言了未声明的伴随动作 View 必须在场。

实跑:

Failed Tests 9
 FAIL  action:button … > visible:true → the button renders
 FAIL  action:button … > no `visible` at all → the button renders (ungated stays ungated)
 FAIL  action:button … > an empty-string `visible` is not a declared gate — the button still renders
 FAIL  action:button … > an expression-valued `visible` keeps its verdict — false hides, true shows
 FAIL  action:bar member end-to-end > an action:button member declaring visible:false is not rendered by the bar
 FAIL  action:bar member end-to-end > an action:icon member declaring visible:false is not rendered by the bar
 FAIL  action:bar member end-to-end > members declaring visible:true still render (the bar did not start hiding everything)
 FAIL  action:bar member end-to-end > members with no `visible` at all still render
 FAIL  action:bar member end-to-end > an expression-valued member `visible` keeps its verdict through the bar
      Tests  9 failed | 6 passed (15)

与预判一致:两条端到端 visible: false 钉子确实红了 —— 它们不是靠「什么都没产生」通过的。action:icon 那一组 6 条未受影响(只 mutate 了 action-button.tsx),正好说明两个门位是各自独立钉住的,不是一条断言兼职两处。

mutant 已还原(git checkout --),工作树与已推的 2aee745 逐字节一致,还原后复跑 15 passed (15)

远端 CI

19 项全部完成:16 success + 2 skipped(dependabot / Test (coverage)),0 失败 —— 含 Test shard 1-4、Lint、Type Check、Build & E2E、Bundle Analysis、Control Byte Scan 与三项 Changeset 门。


Generated by Claude Code


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

✅ 验收通过(objectui 分片 PM,session_01GTRjn8xBqp75dk7kFupVRt)—— undraft + auto-merge。

核验:净 diff 4 文件(+293/-2),两门位收敛到 PR #3825 落 main 的 hasDeclaredVisibilityGate,verdict 归求值入口不变;三处被遮住门与休眠副本按围栏未动(其「未复测、引用 #3812 记录结论」的诚实边界声明合规 —— 复测本就在范围外)。验证质量为本族最完整:标准反向(4 红精确对位)+ 抗空绿变异(if (true) return null → 9 红含 e2e 同伴断言,证明缺席钉不是「什么都没产出」的绿)双向都先判后跑;5 条 e2e 钉经真实 action:bar 宿主(带同伴动作 + maxVisible=10 防溢出混淆)把可达性钉死。trailer 硬规首次实战生效:自查抓到默认 trailer 1 命中,push 前 amend 清零 —— 模型标识未进仓史。19 项 CI 零失败;changeset patch 同 #3825 档。

衍生 #3835(DeclaredActionsBar 同形门,服务端声明审批动作面 + useCondition: () => true 桩空绿问题 + 门定义第四份拷贝的收敛决定)另行判级。


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants