在做 #3770(默认列表视图身份)的**存量数据测量**时端到端读了 override 的读/写两条路径,发现的相邻缺陷,不在该单范围内(不同包、不同事实),按 PD #10 单独立此单,未在 PR 里修。 ## 结论 `ObjectView` 的「每视图配置 override」(密度、列宽、排序、隐藏列、inlineEdit…)**写在一个 metadata 命名空间,读在另一个**。批量读方法永远返回空 map,而它返回的空 map 又恰好挡住了本来正确的逐视图回落路径。结果:用户在列表上调的密度/列宽/排序,硬刷新后拿不回来。 ## 证据(读/写两条路径逐跳) **写(正确)** —— `packages/app-shell/src/views/ObjectView.tsx` `persistViewPatch`: ``` dataSource.updateViewConfig(objectName, viewId, cfg) → packages/data-objectstack/src/index.ts updateViewConfig → client.meta.saveItem('view', viewId, { ...cfg, object: objectName, name: viewId }) → PUT /api/v1/meta/view/{viewId} → sys_metadata 行:type='view', name={viewId} ``` `updateViewConfig` 的注释本身写明了这一点:`type='view'`(NOT `type={objectName}` —— 那是 pre-overlay 的误用,服务端从没接过)。 **读(命名空间错了)** —— 同文件 `listViewOverrides`: ``` client.meta.getItems(objectName) → GET /api/v1/meta/{objectName} ← 把对象名当成 metadata TYPE → protocol.getMetaItems({ type: '{objectName}' }) → registry.listItems('{objectName}') + sys_metadata WHERE type='{objectName}' ``` override 全部写在 `type='view'` 下,这个列举因此**不可能**包含它们。异常时 `listViewOverrides` 自己 `catch` 掉并返回 `{}`。 **空 map 还挡住了正确的回落。** `ObjectView` 的 `loadBatch`: ```ts if (typeof (dataSource as any).listViewOverrides === 'function') { try { const all = await (dataSource as any).listViewOverrides(objectName); if (all && typeof all === 'object') { /* 建 map */ return map; } // {} 也是 object } catch { /* fall through */ } } if (typeof dataSource.getView !== 'function') return {}; // 逐视图 getView → client.meta.getItem('view', viewId) —— 命名空间是对的 ``` `{}` 是 object,于是 `return map` 提前返回;而 `listViewOverrides` 内部已经把异常吃掉,所以对这个适配器来说 `catch` 分支也永不触发 —— **逐视图 `getView` 回落在 data-objectstack 上是不可达代码**,尽管它读的正是写入的那个命名空间。 ## 影响 用户在列表视图上做的持久化偏好(密度切换、列宽拖拽、排序、隐藏列、inlineEdit 开关)写进去了、也确实存在 `sys_metadata` 里,但下次进页面读不回来,表现为「设置没保存」。与视图 id 拼写无关:**任何** id 都读不回来,#3770 改不改都一样(那单只改 id 的推导方式)。 ## 备注 / 方向建议(未擅自动手) 批量方法的动机是好的(避免每个视图打一次 GET、对没定制过的视图刷一串 404),问题只在**类型槽填错**。可能的方向:让 `listViewOverrides` 走 `type='view'` 的列举(如按 `object` 过滤的 `/meta/view?object=`,`listViews()` 已经在用类似的过滤法),或在批量结果为空时不要吞掉逐视图回落。具体取哪条要看服务端列举端点的语义,故留给分诊。 ## 相关 - #3770 / PR(默认列表视图身份;该 PR 的存量数据测量里记录了这条观察) - framework `packages/objectql/src/protocol-view-identity-overlay.test.ts`(#2555 / #3777:真实个性化 PUT 的 sys_metadata 行形状) - 查重已做:objectui open issues 搜 `listViewOverrides` / `updateViewConfig` / 视图 override 持久化,无同题单。
Activity
yinlianghui commentedon Aug 8, 2026
分诊 + 认领:PM 循环批 182(objectui 整仓席位,登记表 objectstack#4604)
分诊:具体缺陷、逐跳证据、用户可感("设置没保存")→ auto-queue(bug + pm:queue)。方向按测量定:①批量读的 type 槽修正(先测服务端
type='view'过滤列举的真实语义——listViews() 已在用类似过滤,照它的拼法);②吞错不再冒充权威(listViewOverrides 内部 catch 掉异常返回{}使逐视图 getView 回落成为不可达代码——错误应穿透到回落,与 filter 契约同族:拿不准就别装知道)。两半件同 PR,各自带钉。会话:
session_01GTRjn8xBqp75dk7kFupVRt分支:
claude/issue-3774-view-overrides-readbackWorktree:
objectui-3774文件面:
packages/data-objectstack/src/index.ts(listViewOverrides)+ 必要时packages/app-shell/src/views/ObjectView.tsx的 loadBatch 遮蔽半件 + 两包测试(越界即停)Generated by Claude Code
yinlianghui commentedon Aug 8, 2026
验收:PR #3777 ACCEPT(会话
session_01GTRjn8xBqp75dk7kFupVRt,PM 循环批 183)。交付内容:①类型槽修正——测量推翻了 issue 建议的
?object=路由(框架 rest-server.ts 实读:GET /meta/:type 只认 package/preview 两参数),按裁定 4 豁免走 listViews 同款批准路线(type='view' 一次列举、客户端收窄),两个读者共用新的viewItemObjectName()访问器使其无法再分叉;②吞错不再冒充权威——空成功仍权威早返回(批量优化保住),失败改为 reject 使逐视图 getView 回落可达,且 rejection 不进缓存(单独钉);消费半径扫出第二个同形消费者 InterfaceListPage(批量调用在外层 try 里,rejection 会连自己的回落一起跳过)给了作用域 catch;DataSource 接口 JSDoc 从"把错误命名空间当范本"改为写明双契约条款——防其他适配器复植。证据:往返式测试设计有据(手写 fixture 会带着 bug 全绿——命名空间错误恰好活在 fixture 里;写路径灌、读路径排才是真钉);revert 类型槽 → 6 红含字面 pre-fix URL,且如实指出空成功测试在该 revert 下保持绿正是"往返而非空成功才是抓手"的证明;我派发词里的预测 (c) 被 dev 论证为不可达成(app-shell 测试直接 stub 数据源,适配器 revert 移不动它)——dev 没有为凑预测造红,而是让适配器钉承担该方向并给出"每半件各自带钉"的不对称性论证,采纳,方法库记一笔:跨层修复的逆向验证要按层预测,PM 不应跨层拍红点位置。字节纪律事故又立一功:Write 工具把模板字面量的空格具象化成 5 个裸 NUL,check-control-bytes 因文件未跟踪而失明(#4890 盲区原样),靠派发词强制的手工 grep -naP 抓住——该手工步骤继续保留为必做项。CI 报告时 14/19 零失败,余下由 merge queue 兜底。
新立 #3778(4 处失效调用无人填充的死缓存键,观察级,两方向留分诊)入分诊池。
Generated by Claude Code
fix(data-objectstack): read view overrides from the type='view' names…
fix(data-objectstack): every view write path invalidates the override…