Skip to content

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

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_listreadonly: 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

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions