Skip to content

fix(app-shell,core): 对象列表工具栏 Import 消费 importPredicates (#5142) - #5154

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-5142-import-predicates
Aug 18, 2026
Merged

fix(app-shell,core): 对象列表工具栏 Import 消费 importPredicates (#5142)#5154
yinlianghui merged 2 commits into
mainfrom
claude/issue-5142-import-predicates

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #5142

背景:与 #4646 / PR #5145 的镜像关系

@objectstack/spec@17.0.0 同时放宽了两个 toolbar-scope 键,不只 create。二者在 spec 中类型完全相同,resolver 对两键各发一个谓词信封,docblock 一句话把绑定绑在一起(importPredicates — same binding as createPredicates)。#4646 补上了 create 半(PR #5145,关联列表工具栏),import 半仍是「已声明、零消费」。

本 PR 是 import 半,逐一镜像 PR #5145evalCreatePredicate 形。

前提三条逐条复测(基线 b4089beca,含 PR #5145#5147):

  1. spec 两键同型、双发信封 —— 对已装 @objectstack/spec@17.0.0 实测:userActions.import 的对象形经 resolveCrudAffordances 返回 importPredicates: { visibleWhen, disabledWhen },与 create 半逐字对称;.d.tscreate: / import: 都是 z.ZodUnion of [ZodBoolean, RowCrudActionOverrideSchema](1208 / 1253 行)。成立
  2. objectui src 零消费 —— cast 感知 grep 在含 PR5145 的新基线上只命中一处,且是 packages/core/src/utils/managedBy.ts:76 那条「刻意不放宽」的注释本身,不是读取点。成立
  3. PR fix(app-shell,plugin-list): 关联列表「+ New」消费 create 谓词,行内编辑开关补 update 权限门 (#4646, #4647) #5145 刻意留窄型并写明理由 —— 注释在 managedBy.ts UserActionsOverride.create 的 docblock 尾部,原话是「widening the type ahead of a consumer would re-declare the inert-metadata defect this change removes, one key over」。本单正是它指名的那一单。成立

消费落点清点

对象列表工具栏的 Import 入口只有一处:ObjectView.tsx 头部按钮(data-testid="object-view-import-button")。与 create 半不同 —— create 在同文件有两个渲染点(头部按钮 + 手机端浮动 +),而 Import 按设计没有移动端对应物(源注释:CSV 导入本质是桌面工作流,移动端不占位)。因此本 PR 关掉的就是全部入口。

实施

⛔ 未动 spec、未动 ObjectGrid。

测试

新增 ObjectView.importPredicates.test.tsx(14 例,真渲染 + data-testid 断言,不是纯函数替身):visibleWhen 假 ⇒ 隐藏 / 真 ⇒ 保留;disabledWhen 真 ⇒ 置灰、假 ⇒ 可用、空串 ⇒ 无条件;纯 boolean 旧形与无 userActions 不回归;enabled: false 对象级关断;权限门(#4647)与谓词层叠加互不短路(谓词为真也无法重开被主体关掉的);config bucket 未 opt-in 时谓词不能复活按钮;record.* 不可绑定的 fail-CLOSED / fail-SOFT 两向。

managedBy.test.ts 增 4 例,其中一例是编译期钉:类型放宽在运行时被擦除,vitest 看不见,只有 tsc 能看见 —— packages/core/tsconfig.test.json 编译该文件且由包 type-check 串起(#3181),即 CI 跑的那条。

证据:

vitest packages/app-shell        → Test Files 428 passed | Tests 4116 passed | 1 skipped
vitest packages/core             → Test Files  89 passed | Tests 1886 passed
vitest ObjectView.importPredicates + managedBy → 44 passed (44)
turbo run type-check --concurrency=2 → Tasks: 81 successful, 81 total
eslint(三个改动文件)            → 0 errors(161 warnings 全为既有 any)
check-control-bytes              → OK(4571 tracked text files)+ 自扫含 ESC 位 0 命中

反向验证(先书面预判,再跑;结果与预判一致,含一处「反直觉但预判正确」的方向)

(a) 摘消费 —— 还原改前门(affordances.import && can(...),去掉 disabled)。预判:恰 3 例红 —— visibleWhen 假、disabledWhen 真、record.* fail-CLOSED;其余 11 例是控制组,压根不进谓词层,两个世界都绿。实测:Tests 3 failed | 11 passed (14),红的正是预判那三条。

(b) 塞假谓词键 —— 把隐藏用例的 visibleWhen 改拼成 visible_when。预判:该例翻红(按钮照出),证明门是钉在规范键上,而不是「只要有个像谓词的键就算」。实测:1 failed | 13 passed,AssertionError: expected button… to be null

(c) 自选非显然方向:只回滚类型、保留消费者 —— 预判:全部运行时用例仍绿(放宽在运行时被擦除,resolver 是 spec 的,从不查 objectui 本地类型),只有 tsc 会红。实测正如预判:vitest … 44 passed (44),而 tsc -p packages/core/tsconfig.test.json

src/utils/managedBy.test.ts(253,9): error TS2322: Type '{ enabled: boolean; visibleWhen: string; disabledWhen: string; }' is not assignable to type 'boolean | undefined'.
src/utils/managedBy.test.ts(286,22): error TS2322: ... is not assignable to type 'boolean | undefined'.

这条正是加编译期钉的理由:类型那一半对整个运行时套件不可见,窄型在仓内也从不承重 —— 它承的是下游作者 tsc 的判词。同一次变异下 @ts-expect-error42 那行)没有报「未使用」,说明放宽到的是 union 而非 any

三次变异均已还原,commit 后重跑复绿。

半径外发现


Generated by Claude Code

claude added 2 commits August 18, 2026 06:47
`@objectstack/spec@17.0.0` 同时放宽了两个 toolbar-scope 键:`userActions.create`
与 `userActions.import` 类型完全相同(`z.union([z.boolean(),
RowCrudActionOverrideSchema])`),`resolveCrudAffordances` 对两键各发一个谓词信封,
docblock 一句话把两者绑在一起(`importPredicates` — same binding as
`createPredicates`)。#4646 补上了 create 半,import 半仍是「已声明、零消费」:
`importPredicates` 在 objectui src 中没有任何读取者。作者可以写
`userActions.import.visibleWhen`,spec 接受、resolver 解析,而对象列表工具栏照旧
无条件提供 CSV 导入入口。

本次让工具栏消费它,逐一镜像关联列表的 create 半:`visibleWhen` fail-CLOSED、
`disabledWhen` fail-SOFT、声明判定用 `?? true` / `!= null`(而非真值),谓词层叠
在对象级判定之上——谓词只能收窄 bucket / 有效 API 操作集 / 主体授权已允许的,
不能重新打开它们已关闭的。

按 spec 的绑定语义,toolbar 谓词对「工具栏所在 scope 的记录」求值一次,而独立对象
列表没有 scope 记录:`os.user.*` / `features.*` 这类作用域谓词才是这里有意义的形态,
读 `record.*` 的谓词无处绑定并按 fail-closed 隐藏按钮——这正是 spec 为该面明写的
规则,不是本实现的取舍。

`UserActionsOverride.import` 由 `boolean` 放宽为与 `create` 同一 union,刻意与消费者
同车:#4646 有意保留窄型并写明理由——类型先于消费者放宽,等于把「已声明、零消费」
的缺陷在隔壁键上重造一遍。

Fixes #5142

Co-authored-by: Claude <noreply@anthropic.com>
类型放宽在运行时被擦除,vitest 看不见它;能观察这一性质的只有 tsc。
packages/core/tsconfig.test.json 会编译该测试文件,并由包的 type-check 脚本串起
(objectui#3181),正是 CI 跑的那条。把 `import` 改回 `boolean`,带类型注解的常量
即编译失败。

同时补运行时钉:importPredicates 穿过 objectui 侧的 #3391 交集层、boolean 分支不带
谓词、`enabled: false` 时 resolver 仍报告谓词(是否呈现由消费者分层规则决定)。

Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
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) 40.21KB 10.79KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.71KB 1.22KB
auth (invitation-status.js) 1.22KB 0.70KB
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) 5.02KB 0.88KB
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) 502.20KB 112.21KB
core (index.js) 4.11KB 1.62KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 159.03KB 44.08KB
fields (index.js) 234.25KB 58.48KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 39.16KB 10.97KB
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) 9.35KB 3.31KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.42KB 1.42KB
permissions (evaluator.js) 5.12KB 1.74KB
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.81KB 0.83KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.75KB 18.37KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 127.85KB 32.73KB
plugin-designer (index.js) 212.39KB 42.83KB
plugin-detail (index.js) 241.46KB 60.56KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 123.33KB 29.93KB
plugin-gantt (index.js) 164.10KB 39.87KB
plugin-grid (index.js) 198.05KB 53.21KB
plugin-kanban (index.js) 52.72KB 14.54KB
plugin-list (index.js) 111.39KB 27.03KB
plugin-map (index.js) 18.72KB 6.09KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.97KB 11.33KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 83.81KB 20.49KB
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.66KB 3.50KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 31.55KB 10.70KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.28KB 0.68KB
react (schema-input.js) 1.45KB 0.83KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (index.js) 4.77KB 2.16KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 10.76KB 3.17KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 6.92KB 2.40KB
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 (dashboard-filter-alias.js) 6.23KB 2.74KB
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) 3.05KB 1.52KB
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

PM 验收:ACCEPT(session_01GTRjn8xBqp75dk7kFupVRt,objectui 分片,批次 23)

实物核验:merge-base b4089beca(含 PR #5145/#5147);head 67c6de5d8;5 files, +468/−9;标识 grep msg/diff 双零,trailer 唯一正确;releases/控制字节(含 ESC 位)零 ✅。CI 亲读:20/20 check 全 completed,18 success + 2 skipped,零失败 ✅。

验收要点:

  1. scope 记录语义按 spec 原文落地:独立对象列表无 scope 记录 ⇒ 谓词上下文刻意传 null,os.user.*/features.* 正常绑定、record.* 无处绑定 fail-CLOSED 隐藏 —— 「spec 原文如此,非本实现取舍」的归因写清,identity-import 旁路不进谓词层的边界也对。
  2. 类型半的编译期钉有清晰的存在论证:(c) 变异证明窄型在仓内从不承重(运行时 44/44 仍绿),它承的是下游作者 tsc 的判词 —— 所以钉放 core 的 tsconfig.test.json 而非运行时;@ts-expect-error 未报未使用证明放宽到 union 而非 any。
  3. 未抽共享 helper 的取舍正确(五处既有消费者同一惯用形 + PR fix(app-shell,plugin-list): 关联列表「+ New」消费 create 谓词,行内编辑开关补 update 权限门 (#4646, #4647) #5145 docblock 明写「声明判定由各调用方自答」—— 避免半径外改造)。
  4. 新卡 Object-list toolbar's "New" (and its mobile FAB) does not consume createPredicates — the create half is honoured on related lists only #5153 是本单最有价值的副产物:对象列表页自己的「New」按钮与移动端浮动 + 同样不消费 createPredicates —— create 半(PR fix(app-shell,plugin-list): 关联列表「+ New」消费 create 谓词,行内编辑开关补 update 权限门 (#4646, #4647) #5145)只落在关联列表,同一份 userActions.create 声明因渲染面不同得到两种判词。PM 分诊:pm:queue + bug,修法直接镜像本 PR(同文件同惯用形),待本 PR 落 main 后派(同文件 ObjectView.tsx)。
  5. 假信号两次自纠(gh CLI 缺失的等待循环按 PID 精确收掉、无 token curl 的假 COMPLETE 弃用改 MCP)—— 处置合规。

三件套照常:本评论 → undraft → auto-merge(SQUASH)。


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

Development

Successfully merging this pull request may close these issues.

importPredicates is produced by the spec resolver and consumed nowhere — the same inverse-symmetry gap as #4646, one key over

2 participants