diff --git a/.changeset/cascade-select-dependent-values.md b/.changeset/cascade-select-dependent-values.md new file mode 100644 index 000000000..85a964628 --- /dev/null +++ b/.changeset/cascade-select-dependent-values.md @@ -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). diff --git a/packages/components/src/renderers/form/__tests__/form-dependent-values.test.tsx b/packages/components/src/renderers/form/__tests__/form-dependent-values.test.tsx index bddafa048..0346107ad 100644 --- a/packages/components/src/renderers/form/__tests__/form-dependent-values.test.tsx +++ b/packages/components/src/renderers/form/__tests__/form-dependent-values.test.tsx @@ -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[]) { @@ -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', + }); + }); + }); + }); diff --git a/packages/components/src/renderers/form/form.tsx b/packages/components/src/renderers/form/form.tsx index 10e68bd16..bc0cc5296 100644 --- a/packages/components/src/renderers/form/form.tsx +++ b/packages/components/src/renderers/form/form.tsx @@ -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>(props: T): T { const { dataSource: _dataSource, @@ -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 } : {}), }; }