mirror of https://github.com/apache/superset.git
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:
parent
3272d1c086
commit
4da4fe4136
|
@ -37,6 +37,7 @@ describe('FilterScope', () => {
|
|||
restoreFilter: jest.fn(),
|
||||
parentFilters: [],
|
||||
save,
|
||||
removedFilters: {},
|
||||
};
|
||||
|
||||
const MockModal = ({ scope }: { scope?: object }) => {
|
||||
|
|
|
@ -44,7 +44,7 @@ import React, {
|
|||
useState,
|
||||
} from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { isEqual } from 'lodash';
|
||||
import { isEqual, isEmpty } from 'lodash';
|
||||
import { FormItem } from 'src/components/Form';
|
||||
import { Input } from 'src/common/components';
|
||||
import { Select } from 'src/components';
|
||||
|
@ -70,7 +70,7 @@ import {
|
|||
} from 'src/dashboard/types';
|
||||
import Loading from 'src/components/Loading';
|
||||
import { ColumnSelect } from './ColumnSelect';
|
||||
import { NativeFiltersForm } from '../types';
|
||||
import { NativeFiltersForm, FilterRemoval } from '../types';
|
||||
import {
|
||||
FILTER_SUPPORTED_TYPES,
|
||||
hasTemporalColumns,
|
||||
|
@ -264,7 +264,7 @@ const FilterPanels = {
|
|||
export interface FiltersConfigFormProps {
|
||||
filterId: string;
|
||||
filterToEdit?: Filter;
|
||||
removed?: boolean;
|
||||
removedFilters: Record<string, FilterRemoval>;
|
||||
restoreFilter: (filterId: string) => void;
|
||||
form: FormInstance<NativeFiltersForm>;
|
||||
parentFilters: { id: string; title: string }[];
|
||||
|
@ -300,21 +300,22 @@ const FiltersConfigForm = (
|
|||
{
|
||||
filterId,
|
||||
filterToEdit,
|
||||
removed,
|
||||
removedFilters,
|
||||
restoreFilter,
|
||||
form,
|
||||
parentFilters,
|
||||
}: FiltersConfigFormProps,
|
||||
ref: React.RefObject<any>,
|
||||
) => {
|
||||
const isRemoved = !!removedFilters[filterId];
|
||||
const [error, setError] = useState<string>('');
|
||||
const [metrics, setMetrics] = useState<Metric[]>([]);
|
||||
const [activeTabKey, setActiveTabKey] = useState<string>(
|
||||
FilterTabs.configuration.key,
|
||||
);
|
||||
const [activeFilterPanelKey, setActiveFilterPanelKey] = useState<
|
||||
string | string[]
|
||||
>(FilterPanels.basic.key);
|
||||
string | string[] | undefined
|
||||
>();
|
||||
const [undoFormValues, setUndoFormValues] = useState<Record<
|
||||
string,
|
||||
any
|
||||
|
@ -325,6 +326,24 @@ const FiltersConfigForm = (
|
|||
const formValues = form.getFieldValue('filters')?.[filterId];
|
||||
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 nativeFilterVizTypes = Object.entries(nativeFilterItems)
|
||||
// @ts-ignore
|
||||
|
@ -374,7 +393,7 @@ const FiltersConfigForm = (
|
|||
filterType: formFilter?.filterType,
|
||||
filterToEdit,
|
||||
formFilter,
|
||||
removed,
|
||||
removed: isRemoved,
|
||||
})
|
||||
: {};
|
||||
const hasColumn = !!mainControlItems.groupby;
|
||||
|
@ -506,19 +525,6 @@ const FiltersConfigForm = (
|
|||
[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 =
|
||||
!!formFilter?.adhoc_filters ||
|
||||
!!formFilter?.time_range ||
|
||||
|
@ -583,28 +589,32 @@ const FiltersConfigForm = (
|
|||
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 = ({
|
||||
value,
|
||||
...rest
|
||||
}: {
|
||||
value?: { value: string | number };
|
||||
}) => (
|
||||
<Select
|
||||
ariaLabel={t('Parent filter')}
|
||||
placeholder={t('None')}
|
||||
options={parentFilterOptions}
|
||||
allowClear
|
||||
value={value?.value}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}) => {
|
||||
const parentId = value?.value;
|
||||
const isParentRemoved = parentId && removedFilters[parentId];
|
||||
let options = parentFilterOptions;
|
||||
if (isParentRemoved) {
|
||||
options = [
|
||||
{ label: t('(deleted)'), value: parentId as string },
|
||||
...parentFilterOptions,
|
||||
];
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
ariaLabel={t('Parent filter')}
|
||||
placeholder={t('None')}
|
||||
options={options}
|
||||
allowClear
|
||||
value={parentId}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (datasetId) {
|
||||
|
@ -647,12 +657,28 @@ const FiltersConfigForm = (
|
|||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const activeFilterPanelKey = [FilterPanels.basic.key];
|
||||
if (hasCheckedAdvancedControl) {
|
||||
activeFilterPanelKey.push(FilterPanels.advanced.key);
|
||||
// Run only once when the control items are available
|
||||
if (!activeFilterPanelKey && !isEmpty(controlItems)) {
|
||||
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 excluded: number[] = [];
|
||||
|
@ -678,20 +704,20 @@ const FiltersConfigForm = (
|
|||
|
||||
useEffect(() => {
|
||||
// just removed, saving current form items for eventual undo
|
||||
if (removed) {
|
||||
if (isRemoved) {
|
||||
setUndoFormValues(formValues);
|
||||
}
|
||||
}, [removed]);
|
||||
}, [isRemoved]);
|
||||
|
||||
useEffect(() => {
|
||||
// the filter was just restored after undo
|
||||
if (undoFormValues && !removed) {
|
||||
if (undoFormValues && !isRemoved) {
|
||||
setNativeFilterFieldValues(form, filterId, undoFormValues);
|
||||
setUndoFormValues(null);
|
||||
}
|
||||
}, [formValues, filterId, form, removed, undoFormValues]);
|
||||
}, [formValues, filterId, form, isRemoved, undoFormValues]);
|
||||
|
||||
if (removed) {
|
||||
if (isRemoved) {
|
||||
return <RemovedFilter onClick={() => restoreFilter(filterId)} />;
|
||||
}
|
||||
|
||||
|
@ -718,13 +744,13 @@ const FiltersConfigForm = (
|
|||
name={['filters', filterId, 'name']}
|
||||
label={<StyledLabel>{t('Filter name')}</StyledLabel>}
|
||||
initialValue={filterToEdit?.name}
|
||||
rules={[{ required: !removed, message: t('Name is required') }]}
|
||||
rules={[{ required: !isRemoved, message: t('Name is required') }]}
|
||||
>
|
||||
<Input {...getFiltersConfigModalTestId('name-input')} />
|
||||
</StyledFormItem>
|
||||
<StyledFormItem
|
||||
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'}
|
||||
label={<StyledLabel>{t('Filter Type')}</StyledLabel>}
|
||||
{...getFiltersConfigModalTestId('filter-type')}
|
||||
|
@ -782,7 +808,7 @@ const FiltersConfigForm = (
|
|||
: undefined
|
||||
}
|
||||
rules={[
|
||||
{ required: !removed, message: t('Dataset is required') },
|
||||
{ required: !isRemoved, message: t('Dataset is required') },
|
||||
]}
|
||||
{...getFiltersConfigModalTestId('datasource-input')}
|
||||
>
|
||||
|
@ -837,7 +863,7 @@ const FiltersConfigForm = (
|
|||
formChanged();
|
||||
}}
|
||||
>
|
||||
{!removed && (
|
||||
{!isRemoved && (
|
||||
<StyledRowSubFormItem
|
||||
name={['filters', filterId, 'defaultDataMask']}
|
||||
initialValue={initialDefaultValue}
|
||||
|
|
|
@ -196,6 +196,27 @@ export function FiltersConfigModal({
|
|||
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 values: NativeFiltersForm | null = await validateForm(
|
||||
form,
|
||||
|
@ -207,6 +228,7 @@ export function FiltersConfigModal({
|
|||
);
|
||||
|
||||
if (values) {
|
||||
cleanDeletedParents(values);
|
||||
createHandleSave(
|
||||
filterConfigMap,
|
||||
filterIds,
|
||||
|
@ -295,7 +317,7 @@ export function FiltersConfigModal({
|
|||
form={form}
|
||||
filterId={id}
|
||||
filterToEdit={filterConfigMap[id]}
|
||||
removed={!!removedFilters[id]}
|
||||
removedFilters={removedFilters}
|
||||
restoreFilter={restoreFilter}
|
||||
parentFilters={getParentFilters(id)}
|
||||
/>
|
||||
|
|
|
@ -36,7 +36,7 @@ export interface NativeFiltersFormItem {
|
|||
};
|
||||
defaultValue: any;
|
||||
defaultDataMask: DataMask;
|
||||
parentFilter: {
|
||||
parentFilter?: {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
|
|
@ -65,8 +65,8 @@ export const validateForm = async (
|
|||
'Cannot create cyclic hierarchy',
|
||||
);
|
||||
}
|
||||
const parentId = formValues.filters[filterId]
|
||||
? formValues.filters[filterId].parentFilter?.value
|
||||
const parentId = formValues.filters?.[filterId]
|
||||
? formValues.filters[filterId]?.parentFilter?.value
|
||||
: filterConfigMap[filterId]?.cascadeParentIds?.[0];
|
||||
if (parentId) {
|
||||
validateCycles(parentId, [...trace, filterId]);
|
||||
|
@ -111,7 +111,7 @@ export const createHandleSave = (
|
|||
.filter(id => !removedFilters[id])
|
||||
.map(id => {
|
||||
// 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 (!formInputs) return filterConfigMap[id];
|
||||
const target: Partial<Target> = {};
|
||||
|
|
Loading…
Reference in New Issue