chore: Rename and reposition native filter modal fields (#18558)

* chore: Rename and reposition native filter modal fields

* Fixes collapse initial state

* Changes the button to sentence case
This commit is contained in:
Michael S. Molina 2022-02-08 16:36:40 -03:00 committed by GitHub
parent 125be78ee6
commit fdf57cc22a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 399 additions and 434 deletions

View File

@ -20,6 +20,7 @@ import qs from 'querystring';
import { dashboardView, nativeFilters } from 'cypress/support/directories';
import { testItems } from './dashboard.helper';
import { DASHBOARD_LIST } from '../dashboard_list/dashboard_list.helper';
import { CHART_LIST } from '../chart_list/chart_list.helper';
const getTestTitle = (
test: Mocha.Suite = (Cypress as any).mocha.getRunner().suite.ctx.test,
@ -43,7 +44,8 @@ describe('Nativefilters Sanity test', () => {
).then(xhr => {
const dashboards = xhr.body.result;
const worldBankDashboard = dashboards.find(
d => d.dashboard_title === "World Bank's Data",
(d: { dashboard_title: string }) =>
d.dashboard_title === "World Bank's Data",
);
cy.visit(worldBankDashboard.url);
});
@ -65,7 +67,8 @@ describe('Nativefilters Sanity test', () => {
).then(xhr => {
const dashboards = xhr.body.result;
const testDashboard = dashboards.find(
d => d.dashboard_title === testItems.dashboard,
(d: { dashboard_title: string }) =>
d.dashboard_title === testItems.dashboard,
);
cy.visit(testDashboard.url);
});
@ -107,13 +110,14 @@ describe('Nativefilters Sanity test', () => {
cy.get(nativeFilters.createFilterButton).should('be.visible').click();
cy.get(nativeFilters.modal.container)
.find(nativeFilters.filtersPanel.filterName)
.click()
.click({ force: true })
.type('Country name');
cy.get(nativeFilters.modal.container)
.find(nativeFilters.filtersPanel.datasetName)
.click()
.type('wb_health_population{enter}');
.click({ force: true })
.within(() =>
cy.get('input').type('wb_health_population{enter}', { force: true }),
);
// Add following step to avoid flaky enter value in line 177
cy.get(nativeFilters.filtersPanel.inputDropdown)
.should('be.visible', { timeout: 20000 })
@ -163,7 +167,7 @@ describe('Nativefilters Sanity test', () => {
cy.get(nativeFilters.createFilterButton).click({ force: true });
cy.get(nativeFilters.modal.container)
.find(nativeFilters.filtersPanel.filterName)
.click()
.click({ force: true })
.type('suffix');
cy.get(nativeFilters.modal.container)
.find(nativeFilters.filtersPanel.datasetName)
@ -218,12 +222,14 @@ describe('Nativefilters Sanity test', () => {
cy.get(nativeFilters.modal.container).should('be.visible');
cy.get(nativeFilters.modal.container)
.find(nativeFilters.filtersPanel.filterName)
.click()
.click({ force: true })
.type('Country name');
cy.get(nativeFilters.modal.container)
.find(nativeFilters.filtersPanel.datasetName)
.click()
.type('wb_health_population{enter}');
.click({ force: true })
.within(() =>
cy.get('input').type('wb_health_population{enter}', { force: true }),
);
cy.get('.loading inline-centered css-101mkpk').should('not.exist');
// hack for unclickable country_name
@ -255,53 +261,42 @@ describe('Nativefilters Sanity test', () => {
cy.get(nativeFilters.filterFromDashboardView.expand).click({ force: true });
cy.get(nativeFilters.createFilterButton).should('be.visible').click();
cy.get(nativeFilters.modal.container).should('be.visible');
cy.get(nativeFilters.filterConfigurationSections.collapseExpandButton)
.last()
.click();
[
'Filter has default value',
'Multiple select',
'Required',
'Can select multiple values',
'Filter value is required',
'Filter is hierarchical',
'Default to first item',
'Select first filter value by default',
'Inverse selection',
'Search all filter options',
'Dynamically search all filter values',
'Pre-filter available values',
'Sort filter values',
].forEach(el => {
cy.contains(el);
});
cy.get(nativeFilters.filterConfigurationSections.checkedCheckbox).contains(
'Multiple select',
'Can select multiple values',
);
cy.get(nativeFilters.filterConfigurationSections.infoTooltip)
.eq(0)
.trigger('mouseover');
cy.contains('Allow selecting multiple values');
cy.get(nativeFilters.filterConfigurationSections.infoTooltip)
.eq(1)
.trigger('mouseover');
.trigger('mouseover', { force: true });
cy.contains('User must select a value before applying the filter');
cy.get(nativeFilters.filterConfigurationSections.infoTooltip)
.eq(1)
.trigger('mouseover', { force: true });
cy.contains('When using this option, default value cant be set');
cy.get(nativeFilters.filterConfigurationSections.infoTooltip)
.eq(2)
.trigger('mouseover');
cy.contains(
'Select first item by default (when using this option, default value cant be set)',
);
cy.get(nativeFilters.filterConfigurationSections.infoTooltip)
.eq(3)
.trigger('mouseover');
cy.contains('Exclude selected values');
cy.get(nativeFilters.filterConfigurationSections.infoTooltip)
.eq(4)
.trigger('mouseover');
.trigger('mouseover', { force: true });
cy.contains(
'By default, each filter loads at most 1000 choices at the initial page load. Check this box if you have more than 1000 filter values and want to enable dynamically searching that loads filter values as users type (may add stress to your database).',
);
cy.get(nativeFilters.filterConfigurationSections.infoTooltip)
.eq(3)
.trigger('mouseover', { force: true });
cy.contains('Exclude selected values');
});
it("User can check 'Filter has default value'", () => {
cy.get(nativeFilters.filterFromDashboardView.expand).click({ force: true });
@ -312,11 +307,13 @@ describe('Nativefilters Sanity test', () => {
cy.get(nativeFilters.modal.container)
.find(nativeFilters.filtersPanel.datasetName)
.click()
.type('wb_health_population{enter}');
.click({ force: true })
.within(() =>
cy.get('input').type('wb_health_population{enter}', { force: true }),
);
cy.get(nativeFilters.modal.container)
.find(nativeFilters.filtersPanel.filterName)
.click()
.click({ force: true })
.type('country_name');
// hack for unclickable datetime
cy.wait(5000);

View File

@ -100,7 +100,7 @@ test('remove filter', async () => {
test('add filter', async () => {
defaultRender();
// First trash icon
const addButton = screen.getByText('Add')!;
const addButton = screen.getByText('Add filters and dividers')!;
fireEvent.mouseOver(addButton);
const addFilterButton = await screen.findByText('Filter');
@ -118,7 +118,7 @@ test('add filter', async () => {
test('add divider', async () => {
defaultRender();
const addButton = screen.getByText('Add')!;
const addButton = screen.getByText('Add filters and dividers')!;
fireEvent.mouseOver(addButton);
const addFilterButton = await screen.findByText('Divider');
await act(async () => {

View File

@ -36,27 +36,13 @@ interface Props {
erroredFilters: string[];
}
const StyledPlusButton = styled.div`
color: ${({ theme }) => theme.colors.primary.dark1};
`;
const StyledHeader = styled.div`
${({ theme }) => `
color: ${theme.colors.grayscale.dark1};
font-size: ${theme.typography.sizes.l}px;
padding-top: ${theme.gridUnit * 4}px;
padding-right: ${theme.gridUnit * 4}px;
padding-left: ${theme.gridUnit * 4}px;
padding-bottom: ${theme.gridUnit * 2}px;
`}
`;
const StyledAddBox = styled.div`
${({ theme }) => `
cursor: pointer;
margin: ${theme.gridUnit * 4}px;
color: ${theme.colors.primary.base};
&:hover {
color: ${theme.colors.primary.base};
color: ${theme.colors.primary.dark1};
}
`}
`;
@ -104,7 +90,12 @@ const FilterTitlePane: React.FC<Props> = ({
);
return (
<TabsContainer>
<StyledHeader>Filters</StyledHeader>
<Dropdown overlay={menu} arrow placement="topLeft" trigger={['hover']}>
<StyledAddBox>
<div data-test="new-dropdown-icon" className="fa fa-plus" />{' '}
<span>{t('Add filters and dividers')}</span>
</StyledAddBox>
</Dropdown>
<div
css={{
height: '100%',
@ -124,15 +115,6 @@ const FilterTitlePane: React.FC<Props> = ({
restoreFilter={restoreFilter}
/>
</div>
<Dropdown overlay={menu} arrow placement="topLeft" trigger={['hover']}>
<StyledAddBox>
<StyledPlusButton
data-test="new-dropdown-icon"
className="fa fa-plus"
/>{' '}
<span>{t('Add')}</span>
</StyledAddBox>
</Dropdown>
</TabsContainer>
);
};

View File

@ -43,7 +43,7 @@ describe('FilterScope', () => {
save,
removedFilters: {},
handleActiveFilterPanelChange: jest.fn(),
activeFilterPanelKeys: `DefaultFilterId-${FilterPanels.basic.key}`,
activeFilterPanelKeys: `DefaultFilterId-${FilterPanels.configuration.key}`,
isActive: true,
};

View File

@ -45,6 +45,7 @@ import React, {
useMemo,
useState,
} from 'react';
import { PluginFilterSelectCustomizeProps } from 'src/filters/components/Select/types';
import { useSelector } from 'react-redux';
import { getChartDataRequest } from 'src/chart/chartAction';
import { Input, TextArea } from 'src/common/components';
@ -118,6 +119,16 @@ const StyledRowContainer = styled.div`
padding: 0px ${({ theme }) => theme.gridUnit * 4}px;
`;
type ControlKey = keyof PluginFilterSelectCustomizeProps;
const controlsOrder: ControlKey[] = [
'enableEmptyFilter',
'defaultToFirstItem',
'multiSelect',
'searchAllOptions',
'inverseSelection',
];
export const StyledFormItem = styled(FormItem)`
width: 49%;
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
@ -249,7 +260,7 @@ const StyledAsterisk = styled.span`
const FilterTabs = {
configuration: {
key: 'configuration',
name: t('Configuration'),
name: t('Settings'),
},
scoping: {
key: 'scoping',
@ -258,13 +269,13 @@ const FilterTabs = {
};
export const FilterPanels = {
basic: {
key: 'basic',
name: t('Basic'),
configuration: {
key: 'configuration',
name: t('Filter Configuration'),
},
advanced: {
key: 'advanced',
name: t('Advanced'),
settings: {
key: 'settings',
name: t('Filter Settings'),
},
};
@ -287,8 +298,6 @@ export interface FiltersConfigFormProps {
const FILTERS_WITH_ADHOC_FILTERS = ['filter_select', 'filter_range'];
const BASIC_CONTROL_ITEMS = ['enableEmptyFilter', 'multiSelect'];
// TODO: Rename the filter plugins and remove this mapping
const FILTER_TYPE_NAME_MAPPING = {
[t('Select filter')]: t('Value'),
@ -319,7 +328,6 @@ const FiltersConfigForm = (
form,
parentFilters,
activeFilterPanelKeys,
isActive,
restoreFilter,
onFilterHierarchyChange,
handleActiveFilterPanelChange,
@ -607,10 +615,6 @@ const FiltersConfigForm = (
const defaultToFirstItem = formFilter?.controlValues?.defaultToFirstItem;
const hasAdvancedSection =
formFilter?.filterType === 'filter_select' ||
formFilter?.filterType === 'filter_range';
const initialDefaultValue =
formFilter?.filterType === filterToEdit?.filterType
? filterToEdit?.defaultDataMask
@ -690,28 +694,6 @@ const FiltersConfigForm = (
showDataset,
]);
useEffect(() => {
// Run only once
if (isActive) {
const hasCheckedAdvancedControl =
hasParentFilter ||
hasPreFilter ||
hasSorting ||
hasEnableSingleValue ||
Object.keys(controlItems)
.filter(key => !BASIC_CONTROL_ITEMS.includes(key))
.some(key => controlItems[key].checked);
handleActiveFilterPanelChange(
hasCheckedAdvancedControl
? [
`${filterId}-${FilterPanels.basic.key}`,
`${filterId}-${FilterPanels.advanced.key}`,
]
: `${filterId}-${FilterPanels.basic.key}`,
);
}
}, [isActive]);
const initiallyExcludedCharts = useMemo(() => {
const excluded: number[] = [];
if (formFilter?.dataset?.value === undefined) {
@ -879,9 +861,280 @@ const FiltersConfigForm = (
>
<Collapse.Panel
forceRender
header={FilterPanels.basic.name}
key={`${filterId}-${FilterPanels.basic.key}`}
header={FilterPanels.configuration.name}
key={`${filterId}-${FilterPanels.configuration.key}`}
>
{isCascadingFilter && (
<CleanFormItem name={['filters', filterId, 'hierarchicalFilter']}>
<CollapsibleControl
title={t('Filter is hierarchical')}
initialValue={hasParentFilter}
onChange={checked => {
formChanged();
// execute after render
setTimeout(() => {
if (checked) {
form.validateFields([
['filters', filterId, 'parentFilter'],
]);
} else {
setNativeFilterFieldValues(form, filterId, {
parentFilter: undefined,
});
}
onFilterHierarchyChange(
filterId,
checked
? form.getFieldValue('filters')[filterId].parentFilter
: undefined,
);
}, 0);
}}
>
<StyledRowSubFormItem
name={['filters', filterId, 'parentFilter']}
label={<StyledLabel>{t('Parent filter')}</StyledLabel>}
initialValue={parentFilter}
normalize={value => (value ? { value } : undefined)}
data-test="parent-filter-input"
required
rules={[
{
required: true,
message: t('Parent filter is required'),
},
]}
>
<ParentSelect />
</StyledRowSubFormItem>
</CollapsibleControl>
</CleanFormItem>
)}
{hasDataset && hasAdditionalFilters && (
<CleanFormItem name={['filters', filterId, 'preFilter']}>
<CollapsibleControl
initialValue={hasPreFilter}
title={t('Pre-filter available values')}
onChange={checked => {
formChanged();
if (checked) {
validatePreFilter();
}
}}
>
<StyledRowSubFormItem
name={['filters', filterId, 'adhoc_filters']}
initialValue={filterToEdit?.adhoc_filters}
required
rules={[
{
validator: preFilterValidator,
},
]}
>
<AdhocFilterControl
columns={
datasetDetails?.columns?.filter(
(c: ColumnMeta) => c.filterable,
) || []
}
savedMetrics={datasetDetails?.metrics || []}
datasource={datasetDetails}
onChange={(filters: AdhocFilter[]) => {
setNativeFilterFieldValues(form, filterId, {
adhoc_filters: filters,
});
forceUpdate();
validatePreFilter();
}}
label={
<span>
<StyledLabel>{t('Pre-filter')}</StyledLabel>
{!hasTimeRange && <StyledAsterisk />}
</span>
}
/>
</StyledRowSubFormItem>
{showTimeRangePicker && (
<StyledRowFormItem
name={['filters', filterId, 'time_range']}
label={<StyledLabel>{t('Time range')}</StyledLabel>}
initialValue={filterToEdit?.time_range || 'No filter'}
required={!hasAdhoc}
rules={[
{
validator: preFilterValidator,
},
]}
>
<DateFilterControl
name="time_range"
endpoints={['inclusive', 'exclusive']}
onChange={timeRange => {
setNativeFilterFieldValues(form, filterId, {
time_range: timeRange,
});
forceUpdate();
validatePreFilter();
}}
/>
</StyledRowFormItem>
)}
{hasTimeRange && (
<StyledRowFormItem
name={['filters', filterId, 'granularity_sqla']}
label={
<>
<StyledLabel>{t('Time column')}</StyledLabel>&nbsp;
<InfoTooltipWithTrigger
placement="top"
tooltip={t(
'Optional time column if time range should apply to another column than the default time column',
)}
/>
</>
}
initialValue={filterToEdit?.granularity_sqla}
>
<ColumnSelect
allowClear
form={form}
formField="granularity_sqla"
filterId={filterId}
filterValues={(column: Column) => !!column.is_dttm}
datasetId={datasetId}
onChange={column => {
// We need reset default value when when column changed
setNativeFilterFieldValues(form, filterId, {
granularity_sqla: column,
});
forceUpdate();
}}
/>
</StyledRowFormItem>
)}
</CollapsibleControl>
</CleanFormItem>
)}
{formFilter?.filterType !== 'filter_range' ? (
<CleanFormItem name={['filters', filterId, 'sortFilter']}>
<CollapsibleControl
initialValue={hasSorting}
title={t('Sort filter values')}
onChange={checked => {
onSortChanged(checked || undefined);
formChanged();
}}
>
<StyledRowFormItem
name={[
'filters',
filterId,
'controlValues',
'sortAscending',
]}
initialValue={sort}
label={<StyledLabel>{t('Sort type')}</StyledLabel>}
>
<Radio.Group
onChange={value => {
onSortChanged(value.target.value);
}}
>
<Radio value>{t('Sort ascending')}</Radio>
<Radio value={false}>{t('Sort descending')}</Radio>
</Radio.Group>
</StyledRowFormItem>
{hasMetrics && (
<StyledRowSubFormItem
name={['filters', filterId, 'sortMetric']}
initialValue={filterToEdit?.sortMetric}
label={
<>
<StyledLabel>{t('Sort Metric')}</StyledLabel>&nbsp;
<InfoTooltipWithTrigger
placement="top"
tooltip={t(
'If a metric is specified, sorting will be done based on the metric value',
)}
/>
</>
}
data-test="field-input"
>
<Select
allowClear
ariaLabel={t('Sort metric')}
name="sortMetric"
options={metrics.map((metric: Metric) => ({
value: metric.metric_name,
label: metric.verbose_name ?? metric.metric_name,
}))}
onChange={value => {
if (value !== undefined) {
setNativeFilterFieldValues(form, filterId, {
sortMetric: value,
});
forceUpdate();
}
}}
/>
</StyledRowSubFormItem>
)}
</CollapsibleControl>
</CleanFormItem>
) : (
<CleanFormItem name={['filters', filterId, 'rangeFilter']}>
<CollapsibleControl
initialValue={hasEnableSingleValue}
title={t('Single Value')}
onChange={checked => {
onEnableSingleValueChanged(
checked ? SingleValueType.Exact : undefined,
);
formChanged();
}}
>
<StyledRowFormItem
name={[
'filters',
filterId,
'controlValues',
'enableSingleValue',
]}
initialValue={enableSingleValue}
label={<StyledLabel>{t('Single value type')}</StyledLabel>}
>
<Radio.Group
onChange={value =>
onEnableSingleValueChanged(value.target.value)
}
>
<Radio value={SingleValueType.Minimum}>
{t('Minimum')}
</Radio>
<Radio value={SingleValueType.Exact}>{t('Exact')}</Radio>
<Radio value={SingleValueType.Maximum}>
{t('Maximum')}
</Radio>
</Radio.Group>
</StyledRowFormItem>
</CollapsibleControl>
</CleanFormItem>
)}
</Collapse.Panel>
<Collapse.Panel
forceRender
header={FilterPanels.settings.name}
key={`${filterId}-${FilterPanels.settings.key}`}
>
<StyledFormItem
name={['filters', filterId, 'description']}
initialValue={filterToEdit?.description}
label={<StyledLabel>{t('Description')}</StyledLabel>}
>
<TextArea />
</StyledFormItem>
<CleanFormItem
name={['filters', filterId, 'defaultValueQueriesData']}
hidden
@ -986,292 +1239,13 @@ const FiltersConfigForm = (
</CollapsibleControl>
</CleanFormItem>
{Object.keys(controlItems)
.filter(key => BASIC_CONTROL_ITEMS.includes(key))
.sort(
(a, b) =>
controlsOrder.indexOf(a as ControlKey) -
controlsOrder.indexOf(b as ControlKey),
)
.map(key => controlItems[key].element)}
<StyledFormItem
name={['filters', filterId, 'description']}
initialValue={filterToEdit?.description}
label={<StyledLabel>{t('Description')}</StyledLabel>}
>
<TextArea />
</StyledFormItem>
</Collapse.Panel>
{hasAdvancedSection && (
<Collapse.Panel
forceRender
header={FilterPanels.advanced.name}
key={`${filterId}-${FilterPanels.advanced.key}`}
>
{isCascadingFilter && (
<CleanFormItem
name={['filters', filterId, 'hierarchicalFilter']}
>
<CollapsibleControl
title={t('Filter is hierarchical')}
initialValue={hasParentFilter}
onChange={checked => {
formChanged();
// execute after render
setTimeout(() => {
if (checked) {
form.validateFields([
['filters', filterId, 'parentFilter'],
]);
} else {
setNativeFilterFieldValues(form, filterId, {
parentFilter: undefined,
});
}
onFilterHierarchyChange(
filterId,
checked
? form.getFieldValue('filters')[filterId]
.parentFilter
: undefined,
);
}, 0);
}}
>
<StyledRowSubFormItem
name={['filters', filterId, 'parentFilter']}
label={<StyledLabel>{t('Parent filter')}</StyledLabel>}
initialValue={parentFilter}
normalize={value => (value ? { value } : undefined)}
data-test="parent-filter-input"
required
rules={[
{
required: true,
message: t('Parent filter is required'),
},
]}
>
<ParentSelect />
</StyledRowSubFormItem>
</CollapsibleControl>
</CleanFormItem>
)}
{Object.keys(controlItems)
.filter(key => !BASIC_CONTROL_ITEMS.includes(key))
.map(key => controlItems[key].element)}
{hasDataset && hasAdditionalFilters && (
<CleanFormItem name={['filters', filterId, 'preFilter']}>
<CollapsibleControl
initialValue={hasPreFilter}
title={t('Pre-filter available values')}
onChange={checked => {
formChanged();
if (checked) {
validatePreFilter();
}
}}
>
<StyledRowSubFormItem
name={['filters', filterId, 'adhoc_filters']}
initialValue={filterToEdit?.adhoc_filters}
required
rules={[
{
validator: preFilterValidator,
},
]}
>
<AdhocFilterControl
columns={
datasetDetails?.columns?.filter(
(c: ColumnMeta) => c.filterable,
) || []
}
savedMetrics={datasetDetails?.metrics || []}
datasource={datasetDetails}
onChange={(filters: AdhocFilter[]) => {
setNativeFilterFieldValues(form, filterId, {
adhoc_filters: filters,
});
forceUpdate();
validatePreFilter();
}}
label={
<span>
<StyledLabel>{t('Pre-filter')}</StyledLabel>
{!hasTimeRange && <StyledAsterisk />}
</span>
}
/>
</StyledRowSubFormItem>
{showTimeRangePicker && (
<StyledRowFormItem
name={['filters', filterId, 'time_range']}
label={<StyledLabel>{t('Time range')}</StyledLabel>}
initialValue={filterToEdit?.time_range || 'No filter'}
required={!hasAdhoc}
rules={[
{
validator: preFilterValidator,
},
]}
>
<DateFilterControl
name="time_range"
endpoints={['inclusive', 'exclusive']}
onChange={timeRange => {
setNativeFilterFieldValues(form, filterId, {
time_range: timeRange,
});
forceUpdate();
validatePreFilter();
}}
/>
</StyledRowFormItem>
)}
{hasTimeRange && (
<StyledRowFormItem
name={['filters', filterId, 'granularity_sqla']}
label={
<>
<StyledLabel>{t('Time column')}</StyledLabel>&nbsp;
<InfoTooltipWithTrigger
placement="top"
tooltip={t(
'Optional time column if time range should apply to another column than the default time column',
)}
/>
</>
}
initialValue={filterToEdit?.granularity_sqla}
>
<ColumnSelect
allowClear
form={form}
formField="granularity_sqla"
filterId={filterId}
filterValues={(column: Column) => !!column.is_dttm}
datasetId={datasetId}
onChange={column => {
// We need reset default value when when column changed
setNativeFilterFieldValues(form, filterId, {
granularity_sqla: column,
});
forceUpdate();
}}
/>
</StyledRowFormItem>
)}
</CollapsibleControl>
</CleanFormItem>
)}
{formFilter?.filterType !== 'filter_range' ? (
<CleanFormItem name={['filters', filterId, 'sortFilter']}>
<CollapsibleControl
initialValue={hasSorting}
title={t('Sort filter values')}
onChange={checked => {
onSortChanged(checked || undefined);
formChanged();
}}
>
<StyledRowFormItem
name={[
'filters',
filterId,
'controlValues',
'sortAscending',
]}
initialValue={sort}
label={<StyledLabel>{t('Sort type')}</StyledLabel>}
>
<Radio.Group
onChange={value => {
onSortChanged(value.target.value);
}}
>
<Radio value>{t('Sort ascending')}</Radio>
<Radio value={false}>{t('Sort descending')}</Radio>
</Radio.Group>
</StyledRowFormItem>
{hasMetrics && (
<StyledRowSubFormItem
name={['filters', filterId, 'sortMetric']}
initialValue={filterToEdit?.sortMetric}
label={
<>
<StyledLabel>{t('Sort Metric')}</StyledLabel>&nbsp;
<InfoTooltipWithTrigger
placement="top"
tooltip={t(
'If a metric is specified, sorting will be done based on the metric value',
)}
/>
</>
}
data-test="field-input"
>
<Select
allowClear
ariaLabel={t('Sort metric')}
name="sortMetric"
options={metrics.map((metric: Metric) => ({
value: metric.metric_name,
label: metric.verbose_name ?? metric.metric_name,
}))}
onChange={value => {
if (value !== undefined) {
setNativeFilterFieldValues(form, filterId, {
sortMetric: value,
});
forceUpdate();
}
}}
/>
</StyledRowSubFormItem>
)}
</CollapsibleControl>
</CleanFormItem>
) : (
<CleanFormItem name={['filters', filterId, 'rangeFilter']}>
<CollapsibleControl
initialValue={hasEnableSingleValue}
title={t('Single Value')}
onChange={checked => {
onEnableSingleValueChanged(
checked ? SingleValueType.Exact : undefined,
);
formChanged();
}}
>
<StyledRowFormItem
name={[
'filters',
filterId,
'controlValues',
'enableSingleValue',
]}
initialValue={enableSingleValue}
label={
<StyledLabel>{t('Single value type')}</StyledLabel>
}
>
<Radio.Group
onChange={value =>
onEnableSingleValueChanged(value.target.value)
}
>
<Radio value={SingleValueType.Minimum}>
{t('Minimum')}
</Radio>
<Radio value={SingleValueType.Exact}>
{t('Exact')}
</Radio>
<Radio value={SingleValueType.Maximum}>
{t('Maximum')}
</Radio>
</Radio.Group>
</StyledRowFormItem>
</CollapsibleControl>
</CleanFormItem>
)}
</Collapse.Panel>
)}
</StyledCollapse>
</TabPane>
<TabPane

View File

@ -87,10 +87,12 @@ export const useDefaultValue = (
let tooltip = '';
if (defaultToFirstItem) {
tooltip = t(
'Default value set automatically when "Default to first item" is checked',
'Default value set automatically when "Select first filter value by default" is checked',
);
} else if (isRequired) {
tooltip = t('Default value must be set when "Required" is checked');
tooltip = t(
'Default value must be set when "Filter value is required" is checked',
);
} else if (hasDefaultValue) {
tooltip = t(
'Default value must be set when "Filter has default value" is checked',

View File

@ -120,14 +120,14 @@ const NUMERICAL_RANGE_REGEX = /^numerical range$/i;
const TIME_RANGE_REGEX = /^time range$/i;
const TIME_COLUMN_REGEX = /^time column$/i;
const TIME_GRAIN_REGEX = /^time grain$/i;
const ADVANCED_REGEX = /^advanced$/i;
const FILTER_SETTINGS_REGEX = /^filter settings$/i;
const DEFAULT_VALUE_REGEX = /^filter has default value$/i;
const MULTIPLE_REGEX = /^multiple select$/i;
const REQUIRED_REGEX = /^required$/i;
const MULTIPLE_REGEX = /^can select multiple values$/i;
const REQUIRED_REGEX = /^filter value is required$/i;
const HIERARCHICAL_REGEX = /^filter is hierarchical$/i;
const FIRST_ITEM_REGEX = /^default to first item$/i;
const FIRST_VALUE_REGEX = /^select first filter value by default$/i;
const INVERSE_SELECTION_REGEX = /^inverse selection$/i;
const SEARCH_ALL_REGEX = /^search all filter options$/i;
const SEARCH_ALL_REGEX = /^dynamically search all filter values$/i;
const PRE_FILTER_REGEX = /^pre-filter available values$/i;
const SORT_REGEX = /^sort filter values$/i;
const SAVE_REGEX = /^save$/i;
@ -169,7 +169,7 @@ function queryCheckbox(name: RegExp) {
test('renders a value filter type', () => {
defaultRender();
userEvent.click(screen.getByText(ADVANCED_REGEX));
userEvent.click(screen.getByText(FILTER_SETTINGS_REGEX));
expect(screen.getByText(FILTER_TYPE_REGEX)).toBeInTheDocument();
expect(screen.getByText(FILTER_NAME_REGEX)).toBeInTheDocument();
@ -179,7 +179,7 @@ test('renders a value filter type', () => {
expect(getCheckbox(DEFAULT_VALUE_REGEX)).not.toBeChecked();
expect(getCheckbox(REQUIRED_REGEX)).not.toBeChecked();
expect(getCheckbox(HIERARCHICAL_REGEX)).not.toBeChecked();
expect(getCheckbox(FIRST_ITEM_REGEX)).not.toBeChecked();
expect(getCheckbox(FIRST_VALUE_REGEX)).not.toBeChecked();
expect(getCheckbox(INVERSE_SELECTION_REGEX)).not.toBeChecked();
expect(getCheckbox(SEARCH_ALL_REGEX)).not.toBeChecked();
expect(getCheckbox(PRE_FILTER_REGEX)).not.toBeChecked();
@ -195,7 +195,7 @@ test('renders a numerical range filter type', async () => {
await waitFor(() => userEvent.click(screen.getByText(NUMERICAL_RANGE_REGEX)));
userEvent.click(screen.getByText(ADVANCED_REGEX));
userEvent.click(screen.getByText(FILTER_SETTINGS_REGEX));
expect(screen.getByText(FILTER_TYPE_REGEX)).toBeInTheDocument();
expect(screen.getByText(FILTER_NAME_REGEX)).toBeInTheDocument();
@ -208,7 +208,7 @@ test('renders a numerical range filter type', async () => {
expect(queryCheckbox(MULTIPLE_REGEX)).not.toBeInTheDocument();
expect(queryCheckbox(HIERARCHICAL_REGEX)).not.toBeInTheDocument();
expect(queryCheckbox(FIRST_ITEM_REGEX)).not.toBeInTheDocument();
expect(queryCheckbox(FIRST_VALUE_REGEX)).not.toBeInTheDocument();
expect(queryCheckbox(INVERSE_SELECTION_REGEX)).not.toBeInTheDocument();
expect(queryCheckbox(SEARCH_ALL_REGEX)).not.toBeInTheDocument();
expect(queryCheckbox(SORT_REGEX)).not.toBeInTheDocument();
@ -227,8 +227,6 @@ test('renders a time range filter type', async () => {
expect(screen.queryByText(COLUMN_REGEX)).not.toBeInTheDocument();
expect(getCheckbox(DEFAULT_VALUE_REGEX)).not.toBeChecked();
expect(screen.queryByText(ADVANCED_REGEX)).not.toBeInTheDocument();
});
test('renders a time column filter type', async () => {
@ -244,8 +242,6 @@ test('renders a time column filter type', async () => {
expect(screen.queryByText(COLUMN_REGEX)).not.toBeInTheDocument();
expect(getCheckbox(DEFAULT_VALUE_REGEX)).not.toBeChecked();
expect(screen.queryByText(ADVANCED_REGEX)).not.toBeInTheDocument();
});
test('renders a time grain filter type', async () => {
@ -261,8 +257,6 @@ test('renders a time grain filter type', async () => {
expect(screen.queryByText(COLUMN_REGEX)).not.toBeInTheDocument();
expect(getCheckbox(DEFAULT_VALUE_REGEX)).not.toBeChecked();
expect(screen.queryByText(ADVANCED_REGEX)).not.toBeInTheDocument();
});
test('render time filter types as disabled if there are no temporal columns in the dataset', async () => {
@ -310,14 +304,14 @@ test.skip('validates the default value', async () => {
test('validates the hierarchical value', async () => {
defaultRender();
userEvent.click(screen.getByText(ADVANCED_REGEX));
userEvent.click(screen.getByText(FILTER_SETTINGS_REGEX));
userEvent.click(getCheckbox(HIERARCHICAL_REGEX));
expect(await screen.findByText(PARENT_REQUIRED_REGEX)).toBeInTheDocument();
});
test('validates the pre-filter value', async () => {
defaultRender();
userEvent.click(screen.getByText(ADVANCED_REGEX));
userEvent.click(screen.getByText(FILTER_SETTINGS_REGEX));
userEvent.click(getCheckbox(PRE_FILTER_REGEX));
expect(
await screen.findByText(PRE_FILTER_REQUIRED_REGEX),
@ -332,7 +326,7 @@ test.skip("doesn't render time range pre-filter if there are no temporal columns
expect(screen.queryByLabelText('Loading')).not.toBeInTheDocument();
userEvent.click(screen.getByText('birth_names'));
});
userEvent.click(screen.getByText(ADVANCED_REGEX));
userEvent.click(screen.getByText(FILTER_SETTINGS_REGEX));
userEvent.click(getCheckbox(PRE_FILTER_REGEX));
await waitFor(() =>
expect(

View File

@ -175,13 +175,18 @@ export function FiltersConfigModal({
buildFilterGroup(filterHierarchy),
);
const getActiveFilterPanelKey = (filterId: string) => [
`${filterId}-${FilterPanels.configuration.key}`,
`${filterId}-${FilterPanels.settings.key}`,
];
const [activeFilterPanelKey, setActiveFilterPanelKey] = useState<
string | string[]
>(`${initialCurrentFilterId}-${FilterPanels.basic.key}`);
>(getActiveFilterPanelKey(initialCurrentFilterId));
const onTabChange = (filterId: string) => {
setCurrentFilterId(filterId);
setActiveFilterPanelKey(`${filterId}-${FilterPanels.basic.key}`);
setActiveFilterPanelKey(getActiveFilterPanelKey(filterId));
};
// generates a new filter id and appends it to the newFilterIds
@ -196,7 +201,7 @@ export function FiltersConfigModal({
{ id: newFilterId, parentId: null },
]);
setOrderedFilters([...orderedFilters, [newFilterId]]);
setActiveFilterPanelKey(`${newFilterId}-${FilterPanels.basic.key}`);
setActiveFilterPanelKey(getActiveFilterPanelKey(newFilterId));
},
[
newFilterIds,
@ -234,6 +239,9 @@ export function FiltersConfigModal({
setSaveAlertVisible(false);
setFormValues({ filters: {} });
setErroredFilters([]);
if (filterIds.length > 0) {
setActiveFilterPanelKey(getActiveFilterPanelKey(filterIds[0]));
}
if (!isSaving) {
const initialFilterHierarchy = getInitialFilterHierarchy();
setFilterHierarchy(initialFilterHierarchy);
@ -464,7 +472,7 @@ export function FiltersConfigModal({
<StyledModalWrapper
visible={isOpen}
maskClosable={false}
title={t('Filters configuration and scoping')}
title={t('Add and edit filters')}
width="50%"
destroyOnClose
onCancel={handleCancel}

View File

@ -56,12 +56,11 @@ const config: ControlPanelConfig = {
name: 'multiSelect',
config: {
type: 'CheckboxControl',
label: t('Multiple select'),
label: t('Can select multiple values'),
default: multiSelect,
affectsDataMask: true,
resetConfig: true,
renderTrigger: true,
description: t('Allow selecting multiple values'),
},
},
],
@ -70,10 +69,12 @@ const config: ControlPanelConfig = {
name: 'enableEmptyFilter',
config: {
type: 'CheckboxControl',
label: t('Required'),
label: t('Filter value is required'),
default: false,
renderTrigger: true,
description: t('User must select a value for this filter.'),
description: t(
'User must select a value before applying the filter',
),
},
},
],

View File

@ -52,10 +52,12 @@ const config: ControlPanelConfig = {
name: 'enableEmptyFilter',
config: {
type: 'CheckboxControl',
label: t('Required'),
label: t('Filter value is required'),
default: false,
renderTrigger: true,
description: t('User must select a value for this filter.'),
description: t(
'User must select a value before applying the filter',
),
},
},
{

View File

@ -74,12 +74,11 @@ const config: ControlPanelConfig = {
name: 'multiSelect',
config: {
type: 'CheckboxControl',
label: t('Multiple select'),
label: t('Can select multiple values'),
default: multiSelect,
resetConfig: true,
affectsDataMask: true,
renderTrigger: true,
description: t('Allow selecting multiple values'),
},
},
],
@ -88,7 +87,7 @@ const config: ControlPanelConfig = {
name: 'enableEmptyFilter',
config: {
type: 'CheckboxControl',
label: t('Required'),
label: t('Filter value is required'),
default: enableEmptyFilter,
renderTrigger: true,
description: t(
@ -102,14 +101,14 @@ const config: ControlPanelConfig = {
name: 'defaultToFirstItem',
config: {
type: 'CheckboxControl',
label: t('Default to first item'),
label: t('Select first filter value by default'),
default: defaultToFirstItem,
resetConfig: true,
affectsDataMask: true,
renderTrigger: true,
requiredFirst: true,
description: t(
'Select first item by default (when using this option, default value cant be set)',
'When using this option, default value cant be set',
),
},
},
@ -134,7 +133,7 @@ const config: ControlPanelConfig = {
type: 'CheckboxControl',
renderTrigger: true,
affectsDataMask: true,
label: t('Search all filter options'),
label: t('Dynamically search all filter values'),
default: searchAllOptions,
description: t(
'By default, each filter loads at most 1000 choices at the initial page load. ' +

View File

@ -31,7 +31,7 @@ import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
export type SelectValue = (number | string)[] | null | undefined;
interface PluginFilterSelectCustomizeProps {
export interface PluginFilterSelectCustomizeProps {
defaultValue?: SelectValue;
enableEmptyFilter: boolean;
inverseSelection: boolean;

View File

@ -50,10 +50,12 @@ const config: ControlPanelConfig = {
name: 'enableEmptyFilter',
config: {
type: 'CheckboxControl',
label: t('Required'),
label: t('Filter value is required'),
default: false,
renderTrigger: true,
description: t('User must select a value for this filter.'),
description: t(
'User must select a value before applying the filter',
),
},
},
],

View File

@ -30,10 +30,12 @@ const config: ControlPanelConfig = {
name: 'enableEmptyFilter',
config: {
type: 'CheckboxControl',
label: t('Required'),
label: t('Filter value is required'),
default: false,
renderTrigger: true,
description: t('User must select a value for this filter.'),
description: t(
'User must select a value before applying the filter',
),
},
},
],

View File

@ -30,10 +30,12 @@ const config: ControlPanelConfig = {
name: 'enableEmptyFilter',
config: {
type: 'CheckboxControl',
label: t('Required'),
label: t('Filter value is required'),
default: false,
renderTrigger: true,
description: t('User must select a value for this filter.'),
description: t(
'User must select a value before applying the filter',
),
},
},
],