Skip to content

新增 /:objectName/data 参数化裸数据路由:URL 过滤 + 不绑定视图,现有菜单/路由零破坏 #2251

Description

@os-zhuang

背景

来源:导航建模讨论(#2245、#2247 app-composition 指南的后续)。场景:需要通过 URL 传过滤条件展示对象数据,同时显示常见筛选条件栏,并复用完整列表能力(分组/过滤/排序/展示类型切换)——此场景不应锚定到任何已保存视图。但当前对象路由始终解析出一个「活动视图」,URL 条件只能叠在该视图自带的 filter 之上。

设计经过三轮迭代(方案 A 严格语义 → 三表面模型 → 本版),前两版均因破坏存量菜单/深链语义被否。本版为纯增量方案,原「方案 A」描述作废。

现状 / 根因

已有机制(可复用,不需重建):

  • filter[<field>]=<value> URL 参数:packages/app-shell/src/views/ObjectView.tsx:994-1007 解析,:1222-1228 与活动视图的 filter AND 合并后下发查询(仅支持等值)。
  • uf_<field> URL 参数(ADR-0047,packages/app-shell/src/views/userFilterUrlState.ts):常见筛选栏(UserFilters)选择的 URL 镜像,可分享、刷新不丢。
  • 零视图兜底:对象没有任何 list view 时,ObjectView.tsx:557-564 合成一个 All Records 网格——本方案将其泛化为可寻址表面。

问题:

  1. URL 过滤逃不出活动视图自带的 filter(例:默认视图只显示活动记录,URL 条件永远查不到归档记录);无预置过滤的裸表面没有任何入口可寻址。
  2. 语义误导:URL 过滤进入时视图 Tab 仍高亮某个视图,暗示「你在视图 X 里」;后续调整可能触发视图保存流程,污染共享视图定义。
  3. 导航契约侧没有表达「带条件的对象入口」的字段(ObjectNavItem 无 filters),只能为每个条件建一个视图。

设计:三路由模型(纯增量)

路由 视图 Tab 栏 语义 状态
/:objectName ✅ 工作台:默认视图 + 已保存视图切换 不动(存量菜单/书签零影响)
/:objectName/view/:viewId ✅ 工作台,锚定具名视图 不动
/:objectName/data ± filter[...]/uf_* ❌ 参数化裸数据表面(新增) 新增

/data 表面的行为

  1. 数据范围:行级权限内全量(见安全模型),无任何已保存视图的预置过滤;URL filter[...]/uf_* 条件直接叠加其上,渲染为筛选栏中可见、可删除的 chip。
  2. 能力:常见筛选栏、分组、排序、展示类型切换(grid ↔ kanban ↔ calendar,Odoo view-mode 语义)全部可用;展示类型切换时 URL 过滤状态原样保留(数据范围与展示类型正交)。
  3. 无视图 chrome:不显示已保存视图 Tab 栏——心智模型是「一个 URL 定义的数据切面」,不是「在视图之间浏览」。
  4. 不回写:任何调整不写回任何已保存视图;出口为「另存为新视图」(复用 viewDraft + CreateViewDialog),保存后跳转 /view/<新id> 进入工作台。
  5. 列配置:自动生成自对象定义(经字段级权限过滤,见安全模型);常见筛选栏来源为对象级 defaultUserFilters 或按 filterable 字段自动生成(选型待评审)。
  6. 路由保留段:data 加入 new/view/record 保留段清单(packages/app-shell/src/console/AppContent.tsx 路由表)。
  7. 无参数的 /data 语义明确(权限内全量、无 chrome),可作为未来嵌入/iframe 表面复用。

导航契约(spec 侧,framework 仓)

  • ObjectNavItem 加可选 filters(支持 {current_user_id} 等模板变量,先例 recordId/params)。
  • resolveHref(packages/layout/src/NavigationRenderer.tsx:377)新增分支:带 filters 的 object 项 → /:objectName/data?filter[...];不带 filters 的项行为完全不变。
  • 典型消费方:带条件的菜单入口、仪表盘/报表钻取链接、AI 生成的链接。

操作符扩展(可后置)

保留 filter[field]=value 等值简写,补富语法(如 filter[field][gte]=...)。

安全模型(裸表面 ≠ 绕过授权)

原则:视图不是安全边界。/data 显示的是「行级权限过滤后你有权看到的全量」,不是「所有数据」。保密性由权限层承担(@object-ui/permissions 三层模型:对象级 actions / rowPermissions.filter / fieldPermissions,见 packages/permissions/src/evaluator.ts),视图 filter 只是展示配置。

  1. 路由门禁:无对象 read 权限时 /data(与 /view/:viewId)返回 403 视图,不渲染空列表。
  2. 合成列过字段级权限:/data 的默认列自动生成自对象定义,必须先经 fieldPermissions 过滤——精选视图故意省略的敏感列不得因列合成而暴露(本方案唯一新增的暴露面)。
  3. 不可读字段禁止参与过滤/排序/分组:防 filter-oracle 泄露(如 filter[salary][gte]=... 通过结果集反推数值)。服务端丢弃不可读字段上的谓词;筛选栏同样不提供这些字段。
  4. 行级过滤与入口过滤呈现区分:行级权限 filter 是隐形底线(不出现在筛选栏、不可移除,Odoo ir.rule 式);URL 条件渲染为可见、可删除的 chip。
  5. 强制点在服务端:rowFilter 与字段级限制由 framework 在 API 查询层注入;UI 权限包仅做同构体验裁剪,不作为防线。

验收标准

  • /:objectName/data?filter[status]=open 展示行级权限内全量数据经 URL 条件过滤的结果,不受任何已保存视图 filter 影响;筛选栏/分组/排序/展示类型切换可用。
  • /data 表面不显示已保存视图 Tab 栏;展示类型切换(grid → kanban)后 filter[...]/uf_* 状态保留。
  • /data 上的调整不产生对已保存视图的写回;「另存为新视图」保存后跳转对应 /view/:viewId。
  • 存量行为回归:/:objectName 与 /view/:viewId 的现有行为(默认视图解析、Tab 栏、filter[...] 叠加语义)不变,现有 E2E 全绿。
  • ObjectNavItem.filters 经 resolveHref 产出 /data?filter[...],有单测;不带 filters 的项输出不变。
  • 安全:低权限角色访问 /data?filter[...] 不越过行级 filter;敏感字段不出现在合成列与筛选栏;对不可读字段的 URL 谓词被服务端忽略(有测试覆盖)。
  • 两份 app-composition 文档(docs(skills): add app-composition decision guide (nav vs views vs pages) #2247)同步更新:rule 2 补充「参数化/一次性切面走 /data + URL 条件,值得沉淀的切面才建具名视图」。

关联

Activity

  1. os-zhuang commented on Jul 5, 2026

    @os-zhuang
    ContributorAuthor

    设计讨论补充两组约束(来自 Odoo 模型对照与数据保密性评审):

    补充验收标准:展示类型切换保留过滤状态

    裸表面(_all)上切换展示类型(grid → kanban 等,不点任何具名视图 Tab)时,URL 中的 filter[...]/uf_* 状态必须原样保留——数据范围与展示类型正交(Odoo 的 view-mode 切换语义)。

    补充章节:安全模型(裸表面 ≠ 绕过授权)

    原则:视图不是安全边界。裸表面显示的是「行级权限过滤后你有权看到的全量」,不是「所有数据」。保密性由权限层承担(参照 @object-ui/permissions 已有的三层模型:对象级 actions / rowPermissions.filter / fieldPermissions,见 packages/permissions/src/evaluator.ts),视图 filter 只是展示配置。

    必须满足:

    1. 路由门禁:无对象 read 权限时,裸路由与 /view/:viewId 返回 403 视图,不渲染空列表。
    2. 合成列过字段级权限:_all 的默认列自动生成自对象定义,必须先经 fieldPermissions 过滤——精选视图故意省略的敏感列不得因列合成而暴露(方案 A 相对现状唯一新增的暴露面)。
    3. 不可读字段禁止参与过滤/排序/分组:防 filter-oracle 泄露(如 filter[salary][gte]=... 通过结果集反推数值)。服务端丢弃不可读字段上的谓词;筛选栏(defaultUserFilters/自动生成)同样不提供这些字段。
    4. 行级过滤与入口过滤呈现区分:行级权限 filter 是隐形底线(不出现在筛选栏、不可移除,Odoo ir.rule 式);入口/URL 条件渲染为可见、可删除的 chip(优于 Odoo action domain 的隐形设计)。
    5. 强制点在服务端:rowFilter 与字段级限制由 framework 在 API 查询层注入;UI 权限包仅做同构体验裁剪,不作为防线。

    相应地,验收标准追加:以低权限角色访问 /:objectName?filter[...],返回数据不越过行级 filter;敏感字段不出现在合成列与筛选栏;对不可读字段的 URL 谓词被服务端忽略(有测试覆盖)。


    Generated by Claude Code

  2. changed the title [-]对象路由语义重构:裸对象路由 = 全量数据 + URL 过滤的全功能表面,视图绑定仅走 /view/:viewId(方案 A)[/-] [+]新增 /:objectName/data 参数化裸数据路由:URL 过滤 + 不绑定视图,现有菜单/路由零破坏[/+] on Jul 5, 2026
  3. os-zhuang commented on Jul 5, 2026

    @os-zhuang
    ContributorAuthor

    objectui 侧实现已随 #2255 合入 main(squash b8f158c),决策记录沉淀为 ADR-0055(docs/adr/0055-parameterized-bare-data-surface.md)。

    已落地:/:objectName/data 路由 + ObjectDataPage(可删 chip、无回写、另存为视图、权限门禁与字段级裁剪、展示切换保留过滤)、NavigationItem.filters + resolveHref 分支(12 单测)、i18n(en/zh)、app-composition 两份指南与 console 路由文档更新。

    本 issue 保持 open,剩余为 framework 仓联动项:


    Generated by Claude Code

  4. os-zhuang commented on Jul 5, 2026

    @os-zhuang
    ContributorAuthor

    进度更新:framework 侧 spec 同步已完成。

    剩余:

    • 服务端强制:行级 filter 注入 + 丢弃不可读字段谓词(anti filter-oracle)— framework API 层,建议单独立项
    • 富操作符语法 filter[<field>][<op>]=(可选,后置)
    • 带种子数据环境的实机验收

    Generated by Claude Code

  5. os-zhuang commented on Jul 5, 2026

    @os-zhuang
    ContributorAuthor

    进度更新:剩余项全部进入 PR。

    仍开放:富操作符语法(filter[<field>][<op>]=,等需求)、带种子数据环境的实机验收、packages/verify 的 filter-oracle 不变量(#2630 备注为 follow-up)。


    Generated by Claude Code

  6. os-zhuang commented on Jul 5, 2026

    @os-zhuang
    ContributorAuthor

    全部合并完成。

    本线程剩余开放项(非阻塞):

    • 富操作符语法 filter[<field>][<op>]=(等真实需求)
    • 带种子数据环境的 /data 实机验收
    • packages/verify 的 filter-oracle 不变量(framework#2630 已备注为 follow-up)

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions