chore: Improves the native filters UI/UX - iteration 3 (#14824)
This commit is contained in:
parent
9fe0222009
commit
0c0eccb81a
|
|
@ -110,7 +110,7 @@ describe('FiltersConfigModal', () => {
|
|||
|
||||
function addFilter() {
|
||||
act(() => {
|
||||
wrapper.find('button[aria-label="Add tab"]').at(0).simulate('click');
|
||||
wrapper.find('[aria-label="Add filter"]').at(0).simulate('click');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -167,7 +167,9 @@ const FilterTabs: FC<FilterTabsProps> = ({
|
|||
addIcon={
|
||||
<StyledAddFilterBox>
|
||||
<PlusOutlined />{' '}
|
||||
<span data-test="add-filter-button">{t('Add filter')}</span>
|
||||
<span data-test="add-filter-button" aria-label="Add filter">
|
||||
{t('Add filter')}
|
||||
</span>
|
||||
</StyledAddFilterBox>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* 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 React, { ReactNode, useState } from 'react';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { Checkbox } from 'src/common/components';
|
||||
|
||||
interface CollapsibleControlProps {
|
||||
checked?: boolean;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
onChange?: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
const StyledContainer = styled.div<{ checked: boolean }>`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
min-height: ${({ theme }) => theme.gridUnit * 10}px;
|
||||
padding-top: ${({ theme }) => theme.gridUnit * 2 + 2}px;
|
||||
|
||||
.checkbox {
|
||||
margin-bottom: ${({ theme, checked }) => (checked ? theme.gridUnit : 0)}px;
|
||||
}
|
||||
|
||||
& > div {
|
||||
margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
}
|
||||
`;
|
||||
|
||||
const CollapsibleControl = (props: CollapsibleControlProps) => {
|
||||
const { checked = false, title, children, onChange } = props;
|
||||
const [isChecked, setIsChecked] = useState(checked);
|
||||
return (
|
||||
<StyledContainer checked={isChecked}>
|
||||
<Checkbox
|
||||
className="checkbox"
|
||||
checked={isChecked}
|
||||
onChange={e => {
|
||||
const value = e.target.checked;
|
||||
setIsChecked(value);
|
||||
if (onChange) {
|
||||
onChange(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Checkbox>
|
||||
{isChecked && children}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export { CollapsibleControl, CollapsibleControlProps };
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
/**
|
||||
* 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 {
|
||||
CustomControlItem,
|
||||
InfoTooltipWithTrigger,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import React, { FC } from 'react';
|
||||
import { Checkbox } from 'src/common/components';
|
||||
import { FormInstance } from 'antd/lib/form';
|
||||
import { getChartControlPanelRegistry, t } from '@superset-ui/core';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import { getControlItems, setNativeFilterFieldValues } from './utils';
|
||||
import { NativeFiltersForm, NativeFiltersFormItem } from '../types';
|
||||
import { StyledCheckboxFormItem } from './FiltersConfigForm';
|
||||
import { Filter } from '../../types';
|
||||
|
||||
type ControlItemsProps = {
|
||||
disabled: boolean;
|
||||
filterId: string;
|
||||
forceUpdate: Function;
|
||||
filterToEdit?: Filter;
|
||||
form: FormInstance<NativeFiltersForm>;
|
||||
formFilter?: NativeFiltersFormItem;
|
||||
};
|
||||
|
||||
const ControlItems: FC<ControlItemsProps> = ({
|
||||
disabled,
|
||||
forceUpdate,
|
||||
form,
|
||||
filterId,
|
||||
filterToEdit,
|
||||
formFilter,
|
||||
}) => {
|
||||
const filterType = formFilter?.filterType;
|
||||
|
||||
if (!filterType) return null;
|
||||
|
||||
const controlPanelRegistry = getChartControlPanelRegistry();
|
||||
const controlItems =
|
||||
getControlItems(controlPanelRegistry.get(filterType)) ?? [];
|
||||
return (
|
||||
<>
|
||||
{controlItems
|
||||
.filter(
|
||||
(controlItem: CustomControlItem) =>
|
||||
controlItem?.config?.renderTrigger,
|
||||
)
|
||||
.map(controlItem => (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
title={
|
||||
controlItem.config.affectsDataMask &&
|
||||
disabled &&
|
||||
t('Populate "Default value" to enable this control')
|
||||
}
|
||||
>
|
||||
<StyledCheckboxFormItem
|
||||
key={controlItem.name}
|
||||
name={['filters', filterId, 'controlValues', controlItem.name]}
|
||||
initialValue={
|
||||
filterToEdit?.controlValues?.[controlItem.name] ??
|
||||
controlItem?.config?.default
|
||||
}
|
||||
valuePropName="checked"
|
||||
colon={false}
|
||||
>
|
||||
<Checkbox
|
||||
disabled={controlItem.config.affectsDataMask && disabled}
|
||||
onChange={() => {
|
||||
if (!controlItem.config.resetConfig) {
|
||||
forceUpdate();
|
||||
return;
|
||||
}
|
||||
setNativeFilterFieldValues(form, filterId, {
|
||||
defaultDataMask: null,
|
||||
});
|
||||
forceUpdate();
|
||||
}}
|
||||
>
|
||||
{controlItem.config.label}{' '}
|
||||
{controlItem.config.description && (
|
||||
<InfoTooltipWithTrigger
|
||||
placement="top"
|
||||
label={controlItem.config.name}
|
||||
tooltip={controlItem.config.description}
|
||||
/>
|
||||
)}
|
||||
</Checkbox>
|
||||
</StyledCheckboxFormItem>
|
||||
</Tooltip>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ControlItems;
|
||||
|
|
@ -32,7 +32,7 @@ import {
|
|||
Metric,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { FormInstance } from 'antd/lib/form';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Checkbox, Form, Input } from 'src/common/components';
|
||||
import { Select } from 'src/components/Select';
|
||||
|
|
@ -45,7 +45,6 @@ import { addDangerToast } from 'src/messageToasts/actions';
|
|||
import { ClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||
import SelectControl from 'src/explore/components/controls/SelectControl';
|
||||
import Collapse from 'src/components/Collapse';
|
||||
import Button from 'src/components/Button';
|
||||
import { getChartDataRequest } from 'src/chart/chartAction';
|
||||
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
|
||||
import { waitForAsyncData } from 'src/middleware/asyncEvent';
|
||||
|
|
@ -60,10 +59,11 @@ import {
|
|||
import { useBackendFormUpdate } from './state';
|
||||
import { getFormData } from '../../utils';
|
||||
import { Filter } from '../../types';
|
||||
import ControlItems from './ControlItems';
|
||||
import getControlItemsMap from './getControlItemsMap';
|
||||
import FilterScope from './FilterScope/FilterScope';
|
||||
import RemovedFilter from './RemovedFilter';
|
||||
import DefaultValue from './DefaultValue';
|
||||
import { CollapsibleControl } from './CollapsibleControl';
|
||||
import {
|
||||
CASCADING_FILTERS,
|
||||
getFiltersConfigModalTestId,
|
||||
|
|
@ -77,19 +77,29 @@ const StyledContainer = styled.div`
|
|||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const StyledDatasetContainer = styled.div`
|
||||
const StyledRowContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const StyledFormItem = styled(Form.Item)`
|
||||
width: 49%;
|
||||
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
|
||||
& .ant-form-item-control-input {
|
||||
min-height: ${({ theme }) => theme.gridUnit * 10}px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledCheckboxFormItem = styled(Form.Item)`
|
||||
margin-bottom: 0;
|
||||
export const StyledRowFormItem = styled(Form.Item)`
|
||||
margin-bottom: 0px;
|
||||
min-width: 50%;
|
||||
|
||||
& .ant-form-item-control-input {
|
||||
min-height: ${({ theme }) => theme.gridUnit * 10}px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledLabel = styled.span`
|
||||
|
|
@ -120,6 +130,10 @@ const StyledCollapse = styled(Collapse)`
|
|||
border: 0px;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding-top: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
}
|
||||
|
||||
&.ant-collapse > .ant-collapse-item {
|
||||
border: 0px;
|
||||
border-radius: 0px;
|
||||
|
|
@ -130,6 +144,10 @@ const StyledTabs = styled(Tabs)`
|
|||
.ant-tabs-nav-list {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.ant-form-item-label {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
const FilterTabs = {
|
||||
|
|
@ -169,8 +187,11 @@ const FILTERS_WITHOUT_COLUMN = [
|
|||
'filter_timecolumn',
|
||||
'filter_groupby',
|
||||
];
|
||||
|
||||
const FILTERS_WITH_ADHOC_FILTERS = ['filter_select', 'filter_range'];
|
||||
|
||||
const BASIC_CONTROL_ITEMS = ['enableEmptyFilter', 'multiSelect'];
|
||||
|
||||
/**
|
||||
* The configuration form for a specific filter.
|
||||
* Assigns field values to `filters[filterId]` in the form.
|
||||
|
|
@ -184,9 +205,14 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
parentFilters,
|
||||
}) => {
|
||||
const [metrics, setMetrics] = useState<Metric[]>([]);
|
||||
const [hasDefaultValue, setHasDefaultValue] = useState(
|
||||
!!filterToEdit?.defaultDataMask?.filterState?.value,
|
||||
);
|
||||
const forceUpdate = useForceUpdate();
|
||||
const [datasetDetails, setDatasetDetails] = useState<Record<string, any>>();
|
||||
const formFilter = form.getFieldValue('filters')?.[filterId] || {};
|
||||
const defaultFormFilter = useMemo(() => {}, []);
|
||||
const formFilter =
|
||||
form.getFieldValue('filters')?.[filterId] || defaultFormFilter;
|
||||
const nativeFilterItems = getChartMetadataRegistry().items;
|
||||
const nativeFilterVizTypes = Object.entries(nativeFilterItems)
|
||||
// @ts-ignore
|
||||
|
|
@ -243,7 +269,7 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
|
||||
useBackendFormUpdate(form, filterId);
|
||||
|
||||
const refreshHandler = () => {
|
||||
const refreshHandler = useCallback(() => {
|
||||
if (!hasDataset || !formFilter?.dataset?.value) {
|
||||
forceUpdate();
|
||||
return;
|
||||
|
|
@ -287,7 +313,7 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
forceUpdate();
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [filterId, forceUpdate, form, formFilter, hasDataset]);
|
||||
|
||||
const defaultDatasetSelectOptions = Object.values(loadedDatasets).map(
|
||||
datasetToSelectOption,
|
||||
|
|
@ -304,6 +330,19 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
...formFilter,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (hasDataset && hasFilledDataset && hasDefaultValue && isDataDirty) {
|
||||
refreshHandler();
|
||||
}
|
||||
}, [
|
||||
hasDataset,
|
||||
hasFilledDataset,
|
||||
hasDefaultValue,
|
||||
formFilter,
|
||||
isDataDirty,
|
||||
refreshHandler,
|
||||
]);
|
||||
|
||||
const onDatasetSelectError = useCallback(
|
||||
({ error, message }: ClientErrorObject) => {
|
||||
let errorText = message || error || t('An error has occurred');
|
||||
|
|
@ -324,8 +363,34 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
label: filter.title,
|
||||
}));
|
||||
|
||||
const parentFilter = parentFilterOptions.find(
|
||||
({ value }) => value === filterToEdit?.cascadeParentIds[0],
|
||||
);
|
||||
|
||||
const showDefaultValue = !hasDataset || (!isDataDirty && hasFilledDataset);
|
||||
|
||||
const controlItems = formFilter
|
||||
? getControlItemsMap({
|
||||
disabled: !showDefaultValue,
|
||||
forceUpdate,
|
||||
form,
|
||||
filterId,
|
||||
filterType: formFilter.filterType,
|
||||
filterToEdit,
|
||||
})
|
||||
: {};
|
||||
|
||||
const onSortChanged = (value: boolean | undefined) => {
|
||||
const previous = form.getFieldValue('filters')?.[filterId].controlValues;
|
||||
setNativeFilterFieldValues(form, filterId, {
|
||||
controlValues: {
|
||||
...previous,
|
||||
sortAscending: value,
|
||||
},
|
||||
});
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTabs defaultActiveKey={FilterTabs.configuration.key} centered>
|
||||
|
|
@ -367,7 +432,7 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
</StyledFormItem>
|
||||
</StyledContainer>
|
||||
{hasDataset && (
|
||||
<StyledDatasetContainer>
|
||||
<StyledRowContainer>
|
||||
<StyledFormItem
|
||||
name={['filters', filterId, 'dataset']}
|
||||
initialValue={{ value: initialDatasetId }}
|
||||
|
|
@ -425,9 +490,9 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
/>
|
||||
</StyledFormItem>
|
||||
)}
|
||||
</StyledDatasetContainer>
|
||||
</StyledRowContainer>
|
||||
)}
|
||||
<StyledCollapse>
|
||||
<StyledCollapse defaultActiveKey={FilterPanels.basic.key}>
|
||||
<Collapse.Panel
|
||||
header={FilterPanels.basic.name}
|
||||
key={FilterPanels.basic.key}
|
||||
|
|
@ -444,31 +509,12 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
hidden
|
||||
initialValue={null}
|
||||
/>
|
||||
{isCascadingFilter && (
|
||||
<StyledFormItem
|
||||
name={['filters', filterId, 'parentFilter']}
|
||||
label={<StyledLabel>{t('Parent filter')}</StyledLabel>}
|
||||
initialValue={parentFilterOptions.find(
|
||||
({ value }) => value === filterToEdit?.cascadeParentIds[0],
|
||||
)}
|
||||
data-test="parent-filter-input"
|
||||
>
|
||||
<Select
|
||||
placeholder={t('None')}
|
||||
options={parentFilterOptions}
|
||||
isClearable
|
||||
/>
|
||||
</StyledFormItem>
|
||||
)}
|
||||
<StyledContainer>
|
||||
<StyledFormItem className="bottom" label={<StyledLabel />}>
|
||||
{hasDataset && hasFilledDataset && (
|
||||
<Button onClick={refreshHandler}>
|
||||
{isDataDirty ? t('Populate') : t('Refresh')}
|
||||
</Button>
|
||||
)}
|
||||
</StyledFormItem>
|
||||
<StyledFormItem
|
||||
<CollapsibleControl
|
||||
title={t('Filter has default value')}
|
||||
checked={hasDefaultValue}
|
||||
onChange={value => setHasDefaultValue(value)}
|
||||
>
|
||||
<StyledRowFormItem
|
||||
name={['filters', filterId, 'defaultDataMask']}
|
||||
initialValue={filterToEdit?.defaultDataMask}
|
||||
data-test="default-input"
|
||||
|
|
@ -488,14 +534,15 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
formData={newFormData}
|
||||
enableNoResults={enableNoResults}
|
||||
/>
|
||||
) : hasFilledDataset ? (
|
||||
t('Click "Populate" to get "Default Value" ->')
|
||||
) : (
|
||||
t('Fill all required fields to enable "Default Value"')
|
||||
)}
|
||||
</StyledFormItem>
|
||||
</StyledContainer>
|
||||
<StyledCheckboxFormItem
|
||||
</StyledRowFormItem>
|
||||
</CollapsibleControl>
|
||||
{Object.keys(controlItems)
|
||||
.filter(key => BASIC_CONTROL_ITEMS.includes(key))
|
||||
.map(key => controlItems[key])}
|
||||
<StyledRowFormItem
|
||||
name={['filters', filterId, 'isInstant']}
|
||||
initialValue={filterToEdit?.isInstant || false}
|
||||
valuePropName="checked"
|
||||
|
|
@ -504,24 +551,44 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
<Checkbox data-test="apply-changes-instantly-checkbox">
|
||||
{t('Apply changes instantly')}
|
||||
</Checkbox>
|
||||
</StyledCheckboxFormItem>
|
||||
<ControlItems
|
||||
disabled={!showDefaultValue}
|
||||
filterToEdit={filterToEdit}
|
||||
formFilter={formFilter}
|
||||
filterId={filterId}
|
||||
form={form}
|
||||
forceUpdate={forceUpdate}
|
||||
/>
|
||||
</StyledRowFormItem>
|
||||
</Collapse.Panel>
|
||||
{((hasDataset && hasAdditionalFilters) || hasMetrics) && (
|
||||
<Collapse.Panel
|
||||
header={FilterPanels.advanced.name}
|
||||
key={FilterPanels.advanced.key}
|
||||
>
|
||||
{isCascadingFilter && (
|
||||
<CollapsibleControl
|
||||
title={t('Filter is hierarchical')}
|
||||
checked={!!parentFilter}
|
||||
>
|
||||
<StyledRowFormItem
|
||||
name={['filters', filterId, 'parentFilter']}
|
||||
label={<StyledLabel>{t('Parent filter')}</StyledLabel>}
|
||||
initialValue={parentFilter}
|
||||
data-test="parent-filter-input"
|
||||
>
|
||||
<Select
|
||||
placeholder={t('None')}
|
||||
options={parentFilterOptions}
|
||||
isClearable
|
||||
/>
|
||||
</StyledRowFormItem>
|
||||
</CollapsibleControl>
|
||||
)}
|
||||
{Object.keys(controlItems)
|
||||
.filter(key => !BASIC_CONTROL_ITEMS.includes(key))
|
||||
.map(key => controlItems[key])}
|
||||
{hasDataset && hasAdditionalFilters && (
|
||||
<>
|
||||
<StyledFormItem
|
||||
<CollapsibleControl
|
||||
title={t('Pre-filter available values')}
|
||||
checked={
|
||||
!!filterToEdit?.adhoc_filters ||
|
||||
!!filterToEdit?.time_range
|
||||
}
|
||||
>
|
||||
<StyledRowFormItem
|
||||
name={['filters', filterId, 'adhoc_filters']}
|
||||
initialValue={filterToEdit?.adhoc_filters}
|
||||
>
|
||||
|
|
@ -541,8 +608,8 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
}}
|
||||
label={<StyledLabel>{t('Adhoc filters')}</StyledLabel>}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
<StyledFormItem
|
||||
</StyledRowFormItem>
|
||||
<StyledRowFormItem
|
||||
name={['filters', filterId, 'time_range']}
|
||||
label={<StyledLabel>{t('Time range')}</StyledLabel>}
|
||||
initialValue={filterToEdit?.time_range || 'No filter'}
|
||||
|
|
@ -556,37 +623,75 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
</>
|
||||
</StyledRowFormItem>
|
||||
</CollapsibleControl>
|
||||
)}
|
||||
{hasMetrics && (
|
||||
<StyledFormItem
|
||||
// don't show the column select unless we have a dataset
|
||||
// style={{ display: datasetId == null ? undefined : 'none' }}
|
||||
name={['filters', filterId, 'sortMetric']}
|
||||
initialValue={filterToEdit?.sortMetric}
|
||||
label={<StyledLabel>{t('Sort Metric')}</StyledLabel>}
|
||||
data-test="field-input"
|
||||
>
|
||||
<SelectControl
|
||||
form={form}
|
||||
filterId={filterId}
|
||||
name="sortMetric"
|
||||
options={metrics.map((metric: Metric) => ({
|
||||
value: metric.metric_name,
|
||||
label: metric.verbose_name ?? metric.metric_name,
|
||||
}))}
|
||||
onChange={(value: string | null): void => {
|
||||
if (value !== undefined) {
|
||||
setNativeFilterFieldValues(form, filterId, {
|
||||
sortMetric: value,
|
||||
});
|
||||
forceUpdate();
|
||||
<CollapsibleControl
|
||||
title={t('Sort filter values')}
|
||||
onChange={checked => onSortChanged(checked || undefined)}
|
||||
checked={
|
||||
typeof filterToEdit?.controlValues?.sortAscending ===
|
||||
'boolean'
|
||||
}
|
||||
>
|
||||
<StyledRowContainer>
|
||||
<StyledFormItem
|
||||
name={[
|
||||
'filters',
|
||||
filterId,
|
||||
'controlValues',
|
||||
'sortAscending',
|
||||
]}
|
||||
initialValue={filterToEdit?.controlValues?.sortAscending}
|
||||
label={<StyledLabel>{t('Sort type')}</StyledLabel>}
|
||||
>
|
||||
<Select
|
||||
form={form}
|
||||
filterId={filterId}
|
||||
name="sortAscending"
|
||||
options={[
|
||||
{
|
||||
value: true,
|
||||
label: t('Sort ascending'),
|
||||
},
|
||||
{
|
||||
value: false,
|
||||
label: t('Sort descending'),
|
||||
},
|
||||
]}
|
||||
onChange={({ value }: { value: boolean }) =>
|
||||
onSortChanged(value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
)}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
{hasMetrics && (
|
||||
<StyledFormItem
|
||||
name={['filters', filterId, 'sortMetric']}
|
||||
initialValue={filterToEdit?.sortMetric}
|
||||
label={<StyledLabel>{t('Sort Metric')}</StyledLabel>}
|
||||
data-test="field-input"
|
||||
>
|
||||
<SelectControl
|
||||
form={form}
|
||||
filterId={filterId}
|
||||
name="sortMetric"
|
||||
options={metrics.map((metric: Metric) => ({
|
||||
value: metric.metric_name,
|
||||
label: metric.verbose_name ?? metric.metric_name,
|
||||
}))}
|
||||
onChange={(value: string | null): void => {
|
||||
if (value !== undefined) {
|
||||
setNativeFilterFieldValues(form, filterId, {
|
||||
sortMetric: value,
|
||||
});
|
||||
forceUpdate();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
)}
|
||||
</StyledRowContainer>
|
||||
</CollapsibleControl>
|
||||
</Collapse.Panel>
|
||||
)}
|
||||
</StyledCollapse>
|
||||
|
|
|
|||
|
|
@ -19,23 +19,57 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Filter } from 'src/dashboard/components/nativeFilters/types';
|
||||
import { FormInstance } from 'src/common/components';
|
||||
import { getControlItems, setNativeFilterFieldValues } from './utils';
|
||||
|
||||
import ControlItems from './ControlItems';
|
||||
import getControlItemsMap, { ControlItemsProps } from './getControlItemsMap';
|
||||
|
||||
jest.mock('./utils', () => ({
|
||||
getControlItems: jest.fn(),
|
||||
setNativeFilterFieldValues: jest.fn(),
|
||||
}));
|
||||
|
||||
const createProps = () => ({
|
||||
forceUpdate: jest.fn(),
|
||||
form: 'form',
|
||||
filterId: 'filterId',
|
||||
filterToEdit: '',
|
||||
formFilter: {
|
||||
filterType: 'filterType',
|
||||
const formMock: FormInstance = {
|
||||
__INTERNAL__: { itemRef: () => () => {} },
|
||||
scrollToField: () => {},
|
||||
getFieldInstance: () => {},
|
||||
getFieldValue: () => {},
|
||||
getFieldsValue: () => {},
|
||||
getFieldError: () => [],
|
||||
getFieldsError: () => [],
|
||||
isFieldsTouched: () => false,
|
||||
isFieldTouched: () => false,
|
||||
isFieldValidating: () => false,
|
||||
isFieldsValidating: () => false,
|
||||
resetFields: () => {},
|
||||
setFields: () => {},
|
||||
setFieldsValue: () => {},
|
||||
validateFields: () => Promise.resolve(),
|
||||
submit: () => {},
|
||||
};
|
||||
|
||||
const filterMock: Filter = {
|
||||
cascadeParentIds: [],
|
||||
defaultDataMask: {},
|
||||
isInstant: false,
|
||||
id: 'mock',
|
||||
name: 'mock',
|
||||
scope: {
|
||||
rootPath: [],
|
||||
excluded: [],
|
||||
},
|
||||
filterType: '',
|
||||
targets: [{}],
|
||||
controlValues: {},
|
||||
};
|
||||
|
||||
const createProps: () => ControlItemsProps = () => ({
|
||||
disabled: false,
|
||||
forceUpdate: jest.fn(),
|
||||
form: formMock,
|
||||
filterId: 'filterId',
|
||||
filterToEdit: filterMock,
|
||||
filterType: 'filterType',
|
||||
});
|
||||
|
||||
const createControlItems = () => [
|
||||
|
|
@ -49,33 +83,32 @@ beforeEach(() => {
|
|||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function renderControlItems(controlItemsMap: {}): any {
|
||||
return render(<>{Object.values(controlItemsMap).map(value => value)}</>);
|
||||
}
|
||||
|
||||
test('Should render null when has no "formFilter"', () => {
|
||||
const defaultProps = createProps();
|
||||
const props = {
|
||||
forceUpdate: defaultProps.forceUpdate,
|
||||
form: defaultProps.form,
|
||||
filterId: defaultProps.filterId,
|
||||
};
|
||||
const { container } = render(<ControlItems {...(props as any)} />);
|
||||
const props = createProps();
|
||||
const controlItemsMap = getControlItemsMap(props);
|
||||
const { container } = renderControlItems(controlItemsMap);
|
||||
expect(container.children).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Should render null when has no "formFilter.filterType" is falsy value', () => {
|
||||
const defaultProps = createProps();
|
||||
const props = {
|
||||
forceUpdate: defaultProps.forceUpdate,
|
||||
form: defaultProps.form,
|
||||
filterId: defaultProps.filterId,
|
||||
formFilter: { name: 'name', filterType: 'filterType' },
|
||||
};
|
||||
const { container } = render(<ControlItems {...(props as any)} />);
|
||||
const props = createProps();
|
||||
const controlItemsMap = getControlItemsMap({
|
||||
...props,
|
||||
filterType: 'filterType',
|
||||
});
|
||||
const { container } = renderControlItems(controlItemsMap);
|
||||
expect(container.children).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Should render null empty when "getControlItems" return []', () => {
|
||||
const props = createProps();
|
||||
(getControlItems as jest.Mock).mockReturnValue([]);
|
||||
const { container } = render(<ControlItems {...(props as any)} />);
|
||||
const controlItemsMap = getControlItemsMap(props);
|
||||
const { container } = renderControlItems(controlItemsMap);
|
||||
expect(container.children).toHaveLength(0);
|
||||
});
|
||||
|
||||
|
|
@ -83,8 +116,8 @@ test('Should render null empty when "controlItems" are falsy', () => {
|
|||
const props = createProps();
|
||||
const controlItems = [null, false, {}, { config: { renderTrigger: false } }];
|
||||
(getControlItems as jest.Mock).mockReturnValue(controlItems);
|
||||
|
||||
const { container } = render(<ControlItems {...(props as any)} />);
|
||||
const controlItemsMap = getControlItemsMap(props);
|
||||
const { container } = renderControlItems(controlItemsMap);
|
||||
expect(container.children).toHaveLength(0);
|
||||
});
|
||||
|
||||
|
|
@ -96,16 +129,16 @@ test('Should render render ControlItems', () => {
|
|||
{ name: 'name_2', config: { renderTrigger: true } },
|
||||
];
|
||||
(getControlItems as jest.Mock).mockReturnValue(controlItems);
|
||||
|
||||
render(<ControlItems {...(props as any)} />);
|
||||
const controlItemsMap = getControlItemsMap(props);
|
||||
renderControlItems(controlItemsMap);
|
||||
expect(screen.getAllByRole('checkbox')).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('Clickin on checkbox', () => {
|
||||
const props = createProps();
|
||||
(getControlItems as jest.Mock).mockReturnValue(createControlItems());
|
||||
render(<ControlItems {...(props as any)} />);
|
||||
|
||||
const controlItemsMap = getControlItemsMap(props);
|
||||
renderControlItems(controlItemsMap);
|
||||
expect(props.forceUpdate).not.toBeCalled();
|
||||
expect(setNativeFilterFieldValues).not.toBeCalled();
|
||||
userEvent.click(screen.getByRole('checkbox'));
|
||||
|
|
@ -118,8 +151,8 @@ test('Clickin on checkbox when resetConfig:flase', () => {
|
|||
(getControlItems as jest.Mock).mockReturnValue([
|
||||
{ name: 'name_1', config: { renderTrigger: true, resetConfig: false } },
|
||||
]);
|
||||
render(<ControlItems {...(props as any)} />);
|
||||
|
||||
const controlItemsMap = getControlItemsMap(props);
|
||||
renderControlItems(controlItemsMap);
|
||||
expect(props.forceUpdate).not.toBeCalled();
|
||||
expect(setNativeFilterFieldValues).not.toBeCalled();
|
||||
userEvent.click(screen.getByRole('checkbox'));
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* 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 {
|
||||
CustomControlItem,
|
||||
InfoTooltipWithTrigger,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import React from 'react';
|
||||
import { Checkbox } from 'src/common/components';
|
||||
import { FormInstance } from 'antd/lib/form';
|
||||
import { getChartControlPanelRegistry, t } from '@superset-ui/core';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import { getControlItems, setNativeFilterFieldValues } from './utils';
|
||||
import { NativeFiltersForm } from '../types';
|
||||
import { StyledRowFormItem } from './FiltersConfigForm';
|
||||
import { Filter } from '../../types';
|
||||
|
||||
export interface ControlItemsProps {
|
||||
disabled: boolean;
|
||||
forceUpdate: Function;
|
||||
form: FormInstance<NativeFiltersForm>;
|
||||
filterId: string;
|
||||
filterType: string;
|
||||
filterToEdit?: Filter;
|
||||
}
|
||||
|
||||
export default function getControlItemsMap({
|
||||
disabled,
|
||||
forceUpdate,
|
||||
form,
|
||||
filterId,
|
||||
filterType,
|
||||
filterToEdit,
|
||||
}: ControlItemsProps) {
|
||||
const controlPanelRegistry = getChartControlPanelRegistry();
|
||||
const controlItems =
|
||||
getControlItems(controlPanelRegistry.get(filterType)) ?? [];
|
||||
const map = {};
|
||||
|
||||
controlItems
|
||||
.filter(
|
||||
(controlItem: CustomControlItem) =>
|
||||
controlItem?.config?.renderTrigger &&
|
||||
controlItem.name !== 'sortAscending',
|
||||
)
|
||||
.forEach(controlItem => {
|
||||
const element = (
|
||||
<Tooltip
|
||||
key={controlItem.name}
|
||||
placement="left"
|
||||
title={
|
||||
controlItem.config.affectsDataMask &&
|
||||
disabled &&
|
||||
t('Populate "Default value" to enable this control')
|
||||
}
|
||||
>
|
||||
<StyledRowFormItem
|
||||
key={controlItem.name}
|
||||
name={['filters', filterId, 'controlValues', controlItem.name]}
|
||||
initialValue={
|
||||
filterToEdit?.controlValues?.[controlItem.name] ??
|
||||
controlItem?.config?.default
|
||||
}
|
||||
valuePropName="checked"
|
||||
colon={false}
|
||||
>
|
||||
<Checkbox
|
||||
disabled={controlItem.config.affectsDataMask && disabled}
|
||||
onChange={() => {
|
||||
if (!controlItem.config.resetConfig) {
|
||||
forceUpdate();
|
||||
return;
|
||||
}
|
||||
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;
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ const StyledModalWrapper = styled(StyledModal)`
|
|||
|
||||
export const StyledModalBody = styled.div`
|
||||
display: flex;
|
||||
height: 500px;
|
||||
height: 700px;
|
||||
flex-direction: row;
|
||||
.filters-list {
|
||||
width: ${({ theme }) => theme.gridUnit * 50}px;
|
||||
|
|
|
|||
Loading…
Reference in New Issue