Skip to content

fix(app-shell,components): 服务端声明动作的 visible 门按「已声明」判定,visible: false 不再渲染 (#3835) - #3843

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-3835-declared-actions-gate
Aug 8, 2026
Merged

fix(app-shell,components): 服务端声明动作的 visible 门按「已声明」判定,visible: false 不再渲染 (#3835)#3843
yinlianghui merged 2 commits into
mainfrom
claude/issue-3835-declared-actions-gate

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3835

门:前后对照

packages/app-shell/src/views/DeclaredActionsBar.tsx

- if ((action as any).visible && !isVisible) return null;
+ if (hasDeclaredVisibilityGate((action as any).visible) && !isVisible) return null;

false && … 为假 → 不 return null → 渲染。作者写 visible: false(最明确的「永不显示」)被真值判读成「没声明门」,verdict 从未被查询。

hasDeclaredVisibilityGate(!= null && !== '')从 @object-ui/components 的 barrel 新增一行 re-export 引入(PM 已裁),不上提 core、不抄第五份(#3142locations 漂移形):

packages/components/src/index.ts

+ export { hasDeclaredVisibilityGate } from './renderers/action/visibility-gate';

verdict 仍交给求值入口,一行未动:toPredicateInput 原样透传布尔、useConditionevaluateCondition 短路,故声明的 falsefalse,表达式取值的 verdict 一律不变。

为什么这处是同族最热的一处

  • 动作 def 服务端声明(objectDef.actions[] / sys_approval_request)。「ActionSchema.visibleExpressionInputSchema、无 boolean 成员,objectstack build 产不出布尔形状」这条族群缓解在这条路径上不成立 —— def 来自服务端 metadata 与进程内构造,布尔是自然写法。
  • 该 bar 由宿主以普通 JSX 直挂(apps/console/src/pages/system/ApprovalsInboxPage.tsx:2014 / :2055),packages/reactSchemaRenderer 不在路径上 —— components/action/**: 同一真值可见性门在动作面上未收口 —— action:group / action:menu 成员动作声明 visible: false 照样渲染(#3758 的同族部分) #3812 判定组件级门休眠的依据(宿主先按 visible !== undefined 求值并隐藏)在此不适用。这个门是该路径上唯一的门。
  • 宿主是审批收件箱记录区动作条:一个本该被 visible: false 关掉的审批动作渲染成可点的 Approve/Reject,而本组件自己的 click handler 就是 POST 审批决定的那一步。

桩收紧(先做,否则钉子空绿 —— #4984 族)

原套件把整个求值入口打桩成常真,注释还写着测试动作不带 visible「所以用不到」:

- vi.mock('@object-ui/react', () => ({
-   useCondition: () => true,
-   toPredicateInput: (v: unknown) => v,
-   …

这让唯一挂载本组件的套件里这道门从未被行使过。重新拼一个「布尔短路」的桩并不能修好这点:visible: false 走到「隐藏」依赖 toPredicateInput 透传布尔 + evaluateCondition 短路,桩就是这套语义的第二份拷贝。改法是只替换动作派发,求值入口用真实实现:

+ vi.mock('@object-ui/react', async (importOriginal) => {
+   const actual = await importOriginal< typeof import('@object-ui/react') >();
+   return { ...actual, ActionProvider: …, useAction: () => ({ execute: executeSpy }) };
+ });

@object-ui/react 的 barrel 对 light dom 项目是可负担的(components 自己的门套件就是不打桩地 import 它)。components 的 barrel 仍打桩(它的重图正是 light 项目故意不加载的),但 hasDeclaredVisibilityGate真实源模块取(纯函数、零依赖)—— 在测试替身里重拼一份 != null && !== '' 会变成这条定义的第五份拷贝,并且无论出货谓词怎么变都保持绿色。

钉子(四形状 + 同伴动作)

packages/app-shell/src/views/__tests__/DeclaredActionsBar.test.tsx 新增 6 例,每例都带一个无门同伴动作(approval_reassign),使「未渲染」不会被误读成「整条 bar 返回 null」(空 located 集是两行之外的另一条码路)。

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

判定: 修前 visible: false 的钉子应红,true / 未声明 / '' / 表达式两向应绿。实跑一致 —— 修前(桩已收紧、源未改):

× visible:false → the declared action does not render
AssertionError: expected  button type="button" …(2)  to be null
+ Received:
  button
    data-testid="declared-action-approval_approve"
    type="button"
    variant="outline"
    Approve
  /button

× a hidden action leaves NO clickable surface in the toolbar
AssertionError: expected …(2) to have a length of 1 but got 2

Tests  2 failed | 18 passed (20)

修后:Tests 20 passed (20)

抗空绿变异(#3836 范式),两个方向,一个如预期一个不如:

  • M1「无条件隐藏」(if (true) return null)→ 15 红(含 visible:true / 未声明 / '' / 表达式为真四例,以及原有 9 例)。如预期:一个「把成员一律隐藏」的改写无法让这套钉子保持绿色。
  • M2「过紧」(visible !== undefined,让 '' 算门)→ 20 全绿与我事先的判定相反,如实记录:这条路径上 '' 被保护了两层 —— 门放过去之后,toPredicateInput('')undefined,evaluateCondition(undefined) 读作「没有条件 → 可见」。所以 '' 那一例记录的是意图语义,在本面不是变异探测器;!== '' 这条腿本身钉在 packages/components 定义旁边。这一点已写进测试的 docblock,以免下个读者把它的通过当成该腿被行使的证明。

验证

仓根 flock + NODE_OPTIONS=--max-old-space-size=4096 + --maxWorkers=2:

命令 结果
vitest run packages/app-shell/src/views/__tests__/DeclaredActionsBar.test.tsx Tests 20 passed (20)
vitest run packages/app-shell/src/views/__tests__/ Test Files 10 passed / Tests 62 passed
vitest run packages/components/src/renderers/action packages/components/src/__tests__/snapshot-critical.test.tsx Test Files 4 passed / Tests 57 passed
pnpm --filter @object-ui/components type-check 通过(tsc --noEmit + typetests)
pnpm --filter @object-ui/app-shell type-check 通过(先 pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build 起依赖 dist;packages/components/dist/index.d.ts 已含新导出)
eslint(三个改动文件) 0 error(仅本文件既有的 no-explicit-any warning)
node scripts/check-control-bytes.mjs OK(3751 tracked;另对改动文件自扫 [\x00-\x08\x0b\x0c\x0e-\x1f] 零命中)

消费半径清扫:全仓仅此一处套件挂载 DeclaredActionsBar(apps/console 无 ApprovalsInboxPage 测试);barrel 改动是纯新增,无导出名冲突,components 侧无公共导出面快照断言。

未动 / 不在本 PR 范围

Changeset

  • .changeset/declared-actions-bar-visible-gate-3835.md@object-ui/app-shell patch,写明审批面变化:visible: false 的服务端声明动作从可点变隐藏。
  • .changeset/components-export-declared-visibility-gate-3835.md@object-ui/components patch,新增 barrel 导出(纯新增,本包无行为变化)。

在飞交叠

#3808 同触 packages/components,但它改 renderers/basic/text-input.tsxplugin-detail,未触 src/index.ts;本 PR 只在 barrel 加一行导出,取并集即可,无文本冲突。#3546 切片四(i18n)不相交。


Generated by Claude Code

…不再渲染 (#3835)

DeclaredActionsBar 的门沿用真值判读
`if ((action as any).visible && !isVisible) return null`,于是
`visible: false` 落进「没声明门」分支,verdict 从未被查询,动作照样渲染。

这是 #3492 同族第五处,也是两条族群缓解在此都不成立的一处:

- 动作 def 是服务端声明的(objectDef.actions[]、sys_approval_request),
  不是手写视图 JSON。「ActionSchema.visible 是 ExpressionInputSchema、无
  boolean 成员,objectstack build 产不出布尔形状」这条缓解在这条路径上不
  成立 —— def 来自服务端 metadata 与进程内构造,布尔是自然写法。
- 该 bar 由宿主以普通 JSX 直挂(apps/console 的 ApprovalsInboxPage),
  packages/react 的 SchemaRenderer 不在这条路径上,#3812 判定组件级门休眠
  的依据(宿主先求值 visible 再隐藏)不适用。这个门是该路径上唯一的门。

宿主是审批收件箱的记录区动作条:一个本该被 visible: false 关掉的审批动作
渲染成可点按钮,而本组件自己的 click handler 就是 POST 审批决定的那一步。

门统一到同族那一处命名定义 hasDeclaredVisibilityGate(`!= null && !== ''`),
从 @object-ui/components 的 barrel 新增一行 re-export 引入,不抄第五份
(#3142 的 locations 漂移形)。verdict 仍交给求值入口:toPredicateInput
原样透传布尔、useCondition 在 evaluateCondition 短路,故声明的 false 即
false,表达式取值的 visible verdict 一律不变。

先收紧常真桩,再落钉子:原套件把整个求值入口打桩成 `useCondition: () =>
true`,注释还写着测试动作不带 visible「所以用不到」—— 这让唯一挂载本组件的
套件里这道门从未被行使(#4984 族:fixture 让坏规则保持绿色)。现在改为只
替换动作派发,useCondition / toPredicateInput 用真实实现,四形状由出货语义
判定。

钉子:false 隐藏 / true 渲染 / 未声明渲染 / `''` 不算门 + 表达式两向 verdict
+ 隐藏动作在工具栏里不留任何可点面(按钮集断言,而非派发计数 —— 未被点击的
多余按钮在两侧都不派发,计数版修前也是绿的)。每例都带一个无门同伴动作,
使「未渲染」不会被误读成「整条 bar 返回 null」。

行为变化面(窄):只有 visible 为字面布尔 false(或其他非空 falsy)的声明
动作从渲染变隐藏;visible: true、`''`、未声明一律不变。

Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt

Co-authored-by: Claude <noreply@anthropic.com>
@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 7:25pm

Request Review

「fixture 让坏规则保持绿色」那一族是 objectstack(framework)仓的 #4984
(见其 packages/lint 内多处裸引用),objectui 侧要按本仓约定写成
objectstack#4984,否则指向本仓一个不存在的号。

Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt

Co-authored-by: Claude <noreply@anthropic.com>
@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-DnpIqogb.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.88KB 106.11KB
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.63KB 49.66KB
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

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

核验:三件按裁定落地(barrel 一行 re-export、门收口、桩收紧),且桩收紧优于要求 —— importOriginal 偏 mock 只替换 ActionProvider/useAction、求值入口用真实实现,避免在测试替身里出现被测语义的第二份拷贝;hasDeclaredVisibilityGate 从真实源模块取,不产生第五份定义。反向验证方向先判后跑吻合;M2 变异「预判红实测绿」的诚实记账采纳('' 在本面被门与求值双层保护,该例记录意图语义而非变异探测器 —— 写进 docblock 不伪造红);过程中一条空绿用例(点同伴数派发)被自查出并整条替换为工具栏按钮集断言。跨仓引用自纠(objectui#4984 → objectstack#4984)合规。19/19 CI 零失败(PM 独立复核);两包 changeset patch;trailer 自查两 commit 均零。衍生 #3842(disabled: '' 反方向失败,审批按钮永久置灰)待判级,本 PR 落 main 后派(同文件)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 8, 2026 19:38
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit d3e738a Aug 8, 2026
20 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3835-declared-actions-gate branch August 8, 2026 19:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants