From 7909095ff3463653fdd4801a1492b00573d55d16 Mon Sep 17 00:00:00 2001 From: endimonan <65144790+endimonan@users.noreply.github.com> Date: Tue, 17 Mar 2026 01:11:53 +0100 Subject: [PATCH 1/3] feat(native-filters): add configurable LIKE/ILIKE operators to Select filter (#38470) Co-authored-by: Evan Rusackas Co-authored-by: Richard Fogaca Nienkotter <63572350+richardfogaca@users.noreply.github.com> --- .../src/query/types/Operator.ts | 1 + .../FiltersConfigForm/FiltersConfigForm.tsx | 111 ++++- .../FiltersConfigForm/getControlItemsMap.tsx | 3 +- .../Select/SelectFilterPlugin.test.tsx | 413 +++++++++++++++++- .../components/Select/SelectFilterPlugin.tsx | 152 +++++-- .../components/Select/buildQuery.test.ts | 94 +++- .../components/Select/controlPanel.test.ts | 43 ++ .../filters/components/Select/controlPanel.ts | 75 +++- .../components/Select/transformProps.ts | 11 +- .../src/filters/components/Select/types.ts | 9 + superset-frontend/src/filters/utils.ts | 45 +- 11 files changed, 907 insertions(+), 50 deletions(-) create mode 100644 superset-frontend/src/filters/components/Select/controlPanel.test.ts diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Operator.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Operator.ts index 0d2cb5b59ba7..98695555068e 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/types/Operator.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/types/Operator.ts @@ -30,6 +30,7 @@ const BINARY_OPERATORS = [ '<=', 'ILIKE', 'LIKE', + 'NOT ILIKE', 'NOT LIKE', 'REGEX', 'TEMPORAL_RANGE', 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 ab6a9f22e695..cc53dcbcb616 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx @@ -45,12 +45,16 @@ import { useEffect, useImperativeHandle, useMemo, + useRef, useState, RefObject, memo, } from 'react'; import rison from 'rison'; -import { PluginFilterSelectCustomizeProps } from 'src/filters/components/Select/types'; +import { + PluginFilterSelectCustomizeProps, + SelectFilterOperatorType, +} from 'src/filters/components/Select/types'; import { useSelector } from 'react-redux'; import { getChartDataRequest } from 'src/components/Chart/chartAction'; import { @@ -624,6 +628,49 @@ 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( () => @@ -685,6 +732,7 @@ const FiltersConfigForm = ( 'columns.filterable', 'columns.is_dttm', 'columns.type', + 'columns.type_generic', 'columns.verbose_name', 'database.id', 'database.database_name', @@ -1501,6 +1549,67 @@ const FiltersConfigForm = ( hidden initialValue={null} /> + {!isChartCustomization && + itemTypeField === 'filter_select' && ( + + {t('Match type')} +   + + + } + > + (parentRef?.current as HTMLElement) || document.body - : (trigger: HTMLElement) => - (trigger?.parentNode as HTMLElement) || document.body - } - showSearch={showSearch} - mode={multiSelect ? 'multiple' : 'single'} - placeholder={placeholderText} - onClear={() => onSearch('')} - onSearch={onSearch} - onBlur={handleBlur} - onFocus={setFocusedFilter} - onMouseEnter={setHoveredFilter} - onMouseLeave={unsetHoveredFilter} - // @ts-expect-error - onChange={handleChange} - ref={inputRef} - loading={isRefreshing} - oneLine={filterBarOrientation === FilterBarOrientation.Horizontal} - invertSelection={inverseSelection && excludeFilterValues} - options={options} - sortComparator={sortComparator} - onOpenChange={setFilterActive} - className="select-container" - /> + {isLikeOperator ? ( + + ) : ( +