This is an automated email from the ASF dual-hosted git repository.
simchashats pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 1fc0852 feat(native-filters): Support default to first value in
select filter (#14869)
1fc0852 is described below
commit 1fc08523af97f8a36a0b18d643a3fe60df815ca4
Author: simcha90 <[email protected]>
AuthorDate: Mon Jun 7 13:41:19 2021 +0300
feat(native-filters): Support default to first value in select filter
(#14869)
* fix:fix get permission function
* feat: add async filters support
* revert: revert ff
* feat: add async filters support
* fix: merge with master
* fix: remove tests
* lint: fix lint
* fix: fix CR notes
* fix: fix with master
* test: fix tests
* refactor: update logic for default first value
* fix: get requiredFirst
* fix: support instant
* docs: update text
* docs: fix comments
* docs: update texts
---
.../DashboardBuilder/DashboardBuilder.tsx | 50 +++-----
.../dashboard/components/DashboardBuilder/state.ts | 93 +++++++++++++++
.../FilterBar/FilterControls/FilterValue.tsx | 2 +-
.../components/nativeFilters/FilterBar/index.tsx | 21 ++--
.../components/nativeFilters/FilterBar/state.ts | 6 +
.../FiltersConfigForm/FiltersConfigForm.tsx | 9 +-
.../FiltersConfigForm/getControlItemsMap.tsx | 100 +++++++++-------
.../nativeFilters/FiltersConfigModal/types.ts | 3 +
.../nativeFilters/FiltersConfigModal/utils.ts | 3 +
.../dashboard/components/nativeFilters/types.ts | 1 +
superset-frontend/src/dataMask/reducer.ts | 2 +-
.../components/Select/SelectFilterPlugin.test.tsx | 16 +++
.../components/Select/SelectFilterPlugin.tsx | 127 ++++++++++++---------
.../src/filters/components/Select/controlPanel.ts | 5 +-
.../src/filters/components/Select/types.ts | 2 +-
15 files changed, 296 insertions(+), 144 deletions(-)
diff --git
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
index 807fe46..9873dfb 100644
---
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
+++
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
@@ -18,7 +18,7 @@
*/
/* eslint-env browser */
import cx from 'classnames';
-import React, { FC, useEffect, useState } from 'react';
+import React, { FC } from 'react';
import { Sticky, StickyContainer } from 'react-sticky';
import { JsonObject, styled } from '@superset-ui/core';
import ErrorBoundary from 'src/components/ErrorBoundary';
@@ -30,7 +30,6 @@ import DashboardComponent from
'src/dashboard/containers/DashboardComponent';
import ToastPresenter from 'src/messageToasts/containers/ToastPresenter';
import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
import getDirectPathToTabIndex from
'src/dashboard/util/getDirectPathToTabIndex';
-import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { URL_PARAMS } from 'src/constants';
import { useDispatch, useSelector } from 'react-redux';
import { getUrlParam } from 'src/utils/urlUtils';
@@ -47,11 +46,11 @@ import {
DashboardStandaloneMode,
} from 'src/dashboard/util/constants';
import FilterBar from 'src/dashboard/components/nativeFilters/FilterBar';
+import Loading from 'src/components/Loading';
import { StickyVerticalBar } from '../StickyVerticalBar';
import { shouldFocusTabs, getRootLevelTabsComponent } from './utils';
-import { useFilters } from '../nativeFilters/FilterBar/state';
-import { Filter } from '../nativeFilters/types';
import DashboardContainer from './DashboardContainer';
+import { useNativeFilters } from './state';
const TABS_HEIGHT = 47;
const HEADER_HEIGHT = 67;
@@ -99,12 +98,6 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
const dashboardLayout = useSelector<RootState, DashboardLayout>(
state => state.dashboardLayout.present,
);
- const showNativeFilters = useSelector<RootState, boolean>(
- state => state.dashboardInfo.metadata?.show_native_filters,
- );
- const canEdit = useSelector<RootState, boolean>(
- ({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
- );
const editMode = useSelector<RootState, boolean>(
state => state.dashboardState.editMode,
);
@@ -112,22 +105,6 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
state => state.dashboardState.directPathToChild,
);
- const filters = useFilters();
- const filterValues = Object.values<Filter>(filters);
-
- const nativeFiltersEnabled =
- showNativeFilters &&
- isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) &&
- (canEdit || (!canEdit && filterValues.length !== 0));
-
- const [dashboardFiltersOpen, setDashboardFiltersOpen] = useState(
- getUrlParam(URL_PARAMS.showFilters) ?? true,
- );
-
- const toggleDashboardFiltersOpen = (visible?: boolean) => {
- setDashboardFiltersOpen(visible ?? !dashboardFiltersOpen);
- };
-
const handleChangeTab = ({
pathToTabIndex,
}: {
@@ -161,15 +138,12 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () =>
{
(hideDashboardHeader ? 0 : HEADER_HEIGHT) +
(topLevelTabs ? TABS_HEIGHT : 0);
- useEffect(() => {
- if (
- filterValues.length === 0 &&
- dashboardFiltersOpen &&
- nativeFiltersEnabled
- ) {
- toggleDashboardFiltersOpen(false);
- }
- }, [filterValues.length]);
+ const {
+ showDashboard,
+ dashboardFiltersOpen,
+ toggleDashboardFiltersOpen,
+ nativeFiltersEnabled,
+ } = useNativeFilters();
return (
<StickyContainer
@@ -245,7 +219,11 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
</ErrorBoundary>
</StickyVerticalBar>
)}
- <DashboardContainer topLevelTabs={topLevelTabs} />
+ {showDashboard ? (
+ <DashboardContainer topLevelTabs={topLevelTabs} />
+ ) : (
+ <Loading />
+ )}
{editMode && <BuilderComponentPane topOffset={barTopOffset} />}
</StyledDashboardContent>
<ToastPresenter />
diff --git
a/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts
b/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts
new file mode 100644
index 0000000..874525d
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts
@@ -0,0 +1,93 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useSelector } from 'react-redux';
+import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
+import { useEffect, useState } from 'react';
+import { URL_PARAMS } from 'src/constants';
+import { getUrlParam } from 'src/utils/urlUtils';
+import { RootState } from 'src/dashboard/types';
+import {
+ useFilters,
+ useNativeFiltersDataMask,
+} from '../nativeFilters/FilterBar/state';
+import { Filter } from '../nativeFilters/types';
+
+// eslint-disable-next-line import/prefer-default-export
+export const useNativeFilters = () => {
+ const [isInitialized, setIsInitialized] = useState(false);
+ const [dashboardFiltersOpen, setDashboardFiltersOpen] = useState(
+ getUrlParam(URL_PARAMS.showFilters) ?? true,
+ );
+ const showNativeFilters = useSelector<RootState, boolean>(
+ state => state.dashboardInfo.metadata?.show_native_filters,
+ );
+ const canEdit = useSelector<RootState, boolean>(
+ ({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
+ );
+
+ const filters = useFilters();
+ const filterValues = Object.values<Filter>(filters);
+
+ const nativeFiltersEnabled =
+ showNativeFilters &&
+ isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) &&
+ (canEdit || (!canEdit && filterValues.length !== 0));
+
+ const requiredFirstFilter = filterValues.filter(
+ ({ requiredFirst }) => requiredFirst,
+ );
+ const dataMask = useNativeFiltersDataMask();
+ const showDashboard =
+ isInitialized ||
+ !nativeFiltersEnabled ||
+ !(
+ nativeFiltersEnabled &&
+ requiredFirstFilter.length &&
+ requiredFirstFilter.find(
+ ({ id }) => dataMask[id]?.filterState?.value === undefined,
+ )
+ );
+
+ const toggleDashboardFiltersOpen = (visible?: boolean) => {
+ setDashboardFiltersOpen(visible ?? !dashboardFiltersOpen);
+ };
+
+ useEffect(() => {
+ if (
+ filterValues.length === 0 &&
+ dashboardFiltersOpen &&
+ nativeFiltersEnabled
+ ) {
+ toggleDashboardFiltersOpen(false);
+ }
+ }, [filterValues.length]);
+
+ useEffect(() => {
+ if (showDashboard) {
+ setIsInitialized(true);
+ }
+ }, [showDashboard]);
+
+ return {
+ showDashboard,
+ dashboardFiltersOpen,
+ toggleDashboardFiltersOpen,
+ nativeFiltersEnabled,
+ };
+};
diff --git
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
index 495f50f..bb07131 100644
---
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
+++
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
@@ -74,7 +74,7 @@ const FilterValue: React.FC<FilterProps> = ({
const { name: groupby } = column;
const hasDataSource = !!datasetId;
const [isLoading, setIsLoading] = useState<boolean>(hasDataSource);
- const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
+ const [isRefreshing, setIsRefreshing] = useState<boolean>(true);
const dispatch = useDispatch();
useEffect(() => {
const newFormData = getFormData({
diff --git
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx
index 44a4f82..631179b 100644
---
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx
+++
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx
@@ -18,7 +18,7 @@
*/
/* eslint-disable no-param-reassign */
-import { HandlerFunction, styled, t } from '@superset-ui/core';
+import { DataMask, HandlerFunction, styled, t } from '@superset-ui/core';
import React, { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import cx from 'classnames';
@@ -26,11 +26,7 @@ import Icon from 'src/components/Icon';
import { Tabs } from 'src/common/components';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { updateDataMask } from 'src/dataMask/actions';
-import {
- DataMaskState,
- DataMaskStateWithId,
- DataMaskWithId,
-} from 'src/dataMask/types';
+import { DataMaskStateWithId, DataMaskWithId } from 'src/dataMask/types';
import { useImmer } from 'use-immer';
import { areObjectsEqual } from 'src/reduxUtils';
import { testWithId } from 'src/utils/testUtils';
@@ -178,10 +174,21 @@ const FilterBar: React.FC<FiltersBarProps> = ({
const handleFilterSelectionChange = (
filter: Pick<Filter, 'id'> & Partial<Filter>,
- dataMask: Partial<DataMaskState>,
+ dataMask: Partial<DataMask>,
) => {
setIsFilterSetChanged(tab !== TabIds.AllFilters);
setDataMaskSelected(draft => {
+ // force instant updating on initialization for filters with
`requiredFirst` is true or instant filters
+ if (
+ (dataMaskSelected[filter.id] && filter.isInstant) ||
+ // filterState.value === undefined - means that value not initialized
+ (dataMask.filterState?.value !== undefined &&
+ dataMaskSelected[filter.id]?.filterState?.value === undefined &&
+ filter.requiredFirst)
+ ) {
+ dispatch(updateDataMask(filter.id, dataMask));
+ }
+
draft[filter.id] = {
...(getInitialDataMask(filter.id) as DataMaskWithId),
...dataMask,
diff --git
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts
index 8edf714..aa4894f 100644
---
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts
+++
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts
@@ -76,6 +76,7 @@ export const useFilterUpdates = (
// Load filters after charts loaded
export const useInitialization = () => {
const [isInitialized, setIsInitialized] = useState<boolean>(false);
+ const filters = useFilters();
const charts = useSelector<RootState, ChartsState>(state => state.charts);
// We need to know how much charts now shown on dashboard to know how many
of all charts should be loaded
@@ -90,6 +91,11 @@ export const useInitialization = () => {
return;
}
+ if (Object.values(filters).find(({ requiredFirst }) => requiredFirst)) {
+ setIsInitialized(true);
+ return;
+ }
+
// For some dashboards may be there are no charts on first page,
// so we check up to 1 sec if there is at least on chart to load
let filterTimeout: NodeJS.Timeout;
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 76fa33b..296ee8e 100644
---
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
+++
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
@@ -443,6 +443,7 @@ const FiltersConfigForm = (
filterId,
filterType: formFilter.filterType,
filterToEdit,
+ formFilter,
})
: {};
@@ -592,6 +593,7 @@ const FiltersConfigForm = (
expandIconPosition="right"
>
<Collapse.Panel
+ forceRender
header={FilterPanels.basic.name}
key={FilterPanels.basic.key}
>
@@ -625,7 +627,11 @@ const FiltersConfigForm = (
{
validator: (rule, value) => {
const hasValue = !!value.filterState?.value;
- if (hasValue) {
+ if (
+ hasValue ||
+ // TODO: do more generic
+ formFilter.controlValues?.defaultToFirstItem
+ ) {
return Promise.resolve();
}
return Promise.reject(
@@ -673,6 +679,7 @@ const FiltersConfigForm = (
</Collapse.Panel>
{((hasDataset && hasAdditionalFilters) || hasMetrics) && (
<Collapse.Panel
+ forceRender
header={FilterPanels.advanced.name}
key={FilterPanels.advanced.key}
>
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 15e877e..097d80b 100644
---
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
+++
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
@@ -23,10 +23,11 @@ import {
import React from 'react';
import { Checkbox } from 'src/common/components';
import { FormInstance } from 'antd/lib/form';
-import { getChartControlPanelRegistry, t } from '@superset-ui/core';
+import { getChartControlPanelRegistry, styled, t } from '@superset-ui/core';
import { Tooltip } from 'src/components/Tooltip';
+import { FormItem } from 'src/components/Form';
import { getControlItems, setNativeFilterFieldValues } from './utils';
-import { NativeFiltersForm } from '../types';
+import { NativeFiltersForm, NativeFiltersFormItem } from '../types';
import { StyledRowFormItem } from './FiltersConfigForm';
import { Filter } from '../../types';
@@ -37,8 +38,13 @@ export interface ControlItemsProps {
filterId: string;
filterType: string;
filterToEdit?: Filter;
+ formFilter?: NativeFiltersFormItem;
}
+const CleanFormItem = styled(FormItem)`
+ margin-bottom: 0;
+`;
+
export default function getControlItemsMap({
disabled,
forceUpdate,
@@ -46,6 +52,7 @@ export default function getControlItemsMap({
filterId,
filterType,
filterToEdit,
+ formFilter,
}: ControlItemsProps) {
const controlPanelRegistry = getChartControlPanelRegistry();
const controlItems =
@@ -66,46 +73,61 @@ export default function getControlItemsMap({
filterToEdit?.controlValues?.[controlItem.name] ??
controlItem?.config?.default;
const element = (
- <Tooltip
- key={controlItem.name}
- placement="left"
- title={
- controlItem.config.affectsDataMask &&
- disabled &&
- t('Populate "Default value" to enable this control')
- }
- >
- <StyledRowFormItem
+ <>
+ <CleanFormItem
+ name={['filters', filterId, 'requiredFirst', controlItem.name]}
+ hidden
+ initialValue={
+ controlItem?.config?.requiredFirst && filterToEdit?.requiredFirst
+ }
+ />
+ <Tooltip
key={controlItem.name}
- name={['filters', filterId, 'controlValues', controlItem.name]}
- initialValue={initialValue}
- valuePropName="checked"
- colon={false}
+ placement="left"
+ title={
+ controlItem.config.affectsDataMask &&
+ disabled &&
+ t('Populate "Default value" to enable this control')
+ }
>
- <Checkbox
- disabled={controlItem.config.affectsDataMask && disabled}
- onChange={() => {
- if (!controlItem.config.resetConfig) {
- forceUpdate();
- return;
- }
- setNativeFilterFieldValues(form, filterId, {
- defaultDataMask: null,
- });
- forceUpdate();
- }}
+ <StyledRowFormItem
+ key={controlItem.name}
+ name={['filters', filterId, 'controlValues', controlItem.name]}
+ initialValue={initialValue}
+ valuePropName="checked"
+ colon={false}
>
- {controlItem.config.label}{' '}
- {controlItem.config.description && (
- <InfoTooltipWithTrigger
- placement="top"
- label={controlItem.config.name}
- tooltip={controlItem.config.description}
- />
- )}
- </Checkbox>
- </StyledRowFormItem>
- </Tooltip>
+ <Checkbox
+ disabled={controlItem.config.affectsDataMask && disabled}
+ onChange={({ target: { checked } }) => {
+ if (controlItem.config.requiredFirst) {
+ setNativeFilterFieldValues(form, filterId, {
+ requiredFirst: {
+ ...formFilter?.requiredFirst,
+ [controlItem.name]: checked,
+ },
+ });
+ }
+ if (controlItem.config.resetConfig) {
+ setNativeFilterFieldValues(form, filterId, {
+ defaultDataMask: null,
+ });
+ }
+ forceUpdate();
+ }}
+ >
+ {controlItem.config.label}{' '}
+ {controlItem.config.description && (
+ <InfoTooltipWithTrigger
+ placement="top"
+ label={controlItem.config.name}
+ tooltip={controlItem.config.description}
+ />
+ )}
+ </Checkbox>
+ </StyledRowFormItem>
+ </Tooltip>
+ </>
);
map[controlItem.name] = { element, checked: initialValue };
});
diff --git
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts
index 60051e7..0ba091f 100644
---
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts
+++
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts
@@ -31,6 +31,9 @@ export interface NativeFiltersFormItem {
controlValues: {
[key: string]: any;
};
+ requiredFirst: {
+ [key: string]: boolean;
+ };
defaultValue: any;
defaultDataMask: DataMask;
parentFilter: {
diff --git
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/utils.ts
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/utils.ts
index 852d4e1..b218ad4 100644
---
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/utils.ts
+++
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/utils.ts
@@ -140,6 +140,9 @@ export const createHandleSave = (
adhoc_filters: formInputs.adhoc_filters,
time_range: formInputs.time_range,
controlValues: formInputs.controlValues ?? {},
+ requiredFirst: Object.values(formInputs.requiredFirst ?? {}).find(
+ rf => rf,
+ ),
name: formInputs.name,
filterType: formInputs.filterType,
// for now there will only ever be one target
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/types.ts
b/superset-frontend/src/dashboard/components/nativeFilters/types.ts
index ac772dc..a1e206b 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/types.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/types.ts
@@ -56,6 +56,7 @@ export interface Filter {
sortMetric?: string | null;
adhoc_filters?: AdhocFilter[];
time_range?: string;
+ requiredFirst?: boolean;
tabsInScope?: string[];
chartsInScope?: number[];
}
diff --git a/superset-frontend/src/dataMask/reducer.ts
b/superset-frontend/src/dataMask/reducer.ts
index a0b0e38..275787e 100644
--- a/superset-frontend/src/dataMask/reducer.ts
+++ b/superset-frontend/src/dataMask/reducer.ts
@@ -49,7 +49,7 @@ export function getInitialDataMask(id: string):
DataMaskWithId {
...otherProps,
extraFormData: {},
filterState: {
- value: null,
+ value: undefined,
},
ownState: {},
} as DataMaskWithId;
diff --git
a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx
b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx
index c42c1ce..70cb434 100644
---
a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx
+++
b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx
@@ -88,6 +88,7 @@ describe('SelectFilterPlugin', () => {
it('Add multiple values with first render', () => {
getWrapper();
expect(setDataMask).toHaveBeenCalledWith({
+ extraFormData: {},
filterState: {
value: ['boy'],
},
@@ -98,6 +99,9 @@ describe('SelectFilterPlugin', () => {
},
});
expect(setDataMask).toHaveBeenCalledWith({
+ __cache: {
+ value: ['boy'],
+ },
extraFormData: {
filters: [
{
@@ -120,6 +124,9 @@ describe('SelectFilterPlugin', () => {
userEvent.click(screen.getByRole('combobox'));
userEvent.click(screen.getByTitle('girl'));
expect(setDataMask).toHaveBeenCalledWith({
+ __cache: {
+ value: ['boy'],
+ },
extraFormData: {
filters: [
{
@@ -146,6 +153,9 @@ describe('SelectFilterPlugin', () => {
getWrapper();
userEvent.click(document.querySelector('[data-icon="close"]')!);
expect(setDataMask).toHaveBeenCalledWith({
+ __cache: {
+ value: ['boy'],
+ },
extraFormData: {
adhoc_filters: [
{
@@ -171,6 +181,9 @@ describe('SelectFilterPlugin', () => {
getWrapper({ enableEmptyFilter: false });
userEvent.click(document.querySelector('[data-icon="close"]')!);
expect(setDataMask).toHaveBeenCalledWith({
+ __cache: {
+ value: ['boy'],
+ },
extraFormData: {},
filterState: {
label: '',
@@ -189,6 +202,9 @@ describe('SelectFilterPlugin', () => {
userEvent.click(screen.getByRole('combobox'));
userEvent.click(screen.getByTitle('girl'));
expect(setDataMask).toHaveBeenCalledWith({
+ __cache: {
+ value: ['boy'],
+ },
extraFormData: {
filters: [
{
diff --git
a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
index 2f7d565..4afdc1d 100644
--- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
+++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
+/* eslint-disable no-param-reassign */
import {
AppSection,
DataMask,
@@ -28,16 +29,11 @@ import {
t,
tn,
} from '@superset-ui/core';
-import React, {
- useCallback,
- useEffect,
- useMemo,
- useReducer,
- useState,
-} from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Select } from 'src/common/components';
import debounce from 'lodash/debounce';
import { SLOW_DEBOUNCE } from 'src/constants';
+import { useImmerReducer } from 'use-immer';
import Icons from 'src/components/Icons';
import { PluginFilterSelectProps, SelectValue } from './types';
import { StyledSelect, Styles } from '../common';
@@ -49,41 +45,33 @@ type DataMaskAction =
| { type: 'ownState'; ownState: JsonObject }
| {
type: 'filterState';
+ __cache: JsonObject;
extraFormData: ExtraFormData;
filterState: { value: SelectValue; label?: string };
};
-function reducer(state: DataMask, action: DataMaskAction): DataMask {
+function reducer(
+ draft: Required<DataMask> & { __cache?: JsonObject },
+ action: DataMaskAction,
+) {
switch (action.type) {
case 'ownState':
- return {
- ...state,
- ownState: {
- ...(state.ownState || {}),
- ...action.ownState,
- },
+ draft.ownState = {
+ ...draft.ownState,
+ ...action.ownState,
};
+ return draft;
case 'filterState':
- return {
- ...state,
- extraFormData: action.extraFormData,
- filterState: {
- ...(state.filterState || {}),
- ...action.filterState,
- },
- };
+ draft.extraFormData = action.extraFormData;
+ // eslint-disable-next-line no-underscore-dangle
+ draft.__cache = action.__cache;
+ draft.filterState = { ...draft.filterState, ...action.filterState };
+ return draft;
default:
- return {
- ...state,
- };
+ return draft;
}
}
-type DataMaskReducer = (
- prevState: DataMask,
- action: DataMaskAction,
-) => DataMask;
-
export default function PluginFilterSelect(props: PluginFilterSelectProps) {
const {
coltypeMap,
@@ -127,32 +115,49 @@ export default function PluginFilterSelect(props:
PluginFilterSelectProps) {
}, [col, selectedValues, data]);
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const [currentSuggestionSearch, setCurrentSuggestionSearch] = useState('');
- const [dataMask, dispatchDataMask] = useReducer<DataMaskReducer>(reducer, {
+ const [dataMask, dispatchDataMask] = useImmerReducer(reducer, {
+ extraFormData: {},
filterState,
ownState: {
coltypeMap,
},
});
- const updateDataMask = (values: SelectValue) => {
- const emptyFilter =
- enableEmptyFilter && !inverseSelection && !values?.length;
- const suffix =
- inverseSelection && values?.length ? ` (${t('excluded')})` : '';
+ const updateDataMask = useCallback(
+ (values: SelectValue) => {
+ const emptyFilter =
+ enableEmptyFilter && !inverseSelection && !values?.length;
- dispatchDataMask({
- type: 'filterState',
- extraFormData: getSelectExtraFormData(
- col,
- values,
- emptyFilter,
- inverseSelection,
- ),
- filterState: {
- value: values,
- label: `${(values || []).join(', ')}${suffix}`,
- },
- });
- };
+ const suffix =
+ inverseSelection && values?.length ? ` (${t('excluded')})` : '';
+
+ dispatchDataMask({
+ type: 'filterState',
+ __cache: filterState,
+ extraFormData: getSelectExtraFormData(
+ col,
+ values,
+ emptyFilter,
+ inverseSelection,
+ ),
+ filterState: {
+ label: `${(values || []).join(', ')}${suffix}`,
+ value:
+ appSection === AppSection.FILTER_CONFIG_MODAL && defaultToFirstItem
+ ? undefined
+ : values,
+ },
+ });
+ },
+ [
+ appSection,
+ col,
+ defaultToFirstItem,
+ dispatchDataMask,
+ enableEmptyFilter,
+ inverseSelection,
+ JSON.stringify(filterState),
+ ],
+ );
useEffect(() => {
if (!isDropdownVisible) {
@@ -216,15 +221,19 @@ export default function PluginFilterSelect(props:
PluginFilterSelectProps) {
};
useEffect(() => {
- const firstItem: SelectValue = data[0]
- ? (groupby.map(col => data[0][col]) as string[])
- : null;
- if (isDisabled) {
+ if (defaultToFirstItem && filterState.value === undefined) {
+ // initialize to first value if set to default to first item
+ const firstItem: SelectValue = data[0]
+ ? (groupby.map(col => data[0][col]) as string[])
+ : null;
+ // firstItem[0] !== undefined for a case when groupby changed but new
data still not fetched
+ // TODO: still need repopulate default value in config modal when column
changed
+ if (firstItem && firstItem[0] !== undefined) {
+ updateDataMask(firstItem);
+ }
+ } else if (isDisabled) {
// empty selection if filter is disabled
updateDataMask(null);
- } else if (!isDisabled && defaultToFirstItem && firstItem) {
- // initialize to first value if set to default to first item
- updateDataMask(firstItem);
} else {
// reset data mask based on filter state
updateDataMask(filterState.value);
@@ -235,6 +244,10 @@ export default function PluginFilterSelect(props:
PluginFilterSelectProps) {
defaultToFirstItem,
enableEmptyFilter,
inverseSelection,
+ updateDataMask,
+ data,
+ groupby,
+ JSON.stringify(filterState),
]);
useEffect(() => {
diff --git a/superset-frontend/src/filters/components/Select/controlPanel.ts
b/superset-frontend/src/filters/components/Select/controlPanel.ts
index 335e444..74891b0 100644
--- a/superset-frontend/src/filters/components/Select/controlPanel.ts
+++ b/superset-frontend/src/filters/components/Select/controlPanel.ts
@@ -93,7 +93,10 @@ const config: ControlPanelConfig = {
resetConfig: true,
affectsDataMask: true,
renderTrigger: true,
- description: t('Select first item by default'),
+ requiredFirst: true,
+ description: t(
+ 'Select first item by default (when using this option, default
value can’t be set)',
+ ),
},
},
],
diff --git a/superset-frontend/src/filters/components/Select/types.ts
b/superset-frontend/src/filters/components/Select/types.ts
index aac5aa9..36052e8 100644
--- a/superset-frontend/src/filters/components/Select/types.ts
+++ b/superset-frontend/src/filters/components/Select/types.ts
@@ -29,7 +29,7 @@ import {
import { RefObject } from 'react';
import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
-export type SelectValue = (number | string)[] | null;
+export type SelectValue = (number | string)[] | null | undefined;
interface PluginFilterSelectCustomizeProps {
defaultValue?: SelectValue;