diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx index 03779ab960..098bd6fc63 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx @@ -18,7 +18,7 @@ */ /* eslint-disable no-param-reassign */ -import { styled, t } from '@superset-ui/core'; +import { HandlerFunction, styled, t } from '@superset-ui/core'; import React, { useState, useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import cx from 'classnames'; @@ -52,6 +52,11 @@ const BarWrapper = styled.div` `; const Bar = styled.div` + & .ant-typography-edit-content { + left: 0; + margin-top: 0; + width: 100%; + } position: absolute; top: 0; left: 0; @@ -169,6 +174,11 @@ interface FiltersBarProps { directPathToChild?: string[]; } +enum TabIds { + AllFilters = 'allFilters', + FilterSets = 'filterSets', +} + const FilterBar: React.FC = ({ filtersOpen, toggleFiltersBar, @@ -183,8 +193,10 @@ const FilterBar: React.FC = ({ const dispatch = useDispatch(); const filterSets = useFilterSets(); const filterSetFilterValues = Object.values(filterSets); + const [isFilterSetChanged, setIsFilterSetChanged] = useState(false); + const [tab, setTab] = useState(TabIds.AllFilters); const filters = useFilters(); - const filterValues = Object.values(filters); + const filterValues = Object.values(filters); const dataMaskApplied = useDataMask(); const canEdit = useSelector( ({ dashboardInfo }) => dashboardInfo.dash_edit_perm, @@ -212,8 +224,8 @@ const FilterBar: React.FC = ({ } const areFiltersInitialized = filterValues.every(filterValue => areObjectsEqual( - filterValue.defaultValue, - dataMaskSelected[filterValue.id]?.currentState?.value, + filterValue?.defaultValue, + dataMaskSelected[filterValue?.id]?.currentState?.value, ), ); if (areFiltersInitialized) { @@ -245,6 +257,7 @@ const FilterBar: React.FC = ({ filter: Pick & Partial, dataMask: Partial, ) => { + setIsFilterSetChanged(tab !== TabIds.AllFilters); setDataMaskSelected(draft => { const children = cascadeChildren[filter.id] || []; // force instant updating on initialization or for parent filters @@ -338,12 +351,13 @@ const FilterBar: React.FC = ({ {isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) ? ( {editFilterSetId && ( = ({ diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/EditSection.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/EditSection.tsx index 5994e6c48e..60f42a203d 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/EditSection.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/EditSection.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { FC, useMemo } from 'react'; +import React, { FC, useMemo, useState } from 'react'; import { HandlerFunction, styled, t } from '@superset-ui/core'; import { Typography, Tooltip } from 'src/common/components'; import { useDispatch } from 'react-redux'; @@ -25,8 +25,9 @@ import { setFilterSetsConfiguration } from 'src/dashboard/actions/nativeFilters' import { DataMaskUnit } from 'src/dataMask/types'; import { WarningOutlined } from '@ant-design/icons'; import { ActionButtons } from './Footer'; -import { useDataMask, useFilterSets } from '../state'; +import { useDataMask, useFilters, useFilterSets } from '../state'; import { APPLY_FILTERS_HINT, findExistingFilterSet } from './utils'; +import { useFilterSetNameDuplicated } from './state'; const Wrapper = styled.div` display: grid; @@ -73,13 +74,26 @@ const EditSection: FC = ({ const dataMaskApplied = useDataMask(); const dispatch = useDispatch(); const filterSets = useFilterSets(); + const filters = useFilters(); const filterSetFilterValues = Object.values(filterSets); + + const [filterSetName, setFilterSetName] = useState( + filterSets[filterSetId].name, + ); + + const isFilterSetNameDuplicated = useFilterSetNameDuplicated( + filterSetName, + filterSets[filterSetId].name, + ); + const handleSave = () => { dispatch( setFilterSetsConfiguration( filterSetFilterValues.map(filterSet => { const newFilterSet = { ...filterSet, + name: filterSetName, + nativeFilters: filters, dataMask: { nativeFilters: { ...dataMaskApplied } }, }; return filterSetId === filterSet.id ? newFilterSet : filterSet; @@ -92,20 +106,30 @@ const EditSection: FC = ({ const foundFilterSet = useMemo( () => findExistingFilterSet({ - dataMaskApplied, dataMaskSelected, filterSetFilterValues, }), - [dataMaskApplied, dataMaskSelected, filterSetFilterValues], + [dataMaskSelected, filterSetFilterValues], ); const isDuplicateFilterSet = foundFilterSet && foundFilterSet.id !== filterSetId; + const resultDisabled = + disabled || isDuplicateFilterSet || isFilterSetNameDuplicated; + return ( {t('Editing filter set:')} - {filterSets[filterSetId].name} + , + onChange: setFilterSetName, + }} + > + {filterSetName} + - + - + filterSetName, +}) => { + const isFilterSetNameDuplicated = useFilterSetNameDuplicated(filterSetName); + + const isCreateDisabled = + !filterSetName || isFilterSetNameDuplicated || disabled; + + return ( + <> + {editMode ? ( + + + + + + + + + ) : ( + - - ) : ( - - - - - - )} - -); + )} + + ); +}; export default Footer; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/state.ts new file mode 100644 index 0000000000..56790f7c8a --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/state.ts @@ -0,0 +1,37 @@ +/** + * 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 { useMemo } from 'react'; +import { useFilterSets } from '../state'; + +// eslint-disable-next-line import/prefer-default-export +export const useFilterSetNameDuplicated = ( + filterSetName: string, + ignoreName?: string, +) => { + const filterSets = useFilterSets(); + const filterSetFilterValues = Object.values(filterSets); + const isFilterSetNameDuplicated = useMemo( + () => !!filterSetFilterValues.find(({ name }) => name === filterSetName), + [filterSetFilterValues, filterSetName], + ); + if (ignoreName === filterSetName) { + return false; + } + return isFilterSetNameDuplicated; +}; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/utils.ts index e7a87ad45b..3f93cad23a 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/utils.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/utils.ts @@ -47,28 +47,22 @@ export const getFilterValueForDisplay = ( export const findExistingFilterSet = ({ filterSetFilterValues, - dataMaskApplied, dataMaskSelected, }: { filterSetFilterValues: FilterSet[]; - dataMaskApplied: DataMaskUnit; dataMaskSelected: DataMaskUnit; }) => - filterSetFilterValues.find(({ dataMask }) => { - if (dataMask?.nativeFilters) { - return Object.values(dataMask?.nativeFilters).every( - filterFromFilterSet => { - let currentValueFromFiltersTab = - dataMaskApplied[filterFromFilterSet.id]?.currentState ?? {}; - if (dataMaskSelected[filterFromFilterSet.id]) { - currentValueFromFiltersTab = - dataMaskSelected[filterFromFilterSet.id]?.currentState; - } - return areObjectsEqual( - filterFromFilterSet.currentState ?? {}, - currentValueFromFiltersTab, - ); - }, + filterSetFilterValues.find(({ dataMask: dataMaskFromFilterSet }) => { + if (dataMaskFromFilterSet?.nativeFilters) { + const dataMaskSelectedEntries = Object.entries(dataMaskSelected); + return dataMaskSelectedEntries.every( + ([id, filterFromSelectedFilters]) => + areObjectsEqual( + filterFromSelectedFilters.currentState, + dataMaskFromFilterSet?.nativeFilters?.[id]?.currentState, + ) && + dataMaskSelectedEntries.length === + Object.keys(dataMaskFromFilterSet?.nativeFilters ?? {}).length, ); } return false; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx index 0ec03f847f..9b5fb92079 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx @@ -70,7 +70,7 @@ const FilterValue: React.FC = ({ groupby, inputRef, }); - if (!areObjectsEqual(formData || {}, newFormData)) { + if (!areObjectsEqual(formData, newFormData)) { setFormData(newFormData); if (!hasDataSource) { return; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts index d0b5a5dc17..db51f30c5e 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts @@ -18,12 +18,12 @@ */ import { useSelector } from 'react-redux'; import { + Filters, FilterSets as FilterSetsType, NativeFiltersState, } from 'src/dashboard/reducers/types'; import { DataMaskUnitWithId } from 'src/dataMask/types'; import { mergeExtraFormData } from '../utils'; -import { Filter } from '../types'; export const useFilterSets = () => useSelector( @@ -31,7 +31,7 @@ export const useFilterSets = () => ); export const useFilters = () => - useSelector(state => state.nativeFilters.filters); + useSelector(state => state.nativeFilters.filters); export const useDataMask = () => useSelector(state => state.dataMask.nativeFilters); 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 8cdc1b9fd4..8c54a82854 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx @@ -267,7 +267,7 @@ export const FiltersConfigForm: React.FC = ({ /> diff --git a/superset-frontend/src/dashboard/reducers/types.ts b/superset-frontend/src/dashboard/reducers/types.ts index a3cd633f2f..535d36aaa0 100644 --- a/superset-frontend/src/dashboard/reducers/types.ts +++ b/superset-frontend/src/dashboard/reducers/types.ts @@ -70,6 +70,7 @@ export type LayoutItem = { export type FilterSet = { id: string; name: string; + nativeFilters: Filters; dataMask: Partial; }; diff --git a/superset-frontend/src/dataMask/actions.ts b/superset-frontend/src/dataMask/actions.ts index 667b6e7250..4340661949 100644 --- a/superset-frontend/src/dataMask/actions.ts +++ b/superset-frontend/src/dataMask/actions.ts @@ -16,16 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import { MaskWithId } from './types'; +import { DataMaskType, MaskWithId } from './types'; import { FilterConfiguration } from '../dashboard/components/nativeFilters/types'; export const UPDATE_DATA_MASK = 'UPDATE_DATA_MASK'; export interface UpdateDataMask { type: typeof UPDATE_DATA_MASK; filterId: string; - nativeFilters?: Omit; - crossFilters?: Omit; - ownFilters?: Omit; + [DataMaskType.NativeFilters]?: Omit; + [DataMaskType.CrossFilters]?: Omit; + [DataMaskType.OwnFilters]?: Omit; } export const SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE = diff --git a/superset-frontend/src/dataMask/reducer.ts b/superset-frontend/src/dataMask/reducer.ts index 261620f5e7..a027a12c35 100644 --- a/superset-frontend/src/dataMask/reducer.ts +++ b/superset-frontend/src/dataMask/reducer.ts @@ -20,7 +20,7 @@ /* eslint-disable no-param-reassign */ // <- When we work with Immer, we need reassign, so disabling lint import produce from 'immer'; -import { MaskWithId, DataMaskType, DataMaskStateWithId } from './types'; +import { MaskWithId, DataMaskType, DataMaskStateWithId, Mask } from './types'; import { AnyDataMaskAction, SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE, @@ -43,8 +43,7 @@ const setUnitDataMask = ( ) => { if (action[unitName]) { dataMaskState[unitName][action.filterId] = { - ...dataMaskState[unitName][action.filterId], - ...action[unitName], + ...(action[unitName] as Mask), id: action.filterId, }; } diff --git a/superset-frontend/src/reduxUtils.ts b/superset-frontend/src/reduxUtils.ts index b30fda70c7..9fb505bd18 100644 --- a/superset-frontend/src/reduxUtils.ts +++ b/superset-frontend/src/reduxUtils.ts @@ -169,9 +169,6 @@ export function areArraysShallowEqual(arr1: unknown[], arr2: unknown[]) { return true; } -export function areObjectsEqual( - obj1: Record, - obj2: Record, -) { +export function areObjectsEqual(obj1: any, obj2: any) { return isEqual(obj1, obj2); }