diff --git a/content/docs/protocol/objectui/layout-dsl.mdx b/content/docs/protocol/objectui/layout-dsl.mdx index 0b2e0c53fc..9b3901843b 100644 --- a/content/docs/protocol/objectui/layout-dsl.mdx +++ b/content/docs/protocol/objectui/layout-dsl.mdx @@ -804,7 +804,8 @@ expression evaluates truthy. // e.g. on a PageComponent — `record` and `current_user` are both bound: visibleWhen: "record.account_type == 'premium'" -// e.g. on a FormSection / FormField — `record` is bound, `current_user` is NOT: +// e.g. on a view FormField — `record`, `previous` and (since objectui#6010) +// `current_user` are bound; on a FormSection, `current_user` is not: visibleWhen: "record.status != 'closed'" ``` @@ -813,9 +814,19 @@ The predicate's **binding root** is set by the layer, not the key: | Layer | Predicate binds | |---|---| | Page components (`*.page.ts`) | `record` + `current_user` (plus `page.`) | -| Runtime record form sections/fields (`*.view.ts`) | `record` + `previous` — **not** `current_user` | +| Runtime record form **fields** (`*.view.ts`) | `record` + `previous` + `current_user` (objectui#6010) | +| Runtime record form **sections** (`*.view.ts`) | `record` + `previous` — **not** `current_user` | | Metadata-editing forms (`*.form.ts`) | `data` — the row under edit | +The two form rows were one row until objectui#6010 bound the host predicate scope +on the form renderer's authored-predicate call sites — only the field half moved. +Two measured caveats travel with them: a **field** predicate is still evaluated +with `current_user` unbound on the console's standalone form routes +(`/forms/:name`, and the public `/f/:slug`), which run a second form renderer +(objectui#6110); and an authored **section** predicate is read by that second +renderer alone, because the object-view chain drops the key before any evaluator +sees it (objectui#6111). + The legacy spellings `visibleOn` (view) and `visibility` (page) are `@deprecated` aliases: still accepted and folded into `visibleWhen` at parse time, so existing metadata keeps working. Author new metadata with `visibleWhen`. @@ -844,15 +855,32 @@ aliases: {/* os:check */} ```typescript -// On a PageComponent, an app/nav entry, or a per-option `visibleWhen`: -visibleWhen: "'sales_manager' in current_user.positions" +import { P } from '@objectstack/spec'; + +// On a PageComponent, an app/nav entry, a per-option `visibleWhen`, or a view +// form field. `P` emits the canonical `{ dialect: 'cel' }` envelope: +visibleWhen: P`'sales_manager' in current_user.positions` ``` -Two limits come with it. **First, `current_user` is not bound on form sections and -form fields** — those predicates evaluate against `record` (plus `previous`) only, -so the expression above names an unbound root there instead of gating anything. -**Second, `visibleWhen` is presentation, not access control**: it decides what a -client draws from data it already holds. To stop someone from *reading* something, +Write the predicate as a `P` envelope rather than a bare string wherever the +schema does not go through spec parse. Authored metadata is normalized for you — +`ExpressionInputSchema` turns a bare string into `{ dialect: 'cel', source }` at +parse time — but a component tree handed straight to the renderer keeps the bare +string, and objectui routes bare strings to its legacy expression evaluator, +which has no `in` operator: the membership test above is rejected there +(`Unexpected token "i" at position 16`) and then fails open. That routing is +deliberate and documented (objectui#2661); the envelope is what makes one +predicate text mean one thing on every surface. + +Two limits come with it. **First, the binding is per surface, not per key.** A +view form **field** predicate binds this scope since objectui#6010; a form +**section** predicate does not (objectui#6111), and neither does an object-level +field rule (`Field.*({ visibleWhen })`, ADR-0036) — that one is evaluated by the +server as well as by the client, binds `record` (plus `previous`, `parent`) only, +and naming `current_user` there is refused by `@objectstack/lint` at build time +rather than faulting at runtime. **Second, `visibleWhen` is presentation, not +access control**: it decides what a client draws from data it already holds. To +stop someone from *reading* something, use the permission layer — [field-level security](/docs/permissions/field-level-security) and [permission sets](/docs/permissions/permission-metadata), or [row-level security](/docs/permissions/rls), whose `using` clause accepts the very diff --git a/content/docs/ui/views.mdx b/content/docs/ui/views.mdx index 1de3ae5f71..2acebdacce 100644 --- a/content/docs/ui/views.mdx +++ b/content/docs/ui/views.mdx @@ -430,7 +430,7 @@ fields: [ | `colSpan` | `1-4` | Legacy absolute column span — prefer `span` | | `widget` | `string` | Custom widget/component name | | `dependsOn` | `string` | Parent field for cascading | -| `visibleWhen` | `string` | Visibility predicate (CEL); runtime forms bind `record` (+ `previous`, `parent`) — **not** `current_user`, which is unbound at field level and would fault the predicate open (was `visibleOn`, ADR-0089) | +| `visibleWhen` | `string` | Visibility predicate (CEL); runtime form fields bind `record` (+ `previous`, `parent`) and, since objectui#6010, `current_user` — the identity scope page components and per-option predicates already bound (ADR-0089 D1). Two surfaces still evaluate it unbound, where the predicate faults open: the console's standalone form routes `/forms/:name` and `/f/:slug` (objectui#6110), and section-level predicates (objectui#6111). (was `visibleOn`, ADR-0089) | ## Complete Example