diff --git a/.changeset/fields-depends-on-declared-read-6153.md b/.changeset/fields-depends-on-declared-read-6153.md new file mode 100644 index 0000000000..5e476f7a5d --- /dev/null +++ b/.changeset/fields-depends-on-declared-read-6153.md @@ -0,0 +1,14 @@ +--- +'@object-ui/fields': patch +--- + +The option widgets and the lookup read `dependsOn` through the declared type. + +`SelectField`, `MultiSelectField`, `RadioField` and `CheckboxesField` now read the +cascade key as `field.dependsOn` — `BaseFieldMetadata.dependsOn` — instead of +through an `as any`; `LookupField` reads both of its spellings (`depends_on`, then +`dependsOn`) through `LookupFieldMetadata`. Behaviour is unchanged: a select whose +metadata carries `dependsOn` still gates and prunes its options, a lookup still +scopes its candidate queries, and the metadata key still wins over the `dependsOn` +widget prop. What changed is that a wrong spelling or shape at the read site is now +a compile error rather than a silent no-op. objectui#6153. diff --git a/.changeset/types-field-depends-on-declared-6153.md b/.changeset/types-field-depends-on-declared-6153.md new file mode 100644 index 0000000000..c4bd2db508 --- /dev/null +++ b/.changeset/types-field-depends-on-declared-6153.md @@ -0,0 +1,20 @@ +--- +'@object-ui/types': minor +--- + +`dependsOn` is now a declared member of the field-metadata face. + +`BaseFieldMetadata` gains `dependsOn?: FieldDependsOn`, the spec's field-level +cascade key in the spec's own shape — derived from `@objectstack/spec/data`'s +`Field` by reference: an array of controlling field names, or `{ field, param }` +entries. Every field type inherits it, so an annotated `SelectFieldMetadata` or +`LookupFieldMetadata` literal can now carry the key the running widgets have +honoured all along; before, the excess-property check refused it and the widgets +reached it through an `as any`. A bare parent name is refused at the type, as the +spec refuses it at publish (`invalid_type`) — that shape belongs to the form-level +`FormField.dependsOn` and to the `dependsOn` widget prop. `FieldDependsOn` is +exported. + +The snake_case `depends_on` stays declared for now: it is objectui's legacy twin, +never a spec key, and retires on its own card (objectui#7357). Maintainer ruling A +on objectui#6153. diff --git a/content/docs/fields/lookup.mdx b/content/docs/fields/lookup.mdx index 3a4f711059..16f6f176f3 100644 --- a/content/docs/fields/lookup.mdx +++ b/content/docs/fields/lookup.mdx @@ -77,6 +77,33 @@ and must never reach authored object metadata `dataSource` a host injects and the `onCreateNew` callback it passes are widget props, not metadata. +A **dependent lookup** scopes its candidates by a sibling field's value, declared +with `dependsOn` — the same `BaseFieldMetadata` member the select widgets gate on +([objectui#6153](https://github.com/objectstack-ai/objectui/issues/6153)), in the +spec's field-level shape: an array of controlling field names, or `{ field, param }` +entries when the remote filter parameter differs from the local field name. While +any controlling value is empty the trigger is gated ("Select account first"); once +set, every candidate query — the typeahead popover, the Record Picker and the +people picker — carries the chain as a hard `$filter` no user input can override. + +```ts +import type { LookupFieldMetadata } from '@object-ui/types'; + +const contact: LookupFieldMetadata = { + type: 'lookup', + name: 'contact', + label: 'Contact', + reference_to: 'contacts', + // Filter `contacts` by `account_id` equal to the form's current `account`. + dependsOn: [{ field: 'account', param: 'account_id' }], +}; +``` + +The snake_case `depends_on` is objectui's legacy twin of the same key: still read +by this widget, never a spec key, and retiring under +[objectui#7357](https://github.com/objectstack-ai/objectui/issues/7357) — author +`dependsOn`. + The value being edited, and the `className` / `disabled` a host supplies, are **not** metadata keys — they are runtime widget props. See [Field Widget Props](/docs/fields/widget-props). diff --git a/content/docs/fields/select.mdx b/content/docs/fields/select.mdx index a2d194d86c..e2cd9bb763 100644 --- a/content/docs/fields/select.mdx +++ b/content/docs/fields/select.mdx @@ -84,7 +84,7 @@ cascade-clear propagate down the chain. `visibleWhen` options are for **small, static dictionaries** (category → subcategory, a handful of provinces). When the data is large, changes over time, or is shared across forms (real country/province/city tables, org units, product -catalogs), model each level as a **`lookup`** with `depends_on` instead — the +catalogs), model each level as a **`lookup`** with `dependsOn` instead — the candidate query is filtered server-side and paginated. See [Lookup Field](/docs/fields/lookup). @@ -116,11 +116,38 @@ const status: SelectFieldMetadata = { }; ``` -Cascading option lists are driven by a sibling field's value. The widget reads a -camelCase `dependsOn` off the metadata, but no exported metadata type declares it — -`BaseFieldMetadata` declares the snake_case `depends_on` instead — so the two -spellings disagree and the gap is tracked as -[objectui#6153](https://github.com/objectstack-ai/objectui/issues/6153). +Cascading option lists are driven by a sibling field's value, declared with +`dependsOn` — a `BaseFieldMetadata` member every field type inherits +([objectui#6153](https://github.com/objectstack-ai/objectui/issues/6153)), in the +shape `@objectstack/spec` declares at field level: an **array** of controlling +field names, or `{ field, param }` entries when the remote parameter name differs +from the local field name. While any controlling value is empty the widget is +gated; once it is set, each option's `visibleWhen` decides whether it is offered, +and a selection the parent no longer offers is cleared. + +```ts +import type { SelectFieldMetadata } from '@object-ui/types'; + +const province: SelectFieldMetadata = { + type: 'select', + name: 'province', + label: 'Province', + dependsOn: ['country'], + options: [ + { label: 'Zhejiang', value: 'zj', visibleWhen: "record.country == 'cn'" }, + { label: 'California', value: 'ca', visibleWhen: "record.country == 'us'" }, + ], +}; +``` + +A bare parent name (`dependsOn: "country"`, as in the form schema above) is the +**form-level** shape, `FormField.dependsOn`; on field metadata the spec accepts +only the array, and `SelectFieldMetadata` refuses the string for the same reason. +The metadata key wins over the `dependsOn` widget prop a host may pass. The +snake_case `depends_on` is objectui's legacy twin of the same key — read only by +the lookup widget and retiring under +[objectui#7357](https://github.com/objectstack-ai/objectui/issues/7357); author +`dependsOn`. The value being edited, and the `className` / `disabled` a host supplies, are **not** metadata keys — they are runtime widget props. See [Field Widget Props](/docs/fields/widget-props). diff --git a/packages/fields/src/widgets/CheckboxesField.tsx b/packages/fields/src/widgets/CheckboxesField.tsx index 9eb37001a0..ae076c0125 100644 --- a/packages/fields/src/widgets/CheckboxesField.tsx +++ b/packages/fields/src/widgets/CheckboxesField.tsx @@ -40,7 +40,8 @@ export function CheckboxesField({ const groupId = useId(); const fieldName = props.name || config?.name || props.id || ''; - const dependsOn = config?.dependsOn ?? dependsOnProp; + // Read through the declared type, not the untyped carrier (objectui#6153) — see SelectField. + const dependsOn = field?.dependsOn ?? dependsOnProp; const { options, gated, dependsOnFields } = useCascadingOptions