fix: Removing parent filter causes incorrect state of child filter (#16876)

* fix: Removing parent filter causes incorrect state of child filter

* removed to isRemoved

* Shows (deleted) when the parent is removed

* Removes unnecessary code
This commit is contained in:
Michael S. Molina 2021-09-29 13:04:07 -03:00 committed by GitHub
parent 3272d1c086
commit 4da4fe4136
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 105 additions and 56 deletions

View File

@ -37,6 +37,7 @@ describe('FilterScope', () => {
restoreFilter: jest.fn(), restoreFilter: jest.fn(),
parentFilters: [], parentFilters: [],
save, save,
removedFilters: {},
}; };
const MockModal = ({ scope }: { scope?: object }) => { const MockModal = ({ scope }: { scope?: object }) => {

View File

@ -44,7 +44,7 @@ import React, {
useState, useState,
} from 'react'; } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { isEqual } from 'lodash'; import { isEqual, isEmpty } from 'lodash';
import { FormItem } from 'src/components/Form'; import { FormItem } from 'src/components/Form';
import { Input } from 'src/common/components'; import { Input } from 'src/common/components';
import { Select } from 'src/components'; import { Select } from 'src/components';
@ -70,7 +70,7 @@ import {
} from 'src/dashboard/types'; } from 'src/dashboard/types';
import Loading from 'src/components/Loading'; import Loading from 'src/components/Loading';
import { ColumnSelect } from './ColumnSelect'; import { ColumnSelect } from './ColumnSelect';
import { NativeFiltersForm } from '../types'; import { NativeFiltersForm, FilterRemoval } from '../types';
import { import {
FILTER_SUPPORTED_TYPES, FILTER_SUPPORTED_TYPES,
hasTemporalColumns, hasTemporalColumns,
@ -264,7 +264,7 @@ const FilterPanels = {
export interface FiltersConfigFormProps { export interface FiltersConfigFormProps {
filterId: string; filterId: string;
filterToEdit?: Filter; filterToEdit?: Filter;
removed?: boolean; removedFilters: Record<string, FilterRemoval>;
restoreFilter: (filterId: string) => void; restoreFilter: (filterId: string) => void;
form: FormInstance<NativeFiltersForm>; form: FormInstance<NativeFiltersForm>;
parentFilters: { id: string; title: string }[]; parentFilters: { id: string; title: string }[];
@ -300,21 +300,22 @@ const FiltersConfigForm = (
{ {
filterId, filterId,
filterToEdit, filterToEdit,
removed, removedFilters,
restoreFilter, restoreFilter,
form, form,
parentFilters, parentFilters,
}: FiltersConfigFormProps, }: FiltersConfigFormProps,
ref: React.RefObject<any>, ref: React.RefObject<any>,
) => { ) => {
const isRemoved = !!removedFilters[filterId];
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const [metrics, setMetrics] = useState<Metric[]>([]); const [metrics, setMetrics] = useState<Metric[]>([]);
const [activeTabKey, setActiveTabKey] = useState<string>( const [activeTabKey, setActiveTabKey] = useState<string>(
FilterTabs.configuration.key, FilterTabs.configuration.key,
); );
const [activeFilterPanelKey, setActiveFilterPanelKey] = useState< const [activeFilterPanelKey, setActiveFilterPanelKey] = useState<
string | string[] string | string[] | undefined
>(FilterPanels.basic.key); >();
const [undoFormValues, setUndoFormValues] = useState<Record< const [undoFormValues, setUndoFormValues] = useState<Record<
string, string,
any any
@ -325,6 +326,24 @@ const FiltersConfigForm = (
const formValues = form.getFieldValue('filters')?.[filterId]; const formValues = form.getFieldValue('filters')?.[filterId];
const formFilter = formValues || undoFormValues || defaultFormFilter; const formFilter = formValues || undoFormValues || defaultFormFilter;
const parentFilterOptions = useMemo(
() =>
parentFilters.map(filter => ({
value: filter.id,
label: filter.title,
})),
[parentFilters],
);
const parentId =
formFilter?.parentFilter?.value || filterToEdit?.cascadeParentIds?.[0];
const parentFilter = parentFilterOptions.find(
({ value }) => value === parentId,
);
const hasParentFilter = !!parentFilter;
const nativeFilterItems = getChartMetadataRegistry().items; const nativeFilterItems = getChartMetadataRegistry().items;
const nativeFilterVizTypes = Object.entries(nativeFilterItems) const nativeFilterVizTypes = Object.entries(nativeFilterItems)
// @ts-ignore // @ts-ignore
@ -374,7 +393,7 @@ const FiltersConfigForm = (
filterType: formFilter?.filterType, filterType: formFilter?.filterType,
filterToEdit, filterToEdit,
formFilter, formFilter,
removed, removed: isRemoved,
}) })
: {}; : {};
const hasColumn = !!mainControlItems.groupby; const hasColumn = !!mainControlItems.groupby;
@ -506,19 +525,6 @@ const FiltersConfigForm = (
[filterId, form, formChanged], [filterId, form, formChanged],
); );
const parentFilterOptions = parentFilters.map(filter => ({
value: filter.id,
label: filter.title,
}));
const parentFilter = parentFilterOptions.find(
({ value }) =>
value === formFilter?.parentFilter?.value ||
value === filterToEdit?.cascadeParentIds?.[0],
);
const hasParentFilter = !!parentFilter;
const hasPreFilter = const hasPreFilter =
!!formFilter?.adhoc_filters || !!formFilter?.adhoc_filters ||
!!formFilter?.time_range || !!formFilter?.time_range ||
@ -583,28 +589,32 @@ const FiltersConfigForm = (
return Promise.reject(new Error(t('Pre-filter is required'))); return Promise.reject(new Error(t('Pre-filter is required')));
}; };
let hasCheckedAdvancedControl = hasParentFilter || hasPreFilter || hasSorting;
if (!hasCheckedAdvancedControl) {
hasCheckedAdvancedControl = Object.keys(controlItems)
.filter(key => !BASIC_CONTROL_ITEMS.includes(key))
.some(key => controlItems[key].checked);
}
const ParentSelect = ({ const ParentSelect = ({
value, value,
...rest ...rest
}: { }: {
value?: { value: string | number }; value?: { value: string | number };
}) => ( }) => {
<Select const parentId = value?.value;
ariaLabel={t('Parent filter')} const isParentRemoved = parentId && removedFilters[parentId];
placeholder={t('None')} let options = parentFilterOptions;
options={parentFilterOptions} if (isParentRemoved) {
allowClear options = [
value={value?.value} { label: t('(deleted)'), value: parentId as string },
{...rest} ...parentFilterOptions,
/> ];
); }
return (
<Select
ariaLabel={t('Parent filter')}
placeholder={t('None')}
options={options}
allowClear
value={parentId}
{...rest}
/>
);
};
useEffect(() => { useEffect(() => {
if (datasetId) { if (datasetId) {
@ -647,12 +657,28 @@ const FiltersConfigForm = (
]); ]);
useEffect(() => { useEffect(() => {
const activeFilterPanelKey = [FilterPanels.basic.key]; // Run only once when the control items are available
if (hasCheckedAdvancedControl) { if (!activeFilterPanelKey && !isEmpty(controlItems)) {
activeFilterPanelKey.push(FilterPanels.advanced.key); const hasCheckedAdvancedControl =
hasParentFilter ||
hasPreFilter ||
hasSorting ||
Object.keys(controlItems)
.filter(key => !BASIC_CONTROL_ITEMS.includes(key))
.some(key => controlItems[key].checked);
setActiveFilterPanelKey(
hasCheckedAdvancedControl
? [FilterPanels.basic.key, FilterPanels.advanced.key]
: FilterPanels.basic.key,
);
} }
setActiveFilterPanelKey(activeFilterPanelKey); }, [
}, [hasCheckedAdvancedControl]); activeFilterPanelKey,
hasParentFilter,
hasPreFilter,
hasSorting,
controlItems,
]);
const initiallyExcludedCharts = useMemo(() => { const initiallyExcludedCharts = useMemo(() => {
const excluded: number[] = []; const excluded: number[] = [];
@ -678,20 +704,20 @@ const FiltersConfigForm = (
useEffect(() => { useEffect(() => {
// just removed, saving current form items for eventual undo // just removed, saving current form items for eventual undo
if (removed) { if (isRemoved) {
setUndoFormValues(formValues); setUndoFormValues(formValues);
} }
}, [removed]); }, [isRemoved]);
useEffect(() => { useEffect(() => {
// the filter was just restored after undo // the filter was just restored after undo
if (undoFormValues && !removed) { if (undoFormValues && !isRemoved) {
setNativeFilterFieldValues(form, filterId, undoFormValues); setNativeFilterFieldValues(form, filterId, undoFormValues);
setUndoFormValues(null); setUndoFormValues(null);
} }
}, [formValues, filterId, form, removed, undoFormValues]); }, [formValues, filterId, form, isRemoved, undoFormValues]);
if (removed) { if (isRemoved) {
return <RemovedFilter onClick={() => restoreFilter(filterId)} />; return <RemovedFilter onClick={() => restoreFilter(filterId)} />;
} }
@ -718,13 +744,13 @@ const FiltersConfigForm = (
name={['filters', filterId, 'name']} name={['filters', filterId, 'name']}
label={<StyledLabel>{t('Filter name')}</StyledLabel>} label={<StyledLabel>{t('Filter name')}</StyledLabel>}
initialValue={filterToEdit?.name} initialValue={filterToEdit?.name}
rules={[{ required: !removed, message: t('Name is required') }]} rules={[{ required: !isRemoved, message: t('Name is required') }]}
> >
<Input {...getFiltersConfigModalTestId('name-input')} /> <Input {...getFiltersConfigModalTestId('name-input')} />
</StyledFormItem> </StyledFormItem>
<StyledFormItem <StyledFormItem
name={['filters', filterId, 'filterType']} name={['filters', filterId, 'filterType']}
rules={[{ required: !removed, message: t('Name is required') }]} rules={[{ required: !isRemoved, message: t('Name is required') }]}
initialValue={filterToEdit?.filterType || 'filter_select'} initialValue={filterToEdit?.filterType || 'filter_select'}
label={<StyledLabel>{t('Filter Type')}</StyledLabel>} label={<StyledLabel>{t('Filter Type')}</StyledLabel>}
{...getFiltersConfigModalTestId('filter-type')} {...getFiltersConfigModalTestId('filter-type')}
@ -782,7 +808,7 @@ const FiltersConfigForm = (
: undefined : undefined
} }
rules={[ rules={[
{ required: !removed, message: t('Dataset is required') }, { required: !isRemoved, message: t('Dataset is required') },
]} ]}
{...getFiltersConfigModalTestId('datasource-input')} {...getFiltersConfigModalTestId('datasource-input')}
> >
@ -837,7 +863,7 @@ const FiltersConfigForm = (
formChanged(); formChanged();
}} }}
> >
{!removed && ( {!isRemoved && (
<StyledRowSubFormItem <StyledRowSubFormItem
name={['filters', filterId, 'defaultDataMask']} name={['filters', filterId, 'defaultDataMask']}
initialValue={initialDefaultValue} initialValue={initialDefaultValue}

View File

@ -196,6 +196,27 @@ export function FiltersConfigModal({
title: getFilterTitle(id), title: getFilterTitle(id),
})); }));
const cleanDeletedParents = (values: NativeFiltersForm | null) => {
Object.keys(filterConfigMap).forEach(key => {
const filter = filterConfigMap[key];
const parentId = filter.cascadeParentIds?.[0];
if (parentId && removedFilters[parentId]) {
filter.cascadeParentIds = [];
}
});
const filters = values?.filters;
if (filters) {
Object.keys(filters).forEach(key => {
const filter = filters[key];
const parentId = filter.parentFilter?.value;
if (parentId && removedFilters[parentId]) {
filter.parentFilter = undefined;
}
});
}
};
const handleSave = async () => { const handleSave = async () => {
const values: NativeFiltersForm | null = await validateForm( const values: NativeFiltersForm | null = await validateForm(
form, form,
@ -207,6 +228,7 @@ export function FiltersConfigModal({
); );
if (values) { if (values) {
cleanDeletedParents(values);
createHandleSave( createHandleSave(
filterConfigMap, filterConfigMap,
filterIds, filterIds,
@ -295,7 +317,7 @@ export function FiltersConfigModal({
form={form} form={form}
filterId={id} filterId={id}
filterToEdit={filterConfigMap[id]} filterToEdit={filterConfigMap[id]}
removed={!!removedFilters[id]} removedFilters={removedFilters}
restoreFilter={restoreFilter} restoreFilter={restoreFilter}
parentFilters={getParentFilters(id)} parentFilters={getParentFilters(id)}
/> />

View File

@ -36,7 +36,7 @@ export interface NativeFiltersFormItem {
}; };
defaultValue: any; defaultValue: any;
defaultDataMask: DataMask; defaultDataMask: DataMask;
parentFilter: { parentFilter?: {
value: string; value: string;
label: string; label: string;
}; };

View File

@ -65,8 +65,8 @@ export const validateForm = async (
'Cannot create cyclic hierarchy', 'Cannot create cyclic hierarchy',
); );
} }
const parentId = formValues.filters[filterId] const parentId = formValues.filters?.[filterId]
? formValues.filters[filterId].parentFilter?.value ? formValues.filters[filterId]?.parentFilter?.value
: filterConfigMap[filterId]?.cascadeParentIds?.[0]; : filterConfigMap[filterId]?.cascadeParentIds?.[0];
if (parentId) { if (parentId) {
validateCycles(parentId, [...trace, filterId]); validateCycles(parentId, [...trace, filterId]);
@ -111,7 +111,7 @@ export const createHandleSave = (
.filter(id => !removedFilters[id]) .filter(id => !removedFilters[id])
.map(id => { .map(id => {
// create a filter config object from the form inputs // create a filter config object from the form inputs
const formInputs = values.filters[id]; const formInputs = values.filters?.[id];
// if user didn't open a filter, return the original config // if user didn't open a filter, return the original config
if (!formInputs) return filterConfigMap[id]; if (!formInputs) return filterConfigMap[id];
const target: Partial<Target> = {}; const target: Partial<Target> = {};