Skip to content

Commit bb41868

Browse files
claude[bot]claude
andauthored
docs: correct the visibleWhen binding claims per surface, not in bulk (#11816)
objectui#6010 bound the host predicate scope on the five authored-predicate call sites in objectui's form renderer, so `current_user` (plus the ADR-0068 `user` / `ctx.user` / `os.user` aliases) now resolves on a runtime form FIELD `visibleWhen` / `visibleOn` the way it already did on the page/app-nav node gate and per-option `visibleWhen`. The two hand-written pages that documented the old asymmetry are corrected to the measured binding. Measured per claim rather than swept: the FIELD half moved, the SECTION half did not. `FormSection.visibleWhen` is read by exactly one runtime evaluator in objectui — the console's second form renderer — which still passes `undefined` for the scope, and the object-view chain (ObjectForm / ModalForm / DrawerForm / SplitForm) drops the key onto a `section-divider` pseudo-field that carries no predicate at all. So the binding table's one form row becomes two, and the section row keeps the claim it still earns. Two renderer caveats are stated where the binding is, because promising a binding a live surface does not honour is the failure this page keeps hitting: the console's standalone form routes (`/forms/:name`, public `/f/:slug`) evaluate field predicates unbound (objectui#6110), and section predicates are inert on the object-view chain (objectui#6111). The position-gated example moves to the canonical `{ dialect: 'cel' }` envelope via `P`. A bare string carrying the CEL membership operator is normalized by spec parse for authored metadata, but a schema handed straight to the renderer keeps the bare string, which objectui routes to its legacy evaluator — no `in` operator, rejected, then fail-open (objectui#2661 keeps that routing). Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx Co-authored-by: Claude <noreply@anthropic.com>
1 parent ce2b9d2 commit bb41868

2 files changed

Lines changed: 38 additions & 10 deletions

File tree

content/docs/protocol/objectui/layout-dsl.mdx

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,8 @@ expression evaluates truthy.
804804
// e.g. on a PageComponent — `record` and `current_user` are both bound:
805805
visibleWhen: "record.account_type == 'premium'"
806806

807-
// e.g. on a FormSection / FormField — `record` is bound, `current_user` is NOT:
807+
// e.g. on a view FormField — `record`, `previous` and (since objectui#6010)
808+
// `current_user` are bound; on a FormSection, `current_user` is not:
808809
visibleWhen: "record.status != 'closed'"
809810
```
810811

@@ -813,9 +814,19 @@ The predicate's **binding root** is set by the layer, not the key:
813814
| Layer | Predicate binds |
814815
|---|---|
815816
| Page components (`*.page.ts`) | `record` + `current_user` (plus `page.<var>`) |
816-
| Runtime record form sections/fields (`*.view.ts`) | `record` + `previous`**not** `current_user` |
817+
| Runtime record form **fields** (`*.view.ts`) | `record` + `previous` + `current_user` (objectui#6010) |
818+
| Runtime record form **sections** (`*.view.ts`) | `record` + `previous`**not** `current_user` |
817819
| Metadata-editing forms (`*.form.ts`) | `data` — the row under edit |
818820

821+
The two form rows were one row until objectui#6010 bound the host predicate scope
822+
on the form renderer's authored-predicate call sites — only the field half moved.
823+
Two measured caveats travel with them: a **field** predicate is still evaluated
824+
with `current_user` unbound on the console's standalone form routes
825+
(`/forms/:name`, and the public `/f/:slug`), which run a second form renderer
826+
(objectui#6110); and an authored **section** predicate is read by that second
827+
renderer alone, because the object-view chain drops the key before any evaluator
828+
sees it (objectui#6111).
829+
819830
The legacy spellings `visibleOn` (view) and `visibility` (page) are `@deprecated`
820831
aliases: still accepted and folded into `visibleWhen` at parse time, so existing
821832
metadata keeps working. Author new metadata with `visibleWhen`.
@@ -844,15 +855,32 @@ aliases:
844855

845856
{/* os:check */}
846857
```typescript
847-
// On a PageComponent, an app/nav entry, or a per-option `visibleWhen`:
848-
visibleWhen: "'sales_manager' in current_user.positions"
858+
import { P } from '@objectstack/spec';
859+
860+
// On a PageComponent, an app/nav entry, a per-option `visibleWhen`, or a view
861+
// form field. `P` emits the canonical `{ dialect: 'cel' }` envelope:
862+
visibleWhen: P`'sales_manager' in current_user.positions`
849863
```
850864

851-
Two limits come with it. **First, `current_user` is not bound on form sections and
852-
form fields** — those predicates evaluate against `record` (plus `previous`) only,
853-
so the expression above names an unbound root there instead of gating anything.
854-
**Second, `visibleWhen` is presentation, not access control**: it decides what a
855-
client draws from data it already holds. To stop someone from *reading* something,
865+
Write the predicate as a `P` envelope rather than a bare string wherever the
866+
schema does not go through spec parse. Authored metadata is normalized for you —
867+
`ExpressionInputSchema` turns a bare string into `{ dialect: 'cel', source }` at
868+
parse time — but a component tree handed straight to the renderer keeps the bare
869+
string, and objectui routes bare strings to its legacy expression evaluator,
870+
which has no `in` operator: the membership test above is rejected there
871+
(`Unexpected token "i" at position 16`) and then fails open. That routing is
872+
deliberate and documented (objectui#2661); the envelope is what makes one
873+
predicate text mean one thing on every surface.
874+
875+
Two limits come with it. **First, the binding is per surface, not per key.** A
876+
view form **field** predicate binds this scope since objectui#6010; a form
877+
**section** predicate does not (objectui#6111), and neither does an object-level
878+
field rule (`Field.*({ visibleWhen })`, ADR-0036) — that one is evaluated by the
879+
server as well as by the client, binds `record` (plus `previous`, `parent`) only,
880+
and naming `current_user` there is refused by `@objectstack/lint` at build time
881+
rather than faulting at runtime. **Second, `visibleWhen` is presentation, not
882+
access control**: it decides what a client draws from data it already holds. To
883+
stop someone from *reading* something,
856884
use the permission layer — [field-level security](/docs/permissions/field-level-security)
857885
and [permission sets](/docs/permissions/permission-metadata), or
858886
[row-level security](/docs/permissions/rls), whose `using` clause accepts the very

content/docs/ui/views.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ fields: [
430430
| `colSpan` | `1-4` | Legacy absolute column spanprefer `span` |
431431
| `widget` | `string` | Custom widget/component name |
432432
| `dependsOn` | `string` | Parent field for cascading |
433-
| `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) |
433+
| `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) |
434434

435435
## Complete Example
436436

0 commit comments

Comments
 (0)