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
23 changes: 23 additions & 0 deletions .changeset/cascade-select-dependent-values.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@object-ui/components': patch
---

fix(form): thread live `dependentValues` to cascading option fields (#2284/#1583)

The form renderer only injected the live form record into data-source widgets
(`lookup`/`master_detail`/… — the `DATA_SOURCE_FIELD_TYPES` set). Registered
option widgets (`field:select`/`field:radio`/`field:multiselect`) that carry
per-option `visibleWhen` + `dependsOn` cascading were **excluded**, so
`stripRegisteredFieldProps` dropped `dependentValues` before it reached
`SelectField`. With no live record and no `formValues` context fallback, a
cascading `select` never saw its controlling field: in a create form the
dependent field stayed permanently gated on the "Select the parent first" hint
even after the parent was chosen (reproduced on the showcase `showcase_cascade`
B3 fixture — country → province never unlocked).

Option field types now receive `dependentValues` too, so the widget's
`dependsOn` gate lifts and its `visibleWhen` set re-filters live as the parent
changes — the same channel the lookup fix (#2215/#2216) already used. Regression
guard added in `form-dependent-values.test.tsx` (drives the registered
`field:select` path, not just the builtin `case 'select'` fallback the prior
cascading-select test covered).
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ beforeAll(async () => {
// Override the protocol placeholder for `field:lookup` with the probe
// (the fields package owns the real widget; components tests never load it).
ComponentRegistry.register('field:lookup', DependentValuesProbe, { namespace: 'test' });
// Cascading `select`/`radio`/`multiselect` options resolve their per-option
// `visibleWhen` + `dependsOn` gate from the SAME live `dependentValues` channel
// (#2284/#1583) — regression guard for the injection reaching option fields.
ComponentRegistry.register('field:select', DependentValuesProbe, { namespace: 'test' });
}, 30000);

function renderForm(fields: any[]) {
Expand Down Expand Up @@ -66,4 +70,38 @@ describe('form renderer — dependentValues injection for data-source fields (#2
});
});
});

it('feeds live form values to a cascading select as the user edits the parent (#2284)', async () => {
renderForm([
{ name: 'country', label: 'Country', type: 'input', defaultValue: '' },
{
name: 'province',
label: 'Province',
// `widget: 'field:select'` forces the REGISTERED SelectField path (what
// ObjectForm uses in the real console) rather than the builtin `case
// 'select'` fallback — the registered path is where the injection was
// missing, leaving a cascading select gated forever in create mode.
type: 'select',
widget: 'field:select',
dependsOn: 'country',
options: [
{ label: 'Zhejiang', value: 'zj', visibleWhen: "record.country == 'cn'" },
{ label: 'California', value: 'ca', visibleWhen: "record.country == 'us'" },
],
},
]);

// The probe stands in for SelectField; it must receive the live record, not
// an undefined/empty snapshot, so its `dependsOn` gate + `visibleWhen`
// filter can resolve against the chosen parent.
expect(JSON.parse(screen.getByTestId('dep-probe').textContent!)).not.toBeNull();

fireEvent.change(screen.getByLabelText(/country/i), { target: { value: 'cn' } });
await waitFor(() => {
expect(JSON.parse(screen.getByTestId('dep-probe').textContent!)).toMatchObject({
country: 'cn',
});
});
});

});
9 changes: 9 additions & 0 deletions packages/components/src/renderers/form/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ const DATA_SOURCE_FIELD_TYPES = new Set([
'object-ref', 'filter-condition', 'recipient-picker',
]);

// Option fields (`select`/`radio`/`multiselect`) support per-option `visibleWhen`
// cascading + `dependsOn` gating (#2284/#1583): the widget re-filters its OFFERED
// set against the live form record (`record.country == 'cn'` …). They take no
// `dataSource`, but they DO need the live `dependentValues` record — without it a
// cascading select never sees its controlling field and stays permanently gated
// on the "select the parent first" hint.
const CASCADE_OPTION_FIELD_TYPES = new Set(['select', 'radio', 'multiselect']);

function stripRendererOnlyProps<T extends Record<string, any>>(props: T): T {
const {
dataSource: _dataSource,
Expand Down Expand Up @@ -200,6 +208,7 @@ function stripRegisteredFieldProps(type: string, props: RenderFieldProps): Rende
return {
...fieldProps,
...(DATA_SOURCE_FIELD_TYPES.has(normalizedType) ? { dataSource, dependentValues } : {}),
...(CASCADE_OPTION_FIELD_TYPES.has(normalizedType) ? { dependentValues } : {}),
};
}

Expand Down
Loading