diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DefaultValue.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DefaultValue.tsx index b1d347c4acfd9..34fd7c0d8e40b 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DefaultValue.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DefaultValue.tsx @@ -17,7 +17,12 @@ * under the License. */ import React, { FC, useEffect, useState } from 'react'; -import { Behavior, SetDataMaskHook, SuperChart } from '@superset-ui/core'; +import { + Behavior, + SetDataMaskHook, + SuperChart, + AppSection, +} from '@superset-ui/core'; import { FormInstance } from 'antd/lib/form'; import Loading from 'src/components/Loading'; import { NativeFiltersForm } from '../types'; @@ -56,6 +61,7 @@ const DefaultValue: FC = ({ ( - defaultValue ?? [], - ); + const forceFirstValue = + appSection === AppSection.FILTER_CONFIG_MODAL && defaultToFirstItem; + const groupby = ensureIsArray(formData.groupby); + // Correct initial value for Ant Select + const initSelectValue: SelectValue = + // `defaultValue` can be `FIRST_VALUE` if `defaultToFirstItem` is checked, so need convert it to correct value for Select + defaultValue === FIRST_VALUE ? [] : defaultValue ?? []; + + const firstItem: SelectValue = data[0] + ? (groupby.map(col => data[0][col]) as string[]) ?? initSelectValue + : initSelectValue; + + // If we are in config modal we always need show empty select for `defaultToFirstItem` + const [values, setValues] = useState( + defaultToFirstItem && appSection !== AppSection.FILTER_CONFIG_MODAL + ? firstItem + : initSelectValue, + ); const [col] = groupby; const datatype: GenericDataType = coltypeMap[col]; @@ -64,26 +82,36 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { timeFormatter: smartDateDetailedFormatter, }); - const handleChange = ( - value?: (number | string)[] | number | string | null, - ) => { - const resultValue: (number | string)[] = ensureIsArray( + const handleChange = (value?: SelectValue | number | string) => { + let selectValue: (number | string)[] = ensureIsArray( value, ); - setValues(resultValue); + let stateValue: SelectValue | typeof FIRST_VALUE = selectValue.length + ? selectValue + : null; + + if (value === FIRST_VALUE) { + selectValue = forceFirstValue ? [] : firstItem; + stateValue = FIRST_VALUE; + } + + setValues(selectValue); const emptyFilter = - enableEmptyFilter && !inverseSelection && resultValue?.length === 0; + enableEmptyFilter && !inverseSelection && selectValue?.length === 0; const dataMask = { extraFormData: getSelectExtraFormData( col, - resultValue, + selectValue, emptyFilter, inverseSelection, ), currentState: { - value: resultValue.length ? resultValue : null, + // We need to save in state `FIRST_VALUE` as some const and not as REAL value, + // because when FiltersBar check if all filters initialized it compares `defaultValue` with this value + // and because REAL value can be unpredictable for users that have different data for same dashboard we use `FIRST_VALUE` + value: stateValue, }, }; @@ -100,18 +128,23 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { }; useEffect(() => { - handleChange(currentValue ?? []); + // For currentValue we need set always `FIRST_VALUE` only if we in config modal for `defaultToFirstItem` mode + handleChange(forceFirstValue ? FIRST_VALUE : currentValue ?? []); }, [ JSON.stringify(currentValue), + defaultToFirstItem, multiSelect, enableEmptyFilter, inverseSelection, ]); useEffect(() => { - handleChange(defaultValue ?? []); + // If we have `defaultToFirstItem` mode it means that default value always `FIRST_VALUE` + handleChange(defaultToFirstItem ? FIRST_VALUE : defaultValue); }, [ JSON.stringify(defaultValue), + JSON.stringify(firstItem), + defaultToFirstItem, multiSelect, enableEmptyFilter, inverseSelection, @@ -127,6 +160,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { allowClear // @ts-ignore value={values} + disabled={forceFirstValue} showSearch={showSearch} mode={multiSelect ? 'multiple' : undefined} placeholder={placeholderText} diff --git a/superset-frontend/src/filters/components/Select/controlPanel.ts b/superset-frontend/src/filters/components/Select/controlPanel.ts index eddcd291ac452..c4a560b8baed4 100644 --- a/superset-frontend/src/filters/components/Select/controlPanel.ts +++ b/superset-frontend/src/filters/components/Select/controlPanel.ts @@ -24,6 +24,7 @@ const { enableEmptyFilter, inverseSelection, multiSelect, + defaultToFirstItem, sortAscending, } = DEFAULT_FORM_DATA; @@ -79,6 +80,19 @@ const config: ControlPanelConfig = { }, }, ], + [ + { + name: 'defaultToFirstItem', + config: { + type: 'CheckboxControl', + label: t('Default to first item'), + default: defaultToFirstItem, + resetConfig: true, + renderTrigger: true, + description: t('Select first item by default'), + }, + }, + ], [ { name: 'inverseSelection', diff --git a/superset-frontend/src/filters/components/Select/transformProps.ts b/superset-frontend/src/filters/components/Select/transformProps.ts index 59262b3eed529..4bf27d008b426 100644 --- a/superset-frontend/src/filters/components/Select/transformProps.ts +++ b/superset-frontend/src/filters/components/Select/transformProps.ts @@ -22,7 +22,15 @@ import { DEFAULT_FORM_DATA, PluginFilterSelectChartProps } from './types'; export default function transformProps( chartProps: PluginFilterSelectChartProps, ) { - const { formData, height, hooks, queriesData, width, behaviors } = chartProps; + const { + formData, + height, + hooks, + queriesData, + width, + behaviors, + appSection, + } = chartProps; const newFormData = { ...DEFAULT_FORM_DATA, ...formData }; const { setDataMask = () => {} } = hooks; const [queryData] = queriesData; @@ -34,6 +42,7 @@ export default function transformProps( return { coltypeMap, + appSection, width, behaviors, height, diff --git a/superset-frontend/src/filters/components/Select/types.ts b/superset-frontend/src/filters/components/Select/types.ts index ddd2c60e83e9e..8542e988fa579 100644 --- a/superset-frontend/src/filters/components/Select/types.ts +++ b/superset-frontend/src/filters/components/Select/types.ts @@ -17,6 +17,7 @@ * under the License. */ import { + AppSection, ChartProps, Behavior, DataRecord, @@ -28,12 +29,16 @@ import { import { RefObject } from 'react'; import { PluginFilterStylesProps } from '../types'; +export const FIRST_VALUE = '__FIRST_VALUE__'; +export type SelectValue = (number | string)[] | null; + interface PluginFilterSelectCustomizeProps { - defaultValue?: (string | number)[] | null; - currentValue?: (string | number)[] | null; + defaultValue?: SelectValue | typeof FIRST_VALUE; + currentValue?: SelectValue | typeof FIRST_VALUE; enableEmptyFilter: boolean; inverseSelection: boolean; multiSelect: boolean; + defaultToFirstItem: boolean; inputRef?: RefObject; sortAscending: boolean; } @@ -51,6 +56,7 @@ export type PluginFilterSelectProps = PluginFilterStylesProps & { data: DataRecord[]; setDataMask: SetDataMaskHook; behaviors: Behavior[]; + appSection: AppSection; formData: PluginFilterSelectQueryFormData; }; @@ -59,6 +65,7 @@ export const DEFAULT_FORM_DATA: PluginFilterSelectCustomizeProps = { currentValue: null, enableEmptyFilter: false, inverseSelection: false, + defaultToFirstItem: false, multiSelect: true, sortAscending: true, };