Skip to content

fix(app-shell): /data 面的「New」读取完整 CRUD affordance 矩阵 (#5164) - #5172

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-5164-datapage-affordance-matrix
Aug 18, 2026
Merged

fix(app-shell): /data 面的「New」读取完整 CRUD affordance 矩阵 (#5164)#5172
yinlianghui merged 1 commit into
mainfrom
claude/issue-5164-datapage-affordance-matrix

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #5164

背景

ObjectDataPage —— 参数化裸数据面(ADR-0055、#2251,路由 /apps/:appName/:objectName/data)—— 的「New」按钮此前只有 can(objectDef.name, 'create') 一个门。该文件从头到尾没有调用过 resolveEffectiveCrudAffordances,所以主体授权之下的四层同时缺席,同一份对象元数据在这个面上得到的 affordance 与隔壁对象列表页不一致:

  1. managedBy bucket 默认被忽略 —— append-only / engine-owned / better-auth 都解析出 create: false,而 /data 仍然给出一个指向 ../new 的「New」;
  2. 对象级 userActions: { create: false } 退出开关关不掉按钮;
  3. [console] 记录详情页头的 api 动作:target URL 的 {field} 占位符从不插值——record_header 执行器不提供 params._rowRecord,literal {id} 被原样发出 #3391 有效 API 操作交集缺失,工具条可以给出服务端会 405 的 create;
  4. createPredicates 谓词层在此未被读取 —— 即 Object-list toolbar's "New" (and its mobile FAB) does not consume createPredicates — the create half is honoured on related lists only #5153 的缺陷,但在这里是上述更大缺口的后果而非缺口本身。

服务端始终是执行点,所以这是 UI 真实性缺陷而非提权:按钮被给出,写入照样被拒。但今日用户可达,且与控制台其余部分渲染所依据的 affordance 矩阵自相矛盾。

改动

门形完全照 ObjectView 的现形(#5153 / PR #5165#5142 / PR #5154 同族),不作二次发明:

affordances = resolveEffectiveCrudAffordances(objectDef, getObjectApiOperations(objectDef.name))
objectCanCreate = affordances.create && can(objectDef.name, 'create')
createPredicates = objectCanCreate ? affordances.createPredicates : undefined
渲染门 = objectCanCreate && createVisible,disabled = createDisabled
  • visibleWhen fail-CLOSED,声明性由 ?? true 判定而非由真值判定 —— 所以字面 visibleWhen: false(objectui#3492 的形)是隐藏按钮,而不是读作「未设门」;默认值是布尔,求值器直接短路,未声明谓词的对象不付任何求值代价。
  • disabledWhen fail-SOFT,!= null 的声明性判定放在求值之外 —— 所以 disabledWhen: '' 读作「无条件」而非「禁用」,不可求值的谓词也永远不会把按钮永久置灰。
  • 绑定:工具条谓词按 spec 的规定,针对工具条所处 scope 的记录求值一次;本面与独立对象列表一样没有记录在 scope 内,故 useRowPredicate(..., null, ...) 是刻意的 —— 读 record.* 的谓词无从绑定,按 fail-closed 规则隐藏按钮,这正是 spec 写明的绑定,不是本 harness 的缺口。os.user.* / features.* 正常绑定,是这里有意义的形。
  • 分层只收窄不重开:既有权限门未被删除,它成为新合取式的一个合取项。谓词无法重新打开 bucket、有效操作或主体授权已关闭的东西。

半径严格限定在 ObjectDataPage.tsx + 测试 + changeset,未触碰 ObjectView.tsx

其它 CRUD 入口普查(只测量,未夹带)

按派发要求普查了本文件是否还有同形缺门的 edit/delete 入口,结论为无,故未开 finding 卡:

  • 「Save as view」按 isAdmin 设门,写的是 view 元数据记录,不是该对象的 CRUD affordance,不属本矩阵;
  • 行级 edit/delete 本文件根本不渲染 —— 这里组装的 list-view schema 不声明 rowActions,也不传 onEdit/onDelete,行 affordance 由 plugin-list / plugin-grid 内部解析,而那两处已各自调用 resolveEffectiveCrudAffordances / isObjectInlineEditable;
  • 抽屉里的 RecordDetailView 是另一张面,有自己的门(此处 onEdit 传的是空函数);
  • 本面没有 Import/Export 入口 —— 「按钮不存在」不构成不真实的 affordance,是面设计问题,不在本卡范围。

另:卡面已核实、此处复核一次 —— 文件内 userActions:(schema 组装处)是视图工具条词汇(search / sort / filter / rowHeight / group / hideFields,#2890),与对象级 CRUD userActions 不是同一键空间,未混用。

测试

新增 packages/app-shell/src/views/ObjectDataPage.createAffordances.test.tsx,29 钉,按四层组合的顺序钉,并把两个 CONTROL 留在旁边,使「整条合取式被删」与「单个合取项被删」可区分:

  • bucket 三钉(append-only / engine-owned / better-auth)+ 谓词不得复活已关 bucket + 已关 bucket 上的显式 create: true opt-in 仍开;
  • 对象级 create: false / enabled: false(带谓词)/ 纯布尔臂;
  • [console] 记录详情页头的 api 动作:target URL 的 {field} 占位符从不插值——record_header 执行器不提供 params._rowRecord,literal {id} 被原样发出 #3391 有效操作:排除 create 关门、包含 create 开门、空集「什么都不暴露」;
  • 谓词层:visibleWhen 真/假、字面 visibleWhen: falsedisabledWhen 置灰(渲染且 disabled,隐藏与置灰保持可区分)、disabledWhen 不成立、disabledWhen: ''record.*visibleWhen fail-closed、record.*disabledWhen fail-soft;
  • 分层:谓词不得重开主体授权关闭的、不得重开有效操作关闭的;
  • 家族 parity 一组:同一份声明在 /data 上与对象列表页取得一致的关门结论。
pnpm exec vitest run packages/app-shell/src/views/ObjectDataPage.createAffordances.test.tsx
  Test Files  1 passed (1)
       Tests  29 passed (29)

# 同族三面回归
pnpm exec vitest run ObjectDataPage.createAffordances ObjectDataPage.saveAsViewFilterFold \
  ObjectView.createPredicates ObjectView.importPredicates RelatedRecordActionsBridge.createPredicates
  Test Files  5 passed (5)
       Tests  91 passed (91)

# 消费半径全扫(app-shell/src/views 全量)
pnpm exec vitest run packages/app-shell/src/views
  Test Files  270 passed (270)
       Tests  2668 passed | 1 skipped (2669)

pnpm --filter @object-ui/app-shell type-check   # tsc --noEmit && tsc -p tsconfig.test.json,通过
pnpm --filter @object-ui/app-shell lint         # 0 errors
pnpm run check:control-bytes                    # OK(4581 tracked text files)

反向验证

先书面预判,再跑变异。 预判:把门改回改动前的裸 can(objectDef.name, 'create')(不解析 affordances、不加谓词层、不传 disabled),应有 18 钉转红、11 钉保持绿;绿的那 11 钉是对照而非覆盖 —— 它们本就走不到本次新增的层。

观测:

Tests  18 failed | 11 passed (29)

红的 18 钉逐条与预判名单一致:三个 bucket 钉、bucket 不被谓词复活、对象级 opt-out、enabled: false、有效操作排除 create、空有效集、visibleWhen 假、字面 visibleWhen: falsedisabledWhen 置灰、record.* fail-closed、谓词不得重开有效操作,以及 parity 五钉。绿的 11 钉:两个 CONTROL、已关 bucket 上的 opt-in、纯布尔臂、有效操作含 createvisibleWhen 真、disabledWhen 不成立、disabledWhen: ''record.*disabledWhen fail-soft、谓词不得重开主体授权、parity 开门钉。

预判与观测完全吻合(matched),无反转、无「更多诊断」情形。还原后 29/29 复绿。


Generated by Claude Code

…atrix (#5164)

`ObjectDataPage` — the parameterized bare data surface (ADR-0055, #2251, route
`/apps/:appName/:objectName/data`) — gated its "New" on
`can(objectDef.name, 'create')` and nothing else. `resolveEffectiveCrudAffordances`
was never called in that file, so all four layers below the principal's grant were
missing at once and one authored object got a different affordance here than on the
object-list page next door:

- the `managedBy` bucket default was ignored (`append-only` / `engine-owned` /
  `better-auth` all resolve `create: false`, yet /data offered "New");
- the object-level `userActions: { create: false }` opt-out did not close it;
- the #3391 effective-API-operation intersection was absent, so the toolbar could
  offer a create the server would 405;
- and `createPredicates` (#5153's layer) was unread here as a consequence.

Resolved exactly as `ObjectView` resolves it: the spec's bucket/`userActions`
matrix intersected with the server-resolved effective operations, then the
toolbar-scope predicate layer on top. Predicate binding and failure posture are
the family's verbatim — `visibleWhen` fails CLOSED with declared-ness by
`?? true`, `disabledWhen` fails SOFT with its `!= null` gate outside the
evaluation, and a `null` scope record so a `record.*` read fails closed per the
spec's documented toolbar binding. The layers only narrow: the pre-existing
permission gate is not replaced, it is one conjunct of the new one.

Fixes #5164

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-Cgljmprf.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) 19.18KB 6.30KB
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 7a1cc688,含 PR #5165c2dc47719 —— 镜像模板在基线上):

  • 3 文件恰为半径(ObjectDataPage.tsx +85/−3 + 29 钉测试 + changeset,src 变更应有 changeset 且有);标识 grep msg/diff 双零(含 ESC),releases 零触碰,ObjectView.tsx 未碰。
  • 门形抽查:resolveEffectiveCrudAffordances 四层齐上,谓词层逐字家族镜像 —— visibleWhen ?? true fail-CLOSED、disabledWhen != null 判定在求值之外 fail-SOFT、scope 传 null。分层钉(谓词不得复活已关 bucket / 不得重开有效操作)是本卡语义的正钉,好。
  • 反向验证:退回裸 can() 门,预判 18 红/11 绿,实测逐条命中零错位;单渲染点差异(无 phone FAB ⇒ 无需 parity 对)已写入注释与测试文档块,29 钉超出卡列五钉的部分各有「删层即漏」的理由,采信。
  • CRUD 入口普查结论(行级 edit/delete 由 plugin-list/grid 自行解析、Save-as-view 非本矩阵)与零新卡,采信。

CI 亲读:21 项中 14 项终态全 success/skipped 零失败(Build & E2E、Bundle Analysis、Changeset 三门、Control Byte 等均绿),Test shard 1-4 / Type Check / Lint 在跑,按尾格零失败先例交 auto-merge 门(绿才合红不合)。席位本地已过 app-shell 全量 270 文件 2668 用例 + 包级双 tsc。

处置:undraft + auto-merge(SQUASH)。交接注记:本 PM 会话处于下班收尾,若后续 CI 转红,由后继 PM 按标准返工流程处理。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 18, 2026 08:53
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 18, 2026
Merged via the queue into main with commit 2f73fef Aug 18, 2026
22 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5164-datapage-affordance-matrix branch August 18, 2026 08:53
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.

ObjectDataPage ("/data" surface) gates "New" on the permission alone — the whole CRUD affordance matrix is unread there

2 participants