Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

A page cannot declare its related lists read-only — "+ New" / row edit / row delete are host-resolved from the CHILD object, so a deliberately read-only record page still offers writes #7300

Closed as not planned

Description

@os-warren

Measured on objectui at the current checkout, against a live seeded app: a read-only "member detail" record page (objectstack-ai/duly#13) rendered 8 "+ New" buttons it had no way to decline.

The gap

PageComponentSchema has no readonly / disabled, and that is a deliberate boundary — ruled 2026-08-12, "editability lives on fields", with EDITABILITY_BOUNDARY_KEYS redirecting an author who writes one. The prescription is right for a field.

It does not reach a related list's CRUD affordances, because those are not fields. RelatedRecordActionsBridge resolves them from the child object:

const rawAff = resolveEffectiveCrudAffordances(childDef, getObjectApiOperations(objectName));
const objectCanCreate = rawAff.create && can(objectName, 'create');

— the child's userActions bucket, intersected with API operations and the principal's grant. Nothing on the page participates. RecordRelatedListProps can decline the opt-in Add-existing picker (add) and row actions, but "+ New", row edit and row delete arrive from the host regardless.

So for a child object that any user may create — here duly_task, where every position holds allowCreate because people do create their own tasks elsewhere in the app — a page whose entire premise is "the manager enters nothing" is handed a create affordance on every task list on it.

The only lever available is object-level (duly_task.userActions.create), which would close creation everywhere — including the screens where it is correct. That is not a workaround, it is a different and wrong change.

Why it is worth a key rather than a doc note

The affordance is not merely cosmetic on a page like this: a manager pressing "+ New" on someone else's task list creates work for that person from a screen documented as read-only, and the write is legal server-side. The page is the only layer that knows this surface is a read surface — the child object is used correctly elsewhere, and the principal's grant is correct too.

Suggested shape

A per-component switch on record:related_list — readonly: true, or affordances: 'none' — consumed where the bridge's handlers are wired in RecordRelatedListBody, narrowing only (never re-opening something the bucket/API/grant already closed, matching the posture createPredicates already takes). A page-level readonly: true that defaults every component would be even better for this use case, but the per-component key is the smaller change and composes.

Related but distinct, both closed: #4646 (related-list "+ New" not consuming userActions.create predicates) and #5153 (the object-list toolbar's half). Those made the object's verdict reach the button. This is about the page having no verdict to give.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p

Activity

  1. added
    enhancementNew feature or request
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 5, 2026
  2. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    分诊 — domain:ui / priority:p2 / pm:blocked / enhancement

    锚定 (anchoring):消费点在 packages/app-shell(bridge + RecordRelatedListBody)⇒ domain:ui。但新键本身写不进本仓 —— packages/types/src/record-components.ts:94 的 docblock 明写 "Aligned with @objectstack/spec RecordRelatedListProps",packages/types/src/index.ts:1288 再导出 SpecRecordRelatedListProps。授权面在上游。

    在 origin/main a472b07 上复核的锚点 —— 逐字成立

    packages/app-shell/src/views/RelatedRecordActionsBridge.tsx:350   const rawAff = resolveEffectiveCrudAffordances(childDef, getObjectApiOperations(objectName));
    packages/app-shell/src/views/RelatedRecordActionsBridge.tsx:351   const objectCanCreate = rawAff.create && can(objectName, 'create');
    packages/app-shell/src/views/RelatedRecordActionsBridge.tsx:358   const createPredicates: RowCrudPredicates | undefined = objectCanCreate
    packages/app-shell/src/views/RelatedRecordActionsBridge.tsx:395     create: objectCanCreate && createVisible,
    

    :350-351 与卡片引的两行一字不差。三个输入 —— 子对象的 userActions 桶、API operations、principal 的 grant —— 页面一个都不参与,这是从 :350 的实参就能读出来的,不需要推断。

    RecordRelatedListProps 的完整已声明键(读 packages/types/src/record-components.ts:94 起的声明体):objectName / relationshipField / columns / sort / limit / filter / title / showViewAll / actions / add / aria。没有 readonly,没有 disabled,没有 affordances。 正控制:同文件 :71-79 确实有一个 readonly,但那是 record:highlights 的 chip 级 inline-edit 开关,不是本卡要的东西 —— 这正好印证卡片的论点:"editability lives on fields" 的处方对字段是对的,但够不到 related list 的 CRUD affordance,因为那些不是字段。

    ⚠️ 一处我验不了、请勿当成已验证:卡片引的 EDITABILITY_BOUNDARY_KEYS 在本仓 git grep 0 命中(全仓,不限目录)。它是 spec 侧的东西。所以"2026-08-12 那条裁决 + 重定向机制"我只能采信卡片,没有本仓证据。这一点对定级不影响(缺陷本身在本仓可验),但对谁来裁很关键。

    为什么是 pm:blocked

    卡片给的形状(record:related_list 上的 readonly: true / affordances: 'none')是一个新的公开可授权键,而 PageComponentSchema / RecordRelatedListProps 都在上游。更要紧的是:这条边界是被明确裁决过的("editability lives on fields"),本卡实质上是在主张"那条裁决没覆盖到 related list 的 CRUD affordance"。⛔ 这不是 objectui 席能单方面推翻的。

    ⚠️ 与 #7297 同型:上游那张卡目前不存在 —— 这是本席在 objectstack-ai/objectstack 未找到对应 issue 的读数。接手第一步是去上游开裁决卡,不是在本仓动手。

    Restart-when(可执行探针) —— ⛔ 判据是装得上,不是"上游 PR 合了":

    pnpm add -D @objectstack/spec@latest >/dev/null 2>&1 \
      && node -e "const s=require('@objectstack/spec/ui');const k=Object.keys(s.RecordRelatedListPropsSchema?.shape??{});console.log(k.join(','));process.exit(/readonly|affordances/.test(k.join(','))?0:1)"

    定级理由

    priority:p2:

    • 卡片最有力的一句我复核并同意:"页面是唯一知道这个表面是只读表面的那一层" —— 子对象在别处被正确使用,principal 的 grant 也是对的,所以现有的任何一层都不该、也不能表达这个意图。
    • 后果不只是难看:经理在别人的任务列表上按 "+ New",从一个被文档描述为只读的屏幕给那个人派了活,而且服务端认这笔写入是合法的。
    • 实测规模:一个只读 member detail 页渲染了 8 个 "+ New",作者无法拒绝其中任何一个。
    • 卡片自己排除了唯一的杠杆:对象级 duly_task.userActions.create 会到处关掉创建,包括那些本该开着的屏幕 —— 那不是绕法,是另一个(错误的)改动。

    不上 p1:没有权限越界(服务端仍按 grant 判定),没有数据损坏,是"提供了不该提供的入口"而非"绕过了应有的检查"。

    ⛔ 定型与边界

    新增授权键 = 加宽公共可授权面 ⇒ Feature ⇒ 踩 manual floor;⊕ 落在协议/契约(spec 的组件 props)⇒ 双重。⛔ 分诊席不裁决、不派单。

    给接手者三条硬要求:

    1. 只收窄,永不放开。 卡片的这个约束是验收条件,不是建议:新键必须只能把 bucket / API / grant 已经关掉的东西继续关着,⛔ 绝不能重新打开。现成的姿态样板就在同文件 :358 的 createPredicates,照它对齐。
    2. 别顺手做页面级 readonly。 卡片自己排了序:"per-component key is the smaller change and composes"。页面级默认值是更大的裁决(它会影响每一个组件类型),⛔ 不要夹带;真要,另起。
    3. 上游卡开出来后把编号回填到这里,探针留在原地。

    给分诊席自己的记录

    本轮已出现第三张"objectui 侧诊断完整、但解锁动作在上游且上游无卡"的卡片(#6947、#7297、本卡)。这不是三次巧合,是一个缺失的例行动作。⛔ 本席不代开上游卡(那属于 objectstack 的 intake),但会在本轮收尾简报里点名,交由席位主帖处理。


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    分诊 — 维护者裁决:不做 · 按 not planned 关闭

    分诊席 #6015,2026-09-24T18:05Z。

    维护者裁决(原文)

    分诊席今天把本卡作为待决事项③提给维护者:

    • 问题:页面上的 record:related_list 能不能声明「只读」?
    • 背景:duly#13 一个刻意只读的成员详情页上出现了 8 个「+ 新建」按钮,因为这些按钮由子对象的权限决定,页面插不上话。
    • 要做就得在 spec 的 RecordRelatedListProps 上新增一个键。本席建议做。

    维护者答复原文:

    「需要我决定的 6 件事: 6947 7300 不做;其他同意」

    ⇒ 本卡不做,按 not_planned 关闭。

    关闭后的现状(照实记录)

    • 关联列表上的「+ 新建」、行编辑、行删除,继续只由子对象的 CRUD 权限和 API 操作决定,即 RelatedRecordActionsBridge 里的 resolveEffectiveCrudAffordances。页面层没有开关。
    • 如果某个页面必须不出现写入入口,现有的办法是从权限上收:让该角色对子对象没有 create / edit / delete 权限。

    什么情况下重开

    维护者改变主意时,由维护者重开。重开后先在 objectstack 开上游 spec 卡(RecordRelatedListProps 新键),本卡挂 Blocked-by:。


    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

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatenhancementNew feature or requestpriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions