Skip to content

fix(app-shell): 对象列表页「New」与移动端浮动 + 消费 createPredicates (#5153) - #5165

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-5153-list-new-create-predicates
Aug 18, 2026
Merged

fix(app-shell): 对象列表页「New」与移动端浮动 + 消费 createPredicates (#5153)#5165
yinlianghui merged 1 commit into
mainfrom
claude/issue-5153-list-new-create-predicates

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #5153

背景:这是 create 一族的最后一块

createPredicates 在 objectui 里此前只有一个消费方 —— 关联列表工具栏(RelatedRecordActionsBridge,#4646 / PR #5145)。独立对象列表页把同一个 create affordance 渲染了两次(PageHeader 按钮 + 手机端顶掉 header 后顶上的浮动 +),两处都只看对象级判词 affordances.create && can(objectDef.name, 'create'),谓词信封整个被丢掉。

于是作者写一份 userActions.create 对象形,会拿到两种不同判词,取决于是哪个界面画的按钮:关联列表认,对象列表不认。visibleWhen: false(objectui#3492 的形)关不掉这个「New」。

本单补齐消费方,家族闭环:

issue 界面 状态
#4646 create 关联列表工具栏 PR #5145 已落
#5142 import 对象列表工具栏 PR #5154 已落
#5153 create 对象列表页 New + 浮动 + 本 PR

修法:逐字镜像同文件刚落的 import 半

PR #5154importVisible / importDisabled 就在同一个文件里,本 PR 照抄其语义,不自创、不抽共享 helper(各调用方自答,沿用 PR #5154 的取舍先例):

谓词只算一次,两个渲染点消费同一对值 —— 这正是让它们不会互相打架的原因。

浮动 + 的 disabled 三态如实分层

浮动 + 是个裸 button,不是设计系统的 Button,自身没有任何置灰形态。这里没有把三态压成两态(那会把 disabledWhen 映射成「隐藏」,丢掉 spec 明确区分的 hidden vs greyed),而是:原生 disabled(这是传达给辅助技术的那一半)+ Button 自己用的同一组 disabled:opacity-50 disabled:pointer-events-none,让视觉可及性与 header 按钮对齐。

无类型改动

UserActionsOverride.create 早在 #4646 就已是放宽后的联合类型,core 未动;RelatedRecordActionsBridge / RelatedList 未动。半径:ObjectView.tsx + 测试 + changeset(patch)。

前提复验(先行门)

卡面三条判断逐条复测,全部成立(行号因 PR #5154 漂移,现场定位):

  1. 两个 create 入口 ObjectView.tsx:2129/:2194 → 现为 :2189 / :2258,确认两处都只有 affordances.create && can(...) 合取,无谓词层。
  2. spec docblock 复核 —— node_modules/@objectstack/spec@17.0.0RowCrudActionOverrideSchema docblock 与 CrudAffordances.createPredicates 逐字含卡面引文:「On a standalone object list there is no record in scope … hides the button」。独立列表工具栏谓词适用范围内,缺的只是记录。
  3. 入口全量清点:仓内 affordances.create 的 UI 消费方共三处 —— 关联列表(已落)+ 本文件两处。同文件无第三个 create 入口(showCreate: false 已显式关掉插件自带的 create 按钮)。

反向验证(先书面预判,再跑;四组预判全部逐条命中)

变异 预判 实测
A1 只摘浮动 + 的消费 7 红 / 15 绿;两条 FAB-alone 红、两条 header-alone 绿 ✅ 完全一致
A2 只摘 header New 的消费 镜像:两条 header-alone 红、两条 FAB-alone 绿 ✅ 完全一致
C 两处同时还原(改前世界) 9 红 / 13 绿;家族判词呈现卡面原话 ✅ 完全一致
B fixture 塞假谓词键 visibleWhenX 1 红且不抛错(未知键被忽略 → ?? true → 按钮渲染) ✅ 完全一致

A1/A2 互为镜像,是「逐入口可观测」的实证:摘掉任一入口,另一入口的钉子仍然绿,所以两个渲染点是分别被钉住的,不是一条断言数了两遍。为此专门加了一组 per-entry 钉子 —— 合并断言省事,但会让任一入口的回归都失败在同一个测试名下。

变异 C 的价值在于它把卡面主张复现成了机器可判的 diff:

-   "mobileFab": false,        +   "mobileFab": true,
-   "objectListNew": false,    +   "objectListNew": true,
    "relatedList": false,          "relatedList": false,

关联列表认这份声明(false = 已隐藏),对象列表两处不认(true = 照样显示)—— 这正是 #5153 描述的缺陷本身。

变异 B 证明「隐藏」是 visibleWhen 这个键造成的,而非任何对象形 userActions.create 的存在;顺带确认拼错的键不会抛错。这不构成新缺陷:spec 的 RowCrudActionOverrideSchema 声明为 z.core.$strict,未知键由生产方在发布期严格拒绝,渲染方的宽容位于严格 schema 的下游 —— 契约优先,消费端无需再设兜底。

测试

新增 ObjectView.createPredicates.test.tsx,22 钉(镜像 import 半 14 钉 + 4 条 per-entry + 4 条家族判词一致性)。家族判词一致性一次渲染三面(关联列表 / 对象列表 New / 浮动 +),对同一份声明断言三者同判 —— 主语必须是 scope-only 谓词(os.user.*),因为那才是三面都能同样绑定的形;record.* 谓词三面本就不该一致(关联列表绑宿主记录,本页没有),那是 spec 的绑定规则而非缺陷,单独钉在 fail-closed 用例里。

vitest run packages/app-shell/                                   429 files | 4134 passed | 1 skipped
vitest run <本单 + PR5154 + PR5145 三个谓词测试>                    3 files  |   52 passed
pnpm type-check                                                  81 successful, 81 total
eslint packages/app-shell/src/views/ObjectView.tsx               0 errors (159 warnings,
                                                                 与基线 cdac3cc20 逐一相同 —— 本改动 0 新增)
node scripts/check-control-bytes.mjs                             OK (4572 files)
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]' 三个改动文件               clean

基线 sha:cdac3cc20(含 PR #5154 的 import 半 9f23d2b32)。

顺带发现(半径外,已单独立卡,未在本 PR 修)

入口清点时发现 #5164:ObjectDataPage(ADR-0055 的 /data 界面)的「New」只看 can(objectDef.name, 'create'),整个 CRUD affordance 矩阵在该文件里从未解析 —— 连 managedBy bucket 与 userActions.create: false 都关不掉它,比本单的缺陷更宽一层。已按查重后 unassigned 立卡,交 PM 分诊;本 PR 半径严守 ObjectView.tsx,未动该文件。


Generated by Claude Code

…edicates (#5153)

`createPredicates` had exactly one consumer in objectui — the related-list
toolbar (`RelatedRecordActionsBridge`, objectui#4646 / PR #5145). The
standalone object-list page renders the same create affordance twice, in the
PageHeader and as the phone-only floating "+", and neither read the key: both
gated on `affordances.create && can(objectDef.name, 'create')`, the
object-level verdict alone. One `userActions.create` object form therefore
produced two different answers depending on which surface drew the button, and
`visibleWhen: false` (the objectui#3492 shape) did not hide this "New".

Both entry points now layer the toolbar-scope predicates on top of that
verdict, mirroring the `import` half landed in this same file (#5142 / PR
#5154): `visibleWhen` fails CLOSED with `?? true` declared-ness, `disabledWhen`
fails SOFT with its `!= null` gate outside the evaluation. The predicate is
computed once and consumed by both render points, which is what keeps them
from disagreeing. Scope record is `null` — the spec's binding for a standalone
list, where a `record.*` predicate has nothing to bind and fails closed.

The phone FAB is a bare `<button>` with none of the design system's `disabled:`
treatment, so it takes the native `disabled` plus the same
`disabled:opacity-50 disabled:pointer-events-none` utilities `Button` carries,
rather than collapsing the greyed state onto "hidden".

No type change: `UserActionsOverride.create` was already the widened union
(#4646), and `RelatedRecordActionsBridge` is untouched.

Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the tests label Aug 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 25.3 KB 350 KB
Entry file index-RGpRBvuZ.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(objectui 分片 PM,session session_01GTRjn8xBqp75dk7kFupVRt,批次 23)

实物核验(merge-base cdac3cc20 与报告一致):

CI 亲读:20 项中 18 项终态全 success/skipped,零失败;Test shard 2/3 在跑,按尾格零失败先例交 auto-merge 门(绿才合红不合)。席位本地等价面已全过(app-shell 429 文件全量、仓根 type-check 81/81)。

衍生卡 #5164(ObjectDataPage 整个 affordance 矩阵不读)已分诊晋级 pm:queue 并另席派发。

处置:undraft + auto-merge(SQUASH)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 18, 2026 08:18
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 18, 2026
Merged via the queue into main with commit c2dc477 Aug 18, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5153-list-new-create-predicates branch August 18, 2026 08:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Object-list toolbar's "New" (and its mobile FAB) does not consume createPredicates — the create half is honoured on related lists only

2 participants