Skip to content

fix(app-shell): 默认列表视图的身份改向组装器查询,译文键 _views.default 不再落空 (#3770) - #3776

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3770-default-view-identity
Aug 8, 2026
Merged

fix(app-shell): 默认列表视图的身份改向组装器查询,译文键 _views.default 不再落空 (#3770)#3776
yinlianghui merged 1 commit into
mainfrom
claude/issue-3770-default-view-identity

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3770

结论

容器(defineView)把默认列表放在 list 这个键下面。list 是授权文档里的一个槽位,不是视图的身份:组装器 expandViewContainer(框架 loader 与 i18n 提取器调用的同一个函数)给未命名的默认列表分配的身份是 {object}.default,给具名的分配 {object}.{list.name}

objectui 这一侧自己推了第三种拼法 list.name || 'list' —— 没有任何生产者会产出它。于是只声明默认 list 的对象在 objectui 上探测 objects.{object}._views.list.label,而译文包里(objectstack#5164 裁 A,objectstack#6124 迁移)是 _views.default.label,命不中 → 标签、description、emptyState 一起回落英文。

修法按卡内 contract-first 方向:不在 viewSuffixes 加 list→default 双读,也不写 ?? 'default' 字面量,而是向组装器要身份

改了什么

  • MetadataProvider.mergeViewsIntoObjects 的容器分支改为跑 expandViewContainer(objName, view),再把结果喂给与 ViewItem(记录闸门)同一段路由代码(抽出的 applyViewItem)。两个闸门因此产出同一套 {object}.{key} 身份,容器还白拿了组装器的两条规则:
    • 折叠:listViews 里某项与 list 结构相同时,组装器把默认列表折进那一项的身份(本地推导永远做不到这件事,只会多出一个重复 tab);
    • 改名去重:同名冲突时的 _2 重命名。
  • ObjectView 两处 primary.name || 'list' 收敛成一个共享 helper defaultListViewId(packages/app-shell/src/utils/viewIdentity.ts),override 查询与视图切换器提升共用同一次推导,不留任何字面量兜底。已带上组装器限定名的条目直接原样返回(否则会得到 crm_lead.crm_lead.default 的双前缀)。
  • AppHeader 未改:那里是按 URL 段去 listViews 里查,本身没有这条推导,id 修正后它自动跟着对。metadata-admin/inspectors/ViewInspector?? 'list' 也未改 —— 那是设计器编辑的容器槽位 storeKey,属另一条轴。

存量数据测量(派发要求的门控,先测后改)

键面与写入位置。 override 的持久化键就是 view id:
ObjectView.persistViewPatch(view.id)dataSource.updateViewConfig(objectName, viewId, cfg)data-objectstack:client.meta.saveItem('view', viewId, { ...cfg, object, name: viewId })PUT /api/v1/meta/view/{viewId} → 一行 sys_metadata(type='view',name= 该 id)。不落 localStorage;defaultViewId 是每次渲染由 views.find(v => v.isDefault) 现算的,从不持久化,自身没有存量。

结论:改名不会让任何存量数据静默失联。 三条证据:

  1. 真实后端跑的是记录闸门,那条路径的 id 早就是限定名。 框架 objectql/engine.ts 对每个从 manifest 注册的容器同时注册容器本身与 expandViewContainer 展开出的 ViewItem;objectui 的 merge 见到 ViewItem 就跳过容器。framework 的 packages/objectql/src/protocol-view-identity-overlay.test.ts 里那行「从 3777 repro 的 sys_metadata 行捕获」的真实个性化 PUT,存的正是 name: 'showcase_task.default'。也就是说 showcase 默认列表的 override 早就写在 showcase_task.default 下 —— 本 PR 不动这条路径的 id(helper 对已限定名原样返回)。
  2. 退休的 'list' 根本不是一个可表示的视图身份,因此不存在「需要迁移到新 id」的合法存量行:ViewItemNameSchema = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/,裸名一律拒。(已实测:ViewItemSchema / ViewItemWireSchema'list''default' 均 REJECT,对 showcase_contact.default 通过。)
  3. URL 侧无损:resolveViewId 双向解析裸名与限定名;旧的 /view/list 链接现在解析不到,回落到 defaultViewId —— 而那正是它本来指的那个视图。

顺带记录一条本 PR 不修的相邻观察(已另立 #3774):data-objectstack.listViewOverrides(objectName) 读的是 client.meta.getItems(objectName),即 GET /api/v1/meta/{objectName}(把对象名当成 metadata type),而写入走的是 type='view',两边键空间不相交;且它失败时返回 {},ObjectView.loadBatch 见到「是对象」就不再回落逐视图 getView

测试

新增 packages/app-shell/src/views/ObjectView.defaultViewIdentity.test.tsx,钉住整条链路(两仓此前都没有的那条):

服务端 TranslationData(键 _views.default)→ transformSpecTranslations(console loadLanguage 用的真实变换)→ mergeViewsIntoObjectsdefaultListViewIduseObjectLabel().viewLabel(objectName, view.name || view.id, fallback),断言 label / description / emptyState 三个都命中中文;具名视图仍按自己的裸键解析;记录闸门那条(本来就对的)一并钉住;ViewItemNameSchema 钉住「推出来的 id 是可表示的持久化键、'list' 不是」,以及重复推导幂等(否则每次渲染都会换名字、把上一次的 override 孤立掉)。

MetadataProvider.merge.test.ts 的容器用例做了逐条分诊而非批量改拼写:未命名默认列表 → {object}.default;作者写了 list.name → 用作者的键;新增折叠用例(listViews 复述 list 时只出一个视图);form 家族路由。原「跳过容器」用例的注释改写为它现在真正的理由(容器展开后会多出第三个 default 列表 tab 并把 formViews.default 挤成 _2,且 ViewItem 行才是运行时把个性化 heal 上去的那一份)。

反向验证(先预测方向再跑,两个方向都出现了):把退休推导装回去(primary.name || 'list' + bucket.listViews[view.list.name || 'list'])后 ——

× resolves the default list label/... under `_views.default`
  AssertionError: expected 'list' to be 'showcase_contact.default'
× does NOT resolve the retired `_views.list` spelling
  AssertionError: expected '不该命中' to be 'All Contacts'      ← 反向:死键开始命中
✓ resolves the same key for the record gate                    ← 保持绿:这条路径本来就对
Tests  5 failed | 3 passed

第二条是反向红:退回去以后那个本该永远命不中的 _views.list 反而解析出来了。两个方向都断言,所以只回退其中一处调用点也无法保持绿。

跑过的命令(worktree objectui-3770,仓根,均在 flock /tmp/os-heavy-verify.lock 下):

pnpm vitest run --maxWorkers=2 packages/app-shell
  Test Files  297 passed (297)
  Tests  2664 passed | 1 skipped (2665)

pnpm vitest run --maxWorkers=2 apps/console/src/__tests__/{record-block-record-reach,public-block-binding-reach,AppContent.systemHubRoutes}.test.tsx
  Test Files  3 passed (3)   Tests  44 passed (44)

pnpm --filter @object-ui/app-shell type-check     # tsc --noEmit && tsc -p tsconfig.typetests.json,0 错
pnpm --filter @object-ui/app-shell lint           # 0 errors(仅既有 warning)
node scripts/check-control-bytes.mjs              # OK(另做了 grep -naP 自查)
node scripts/check-changeset-presence.mjs         # OK

消费半径已按「规则被谁消费」而不是「改了哪个包」清扫:全仓 mergeViewsIntoObjects 的调用方只有 app-shell 这一处(@object-ui/core 里另有一个同名 adapter,但仓内零消费者,已作为观察类 finding 另立 #3775);plugin-view / apps/console 的 listViews fixture 由 schema 作者提供,不经此 merge。

Changeset

.changeset/default-list-view-identity-3770.md(@object-ui/app-shell: patch,用户可见的标签解析行为)。


Generated by Claude Code

… `list.name || 'list'` (#3770)

A `defineView` container declares its default list under the `list` key. That
key is a slot in the authoring document, not the view's identity: the composer
`expandViewContainer` — the same one the framework loader and the i18n
extractor call — registers an unnamed default list as `<object>.default`.

This renderer derived `list.name || 'list'`, a third spelling no producer
emits, so a default-list-only object probed `_views.list.label` and missed the
published `_views.default.label` (objectstack#5164 ruling A, migrated in
objectstack#6124), falling back to English for the view's label, description
and empty state alike.

- `mergeViewsIntoObjects` expands a stack-packaged container through the
  composer and routes the result through the same path as first-class
  ViewItems, so both gates key by `<object>.<key>` and the container inherits
  the composer's folding and collision renaming.
- `ObjectView` derives the primary view id through one shared
  `defaultListViewId` helper (override lookup + switcher promotion), with no
  literal fallback left behind.

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 2:03pm

Request Review

@github-actions github-actions Bot added the tests label Aug 8, 2026
@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-sT8JmNjn.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) 482.22KB 106.21KB
core (index.js) 2.96KB 1.13KB
create-plugin (index.js) 9.85KB 3.18KB
data-objectstack (index.js) 138.22KB 35.46KB
fields (index.js) 230.90KB 56.84KB
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.09KB 42.72KB
plugin-dashboard (index.js) 117.06KB 30.24KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 232.81KB 57.42KB
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.71KB 49.68KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 8, 2026 14:07
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit b691f06 Aug 8, 2026
20 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3770-default-view-identity branch August 8, 2026 14:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants