Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/12696-designer-visiblewhen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@object-ui/app-shell': minor
---

The metadata-admin designer reads and writes the form-view visibility predicate as `visibleWhen` only (objectui#12696). **BREAKING** for a host that still spells the retired `visibleOn` on a form layout it hands to `SchemaForm`.

`@objectstack/spec` 18 retires the form view's `visibleOn` (ADR-0089), and the console form page and `@object-ui/plugin-form` already stopped reading it (objectui#12654). The designer was the last reader and writer:

- **`SchemaForm` no longer folds `visibleOn` into `visibleWhen`.** A FormView section, a section field, a record-editor sub-field or a raw JSONSchema property that carries only `visibleOn` is now shown unconditionally, as it is in the runtime form. `visibleWhen` hides and shows exactly as before.
- **`FormFieldSpec` and `FormSectionSpec` drop their `visibleOn` member.** Both types are exported from the `@object-ui/app-shell` entry. A typed object literal that sets `visibleOn` on either one no longer compiles.
- **The view create form writes `visibleWhen`.** Its `kind` and `formType` layout pickers still show only for their own `viewKind`.
- **A property named `visibleOn` no longer gets the condition builder by name.** No form the bundled spec ships declares one. A `*When` property still does.

Clause-②: no (narrowing). One member is removed from each of two exported types, and `SchemaForm` honours one fewer input key. Nothing is added and no accepted input widens.

The fix for an affected host is one rename: write `visibleWhen` where it wrote `visibleOn`. The predicate value keeps the same shape.
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,7 @@ describe('objectui#5627 — the keys reach the rows, in their own slots', () =>
sections: [
{ label: 'a', visibleWhen: 'record.x == 1', fields: ['f'] },
{ label: 'b', visibleWhen: { dialect: 'cel', source: 'record.x == 2' }, fields: ['f'] },
// @ts-expect-error objectui#12696 — `FormSectionSpec` refuses the retired key; this raw section carries it on purpose
{ label: 'c', visibleOn: 'record.x == 4', fields: ['f'] },
{ label: 'd', fields: ['f'] },
],
Expand Down
4 changes: 2 additions & 2 deletions apps/console/src/components/FormPage.fieldSpec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ export function formFieldSpecContractPins(): void {
// ── PIN C — the measurement, made executable. Every key below was
// `TS2353: … does not exist in type 'FormFieldSpec'` in this app before
// #5542, while the runtime accepted it and metadata-admin authored it. They
// are the difference the key-by-key comparison found.
// are the difference the key-by-key comparison found. (`visibleOn` was one of
// them until objectui#12696 dropped that retired spelling from the type.)
const wasUndeclarableHere: ConsoleFieldSpec = {
field: 'stage',
type: 'select',
Expand All @@ -126,7 +127,6 @@ export function formFieldSpecContractPins(): void {
disclosure: 'popover',
language: 'sql',
visibleWhen: '${data.kind == "deal"}',
visibleOn: { dialect: 'cel', source: 'data.kind == "deal"' },
fields: ['nested', { field: 'deeper' }],
};
void wasUndeclarableHere;
Expand Down
4 changes: 2 additions & 2 deletions apps/console/src/components/FormPage.viewSpec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,13 @@ export function formContainerContractPins(): void {
// ── PIN D — the measurement, made executable. Every key below was
// `TS2353: … does not exist in type 'FormSectionSpec'` in this app before
// #5596, while `@objectstack/spec`'s `FormSectionSchema` accepted all of them
// and app-shell's copy declared three of them.
// and app-shell's copy declared three of them. (`visibleOn` was one of them
// until objectui#12696 dropped that retired spelling from the type.)
const wasUndeclarableHere: ConsoleSectionSpec = {
name: 'advanced_options',
description: 'Rarely-used options',
pane: 'secondary',
visibleWhen: '${record.kind == "deal"}',
visibleOn: { dialect: 'cel', source: 'record.kind == "deal"' },
columns: '3',
fields: ['name'],
};
Expand Down
1 change: 1 addition & 0 deletions apps/console/src/components/FormPage.visibleWhen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ describe('objectui#5594 — the predicate reaches the row and the verdict', () =
fields: [
{ field: 'a', visibleWhen: "record.x == 1" },
{ field: 'b', visibleWhen: { dialect: 'cel', source: 'record.x == 2' } },
// @ts-expect-error objectui#12696 — `FormFieldSpec` refuses the retired key; this raw field carries it on purpose
{ field: 'c', visibleOn: "record.x == 4" },
{ field: 'd' },
'e',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
* ## The defect these cases reproduce
*
* `SchemaForm` routes a field to `ConditionWidget` by NAME CONVENTION
* (`visible` / `hidden` / `disabled` / `visibleOn` / `condition` / `predicate` /
* `*When`), so ONE widget serves every metadata type. It claimed no scope, so
* (`visible` / `hidden` / `disabled` / `condition` / `predicate` / `*When`),
* so ONE widget serves every metadata type. It claimed no scope, so
* every one of those fields fell through to `celAuthoring`'s own
* `hint.scope ?? 'flattened'` default and a bare `status == 'done'` linted
* CLEAN — including on the action and hook tiers, where the runtime binds the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ describe('SchemaForm — collapsible sections', () => {
expect(screen.getByText('Open Field')).toBeInTheDocument();
});

it('hides a section whose visibleOn predicate is false (object/CEL form)', () => {
it('hides a section whose visibleWhen predicate is false (object/CEL form)', () => {
const condForm = {
type: 'simple' as const,
sections: [
{ label: 'Basics', fields: [{ field: 'openField' }] },
{
label: 'Layout',
visibleOn: { dialect: 'cel', source: "data.type != 'list'" },
visibleWhen: { dialect: 'cel', source: "data.type != 'list'" },
fields: [{ field: 'advancedField' }],
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ afterEach(cleanup);

/**
* Flat (non-sectioned) schema-driven rendering must honor per-property
* `visibleOn`, so a create form driven by a bare JSONSchema (no FormView
* layout) can gate a field on a sibling value. This is what lets the View
* `visibleWhen` (the retired `visibleOn` spelling is not read, objectui#12696),
* so a create form driven by a bare JSONSchema (no FormView layout) can gate a
* field on a sibling value. This is what lets the View
* create form show the list layout picker only for `viewKind: 'list'` and the
* form layout picker only for `viewKind: 'form'` (objectui#2323).
*/
Expand All @@ -23,18 +24,18 @@ const schema = {
type: 'string',
title: 'List layout',
enum: ['grid', 'kanban'],
visibleOn: "data.viewKind == 'list'",
visibleWhen: "data.viewKind == 'list'",
},
formType: {
type: 'string',
title: 'Form layout',
enum: ['simple', 'tabbed'],
visibleOn: "data.viewKind == 'form'",
visibleWhen: "data.viewKind == 'form'",
},
},
};

describe('SchemaForm flat path — per-property visibleOn (objectui#2323)', () => {
describe('SchemaForm flat path — per-property visibleWhen (objectui#2323)', () => {
it('shows the list layout picker and hides the form one for a list draft', () => {
render(<SchemaForm schema={schema} value={{ viewKind: 'list' }} onChange={() => {}} />);
expect(screen.getByText('View family')).toBeInTheDocument();
Expand All @@ -49,7 +50,7 @@ describe('SchemaForm flat path — per-property visibleOn (objectui#2323)', () =
expect(screen.queryByText('List layout')).not.toBeInTheDocument();
});

it('fails open — a field with no visibleOn always renders', () => {
it('fails open — a field with no visibleWhen always renders', () => {
render(<SchemaForm schema={schema} value={{}} onChange={() => {}} />);
// `viewKind` has no predicate, so it shows even before a family is picked.
expect(screen.getByText('View family')).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ afterEach(cleanup);
* ADR-0047 follow-up — page inspector section visibility for `list` pages.
*
* Framework PR #1817 marks the page form's "Data Context" (object/variables)
* and "Layout" (regions) sections with `visibleOn: "data.type != 'list'"`,
* and "Layout" (regions) sections with `visibleOn: "data.type != 'list'"` (now
* `visibleWhen`, see the note below),
* and the "Interface (list pages)" section with `data.type == 'list'`. For a
* `type: 'list'` page the data-view sections must hide and the Interface
* section must show.
Expand All @@ -21,13 +22,11 @@ afterEach(cleanup);
* report's "still shows Data Context + Layout" symptom turned out to be a
* stale backend serving the pre-#1817 form.)
*
* NOTE (objectstack#6331): the `visibleOn` spelling below is the DEPRECATED
* ADR-0089 alias, so this file pins the legacy limb, not the shape a current
* backend serves — the spec normaliser rewrites the alias and ships
* `visibleWhen`. Because these fixtures spell the alias they stayed green while
* the reader was alias-only and every spec-served predicate was dead. The
* canonical key (and the bundled `pageForm` itself) is pinned in
* `SchemaForm.visibleWhen.test.tsx`.
* NOTE (objectstack#6331, objectui#12696): PR #1817 spelled these predicates
* `visibleOn`, the ADR-0089 alias that `@objectstack/spec` 18 retires and that
* `SchemaForm` no longer reads. The fixtures below spell the canonical
* `visibleWhen`, the shape a current backend serves. The bundled `pageForm`
* itself is pinned in `SchemaForm.visibleWhen.test.tsx`.
*/

// Minimal JSONSchema mirroring the page item fields the form references.
Expand All @@ -50,12 +49,12 @@ const pageForm = {
{ label: 'Basics', fields: [{ field: 'name' }, { field: 'type' }] },
{
label: 'Data Context',
visibleOn: "data.type != 'list'",
visibleWhen: "data.type != 'list'",
fields: [{ field: 'object' }, { field: 'variables' }],
},
{
label: 'Layout',
visibleOn: "data.type != 'list'",
visibleWhen: "data.type != 'list'",
fields: [{ field: 'regions' }],
},
{
Expand All @@ -64,7 +63,7 @@ const pageForm = {
// exercise the same predicate path the backend serves.
collapsible: true,
collapsed: false,
visibleOn: { dialect: 'cel', source: "data.type == 'list'" },
visibleWhen: { dialect: 'cel', source: "data.type == 'list'" },
fields: [{ field: 'interfaceConfig' }],
},
],
Expand Down
43 changes: 21 additions & 22 deletions packages/app-shell/src/views/metadata-admin/SchemaForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -953,10 +953,10 @@ export function faceLabelling(face: FieldFace): WidgetLabelling {
}
}

const CONDITION_FIELD_NAMES = new Set(['visible', 'hidden', 'disabled', 'visibleOn', 'condition', 'predicate']);
const CONDITION_FIELD_NAMES = new Set(['visible', 'hidden', 'disabled', 'condition', 'predicate']);
/**
* Detect a CEL predicate field by NAME CONVENTION (`visible` / `hidden` /
* `disabled` / `visibleOn` / `condition` / `*When`) so it renders the no-code
* `disabled` / `condition` / `*When`) so it renders the no-code
* condition builder instead of a raw expression text box. No enum, and the
* schema must admit a string — {@link admitsString} holds that veto and
* documents why it looks through nested unions, why an empty schema is not
Expand Down Expand Up @@ -1052,28 +1052,27 @@ function resolveFieldWidget({
}

/**
* Read a visibility predicate off a spec node — **canonical key first**.
*
* ADR-0089 renamed the FormView predicate `visibleOn` → `visibleWhen`, and the
* spec's normaliser REWRITES the alias instead of keeping both: a parsed
* `FormView` carries `visibleWhen` and no `visibleOn` at all
* (`@objectstack/spec` `shared/visibility.ts`). Reading only `visibleOn` — what
* this admin engine did until `7a197e7c5` — therefore found `undefined` on
* every spec-served form and short-circuited each predicate to "always
* visible", so conditional fields/sections rendered unconditionally.
*
* Spelling and precedence mirror the runtime record-form adapter
* (`@object-ui/plugin-form` `sectionFields.ts`: `fd.visibleWhen ?? fd.visibleOn`)
* so metadata-admin and the runtime form speak ONE dialect. The deprecated
* alias stays honoured because it still has live producers: hand-written
* layouts, and this app's own create schemas, which set `visibleOn` directly on
* raw JSONSchema properties (`view-create-body.ts`) — those never pass through
* the spec normaliser.
* Read a visibility predicate off a spec node: `visibleWhen`, and only that.
*
* ADR-0089 renamed the FormView predicate `visibleOn` → `visibleWhen`, and
* `@objectstack/spec` 18 retires the old spelling. A parsed `FormView` carries
* `visibleWhen` and no `visibleOn` at all (`@objectstack/spec`
* `shared/visibility.ts`). Reading only `visibleOn`, which this admin engine did
* until `7a197e7c5`, found `undefined` on every spec-served form and
* short-circuited each predicate to "always visible".
*
* The retired key is NOT read as a fallback (objectui#12696). Its last in-app
* producer, the view anchor's create schema (`anchors.ts`), writes `visibleWhen`
* now, and every authoring form the bundled spec ships carries `visibleWhen`
* only. A layout that still spells `visibleOn` is shown unconditionally here,
* exactly as the runtime form (`@object-ui/plugin-form` `sectionFields.ts`) and
* the console form page treat it, so the designer preview cannot pass a
* predicate that nothing else honours. Rename the key to `visibleWhen`.
*/
function readVisibility(
node: { visibleWhen?: VisibilityPredicate; visibleOn?: VisibilityPredicate } | undefined | null,
node: { visibleWhen?: VisibilityPredicate } | undefined | null,
): VisibilityPredicate | undefined {
return node?.visibleWhen ?? node?.visibleOn;
return node?.visibleWhen;
}

export interface SchemaFormIssue {
Expand All @@ -1099,7 +1098,7 @@ export interface SchemaFormProps {
*/
idPath?: string;
/**
* Optional FormView layout (sections, tabs, widget hints, visibleOn)
* Optional FormView layout (sections, tabs, widget hints, visibleWhen)
* shipped by the framework alongside `schema`. When present, fields
* are grouped into sections and visibility predicates are honoured.
*/
Expand Down
Loading
Loading