From 3596459f200e2c3110c7b9d8decb81c414e6ca43 Mon Sep 17 00:00:00 2001 From: ashah65 <137852504+ashah65@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:52:35 -0400 Subject: [PATCH 1/3] feat(native-filters): make filter dependency support extensible via plugin registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace hardcoded ALLOW_DEPENDENCIES list with a registry-based check so any filter plugin registering with Behavior.NativeFilter automatically supports the cascade dependency feature — no core changes needed for new plugins. Also adds isColumnSelect support in getControlItemsMap to allow plugins to declare dataset column picker controls. --- .../FiltersConfigForm/FiltersConfigForm.tsx | 269 ++---------------- .../FiltersConfigForm/getControlItemsMap.tsx | 172 ++++++++--- .../FiltersConfigModal/FiltersConfigModal.tsx | 22 +- .../FiltersConfigModal/hooks/index.ts | 5 +- .../hooks/useFilterOperations.ts | 22 +- 5 files changed, 186 insertions(+), 304 deletions(-) diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx index c59fec2483b5..244fd566ce7a 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx @@ -45,16 +45,12 @@ import { useEffect, useImperativeHandle, useMemo, - useRef, useState, RefObject, memo, } from 'react'; import rison from 'rison'; -import { - PluginFilterSelectCustomizeProps, - SelectFilterOperatorType, -} from 'src/filters/components/Select/types'; +import { PluginFilterSelectCustomizeProps } from 'src/filters/components/Select/types'; import { useSelector } from 'react-redux'; import { getChartDataRequest } from 'src/components/Chart/chartAction'; import { @@ -93,7 +89,7 @@ import { } from 'src/dashboard/components/nativeFilters/utils'; import { DatasetSelectLabel } from 'src/features/datasets/DatasetSelectLabel'; import { - ALLOW_DEPENDENCIES as TYPES_SUPPORT_DEPENDENCIES, + filterSupportsDependencies, getFiltersConfigModalTestId, } from '../FiltersConfigModal'; import { FilterRemoval, NativeFiltersForm } from '../types'; @@ -107,7 +103,6 @@ import RemovedFilter from './RemovedFilter'; import { useBackendFormUpdate, useDefaultValue } from './state'; import { hasTemporalColumns, - getTimeGrainOptions, isValidFilterValue, mostUsedDataset, setNativeFilterFieldValues, @@ -120,7 +115,6 @@ import { INPUT_WIDTH, } from './constants'; import DependencyList from './DependencyList'; -import { datasetLabel } from 'src/features/semanticLayers/label'; const FORM_ITEM_WIDTH = 260; @@ -264,12 +258,6 @@ export interface FiltersConfigFormProps { const FILTERS_WITH_ADHOC_FILTERS = ['filter_select', 'filter_range']; -const getOptionDataTest = ( - prefix: string, - value: string | number | undefined, -) => - `${prefix}-${String(value ?? 'undefined').replace(/[^a-zA-Z0-9_-]/g, '-')}`; - // TODO: Rename the filter plugins and remove this mapping const FILTER_TYPE_NAME_MAPPING = { [t('Select filter')]: t('Value'), @@ -326,12 +314,6 @@ const FiltersConfigForm = ( const filters = form.getFieldValue('filters'); const formValues = filters?.[filterId]; const formFilter = formValues || undoFormValues || defaultFormFilter; - const formFilterWithTimeGrains = formFilter as typeof formFilter & { - time_grains?: string[]; - }; - const filterToEditWithTimeGrains = filterToEdit as - | (Filter & { time_grains?: string[] }) - | undefined; const handleModifyFilter = useCallback(() => { if (onModifyFilter) { @@ -454,7 +436,7 @@ const FiltersConfigForm = ( formFilter?.filterType, ); - const canDependOnOtherFilters = TYPES_SUPPORT_DEPENDENCIES.includes( + const canDependOnOtherFilters = filterSupportsDependencies( formFilter?.filterType, ); @@ -593,11 +575,6 @@ const FiltersConfigForm = ( !!filterToEdit?.adhoc_filters?.length || !!filterToEdit?.time_range; - const hasTimeGrainPreFilter = !!( - formFilterWithTimeGrains?.time_grains?.length || - filterToEditWithTimeGrains?.time_grains?.length - ); - const hasEnableSingleValue = formFilter?.controlValues?.enableSingleValue !== undefined || filterToEdit?.controlValues?.enableSingleValue !== undefined; @@ -647,49 +624,6 @@ const FiltersConfigForm = ( forceUpdate(); }; - const currentOperatorType: SelectFilterOperatorType = - formFilter?.controlValues?.operatorType ?? - filterToEdit?.controlValues?.operatorType ?? - SelectFilterOperatorType.Exact; - - const selectedColumnIsString = useMemo(() => { - const columnName = formFilter?.column; - if (!columnName || !datasetDetails?.columns) return true; - const colMeta = datasetDetails.columns.find( - (c: { column_name: string }) => c.column_name === columnName, - ); - if (!colMeta) return true; - return colMeta.type_generic === GenericDataType.String; - }, [formFilter?.column, datasetDetails?.columns]); - - const onOperatorTypeChanged = (value: SelectFilterOperatorType) => { - const previous = form.getFieldValue('filters')?.[filterId].controlValues; - setNativeFilterFieldValues(form, filterId, { - controlValues: { - ...previous, - operatorType: value, - }, - defaultDataMask: null, - }); - formChanged(); - forceUpdate(); - }; - - const prevColumnRef = useRef(formFilter?.column); - const datasetLoaded = !!datasetDetails?.columns; - useEffect(() => { - const columnChanged = prevColumnRef.current !== formFilter?.column; - if ( - (columnChanged || datasetLoaded) && - !selectedColumnIsString && - currentOperatorType !== SelectFilterOperatorType.Exact - ) { - onOperatorTypeChanged(SelectFilterOperatorType.Exact); - } - prevColumnRef.current = formFilter?.column; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [formFilter?.column, selectedColumnIsString, datasetLoaded]); - const validatePreFilter = () => setTimeout( () => @@ -751,7 +685,6 @@ const FiltersConfigForm = ( 'columns.filterable', 'columns.is_dttm', 'columns.type', - 'columns.type_generic', 'columns.verbose_name', 'database.id', 'database.database_name', @@ -765,7 +698,6 @@ const FiltersConfigForm = ( 'schema', 'sql', 'table_name', - 'time_grain_sqla', ], })}`, }) @@ -956,16 +888,6 @@ const FiltersConfigForm = ( label: name || pluginKey, }; })} - optionRender={option => ( - - {option.label || option.value} - - )} onChange={value => { setNativeFilterFieldValues(form, filterId, { filterType: value, @@ -1024,16 +946,6 @@ const FiltersConfigForm = ( disabled: isDisabled, }; })} - optionRender={option => ( - - {option.label || option.value} - - )} onChange={value => { setNativeFilterFieldValues(form, filterId, { filterType: value, @@ -1060,7 +972,7 @@ const FiltersConfigForm = ( {datasetLabel()}} + label={{t('Dataset')}} initialValue={ datasetDetails ? { @@ -1080,10 +992,7 @@ const FiltersConfigForm = ( rules={[ { required: !isRemoved, - message: - datasetLabel() === t('Datasource') - ? t('Datasource is required') - : t('Dataset is required'), + message: t('Dataset is required'), }, ]} {...getFiltersConfigModalTestId('datasource-input')} @@ -1109,7 +1018,7 @@ const FiltersConfigForm = ( ) : ( {datasetLabel()}} + label={{t('Dataset')}} > @@ -1297,78 +1206,6 @@ const FiltersConfigForm = ( )} - {itemTypeField === 'filter_timegrain' && - hasDataset && - datasetDetails?.time_grain_sqla && - datasetDetails.time_grain_sqla.length > 0 && ( - - { - if (!checked) { - setNativeFilterFieldValues( - form, - filterId, - { time_grains: undefined }, - ); - forceUpdate(); - } - formChanged(); - }} - > - - { - onOperatorTypeChanged( - value as SelectFilterOperatorType, - ); - }} - /> - - )} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx index 7b8f95777230..94d2ff491746 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx @@ -17,11 +17,14 @@ * under the License. */ import { CustomControlItem } from '@superset-ui/chart-controls'; -import { ReactNode } from 'react'; +import { ReactNode, useState, useEffect } from 'react'; +import rison from 'rison'; +import { cachedSupersetGet } from 'src/utils/cachedSupersetGet'; import { Checkbox, FormItem, InfoTooltip, + Select, Tooltip, type FormInstance, } from '@superset-ui/core/components'; @@ -64,6 +67,87 @@ const CleanFormItem = styled(FormItem)` margin-bottom: 0; `; +/** Resolves the saved or default initial value for a control. */ +function resolveInitialValue( + controlItem: CustomControlItem, + filterToEdit?: ControlItemsProps['filterToEdit'], + customizationToEdit?: ControlItemsProps['customizationToEdit'], +) { + return ( + filterToEdit?.controlValues?.[controlItem.name] ?? + customizationToEdit?.controlValues?.[controlItem.name] ?? + controlItem?.config?.default ?? + null + ); +} + +/** Renders a StyledLabel with an optional description tooltip. */ +function ControlLabel({ + label, + description, +}: { + label?: ReactNode; + description?: ReactNode; +}) { + return ( + + {label} + {description && ( + <> +   + + + )} + + ); +} + +function DatasetColumnSelect({ + datasetId, + value, + onChange, +}: { + datasetId?: number; + value?: string | null; + onChange?: (value: string | null) => void; +}) { + const [{ loadedForId, fetchedColumns }, setFetchState] = useState<{ + loadedForId?: number; + fetchedColumns: string[]; + }>({ fetchedColumns: [] }); + + const loading = !!(datasetId && loadedForId !== datasetId); + const options = loadedForId === datasetId ? fetchedColumns : []; + + useEffect(() => { + if (!datasetId) return; + cachedSupersetGet({ + endpoint: `/api/v1/dataset/${datasetId}?q=${rison.encode({ + columns: ['columns.column_name'], + })}`, + }).then(({ json: { result } }) => { + setFetchState({ + loadedForId: datasetId, + fetchedColumns: result.columns + .map((col: { column_name: string }) => col.column_name) + .filter(Boolean), + }); + }); + }, [datasetId]); + + return ( + 0} + onChange={(values: string[]) => { + setNativeFilterFieldValues( + form, + filterId, + { + time_grains: + values.length > 0 && + values.length < + datasetDetails + .time_grain_sqla.length + ? values + : undefined, + }, + ); + forceUpdate(); + formChanged(); + }} + css={{ width: INPUT_WIDTH }} + /> + + + + )} {itemTypeField !== 'filter_range' ? ( - {hasMetrics && ( + {hasMetrics && !isChartCustomization && ( { - if (value !== undefined) { - const previous = - form.getFieldValue( - 'filters', - )?.[filterId].controlValues || - {}; - setNativeFilterFieldValues( - form, - filterId, - { - controlValues: { - ...previous, - sortMetric: value, - }, + const previous = + form.getFieldValue( + 'filters', + )?.[filterId].controlValues || + {}; + setNativeFilterFieldValues( + form, + filterId, + { + controlValues: { + ...previous, + sortMetric: value, }, - ); - forceUpdate(); - } + }, + ); + forceUpdate(); formChanged(); }} /> @@ -1501,6 +1636,67 @@ const FiltersConfigForm = ( hidden initialValue={null} /> + {!isChartCustomization && + itemTypeField === 'filter_select' && ( + + {t('Match type')} +   + + + } + > + item?.config?.isColumnSelect === true) .forEach(controlItem => { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx index c54fd6fc2caf..cc0c2673289b 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx @@ -57,6 +57,7 @@ import { isFilterId, isChartCustomizationId, transformDividerId, + isDivider, } from './utils'; import { ConfigModalContent } from './ConfigModalContent'; import ConfigModalSidebar from './ConfigModalSidebar'; @@ -471,8 +472,16 @@ function FiltersConfigModal({ ); const handleValuesChange = useMemo( - () => debouncedHandleErroredItems, - [debouncedHandleErroredItems], + () => (changedValues: Partial) => { + const changedId = changedValues.filters + ? Object.keys(changedValues.filters)[0] + : undefined; + if (changedId && isDivider(changedId)) { + handleModifyItem(changedId); + } + debouncedHandleErroredItems(); + }, + [debouncedHandleErroredItems, handleModifyItem], ); const handleActiveFilterPanelChange = useCallback( From 627d19e32ca3ea3ad46b444769e0f3100edd47ef Mon Sep 17 00:00:00 2001 From: ashah65 <137852504+ashah65@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:06:05 -0400 Subject: [PATCH 3/3] fix(native-filters): fix null reference and tooltip ReactNode handling in FiltersConfigForm Add optional chaining on controlValues access to prevent TypeError when filter field value is undefined, and pass ReactNode descriptions directly to InfoTooltip to avoid String() coercion producing [object Object]. Co-Authored-By: Claude Sonnet 4.6 --- .../FiltersConfigForm/FiltersConfigForm.tsx | 8 ++++---- .../FiltersConfigForm/getControlItemsMap.tsx | 11 ++--------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx index 5cc087a44423..616a15048a85 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx @@ -620,7 +620,7 @@ const FiltersConfigForm = ( !mainControlItems.groupby; const onSortChanged = (value: boolean | undefined) => { - const previous = form.getFieldValue('filters')?.[filterId].controlValues; + const previous = form.getFieldValue('filters')?.[filterId]?.controlValues; setNativeFilterFieldValues(form, filterId, { controlValues: { ...previous, @@ -631,7 +631,7 @@ const FiltersConfigForm = ( }; const onEnableSingleValueChanged = (value: SingleValueType | undefined) => { - const previous = form.getFieldValue('filters')?.[filterId].controlValues; + const previous = form.getFieldValue('filters')?.[filterId]?.controlValues; setNativeFilterFieldValues(form, filterId, { controlValues: { ...previous, @@ -657,7 +657,7 @@ const FiltersConfigForm = ( }, [formFilter?.column, datasetDetails?.columns]); const onOperatorTypeChanged = (value: SelectFilterOperatorType) => { - const previous = form.getFieldValue('filters')?.[filterId].controlValues; + const previous = form.getFieldValue('filters')?.[filterId]?.controlValues; setNativeFilterFieldValues(form, filterId, { controlValues: { ...previous, @@ -1439,7 +1439,7 @@ const FiltersConfigForm = ( const previous = form.getFieldValue( 'filters', - )?.[filterId].controlValues || + )?.[filterId]?.controlValues || {}; setNativeFilterFieldValues( form, diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx index 518d887ef7cf..c0de1209d5dd 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx @@ -95,20 +95,13 @@ function ControlLabel({ typeof description === 'function' ? (description as () => ReactNode)() : description; - const tooltipText = - resolvedDescription != null && typeof resolvedDescription === 'string' - ? resolvedDescription - : resolvedDescription != null - ? String(resolvedDescription) - : undefined; - return ( {resolvedLabel} - {tooltipText && ( + {resolvedDescription != null && ( <>   - + )}