fix(native-filters): default value checkbox in config modal (#15257)

* fix(native-filters): default value checkbox in config modal

* disable checkbox if required

* simplify styling logic

* make default value mandatory if checked

* address comments
This commit is contained in:
Ville Brofeldt 2021-06-21 15:58:30 +03:00 committed by GitHub
parent 2792ddc9b5
commit b28d7eace3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 86 additions and 34 deletions

View File

@ -19,11 +19,14 @@
import React, { ReactNode, useEffect, useState } from 'react'; import React, { ReactNode, useEffect, useState } from 'react';
import { styled } from '@superset-ui/core'; import { styled } from '@superset-ui/core';
import { Checkbox } from 'src/common/components'; import { Checkbox } from 'src/common/components';
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
interface CollapsibleControlProps { interface CollapsibleControlProps {
initialValue?: boolean; initialValue?: boolean;
disabled?: boolean;
checked?: boolean; checked?: boolean;
title: string; title: string;
tooltip?: string;
children: ReactNode; children: ReactNode;
onChange?: (checked: boolean) => void; onChange?: (checked: boolean) => void;
} }
@ -48,7 +51,9 @@ const StyledContainer = styled.div<{ checked: boolean }>`
const CollapsibleControl = (props: CollapsibleControlProps) => { const CollapsibleControl = (props: CollapsibleControlProps) => {
const { const {
checked, checked,
disabled,
title, title,
tooltip,
children, children,
onChange = () => {}, onChange = () => {},
initialValue = false, initialValue = false,
@ -68,6 +73,7 @@ const CollapsibleControl = (props: CollapsibleControlProps) => {
<Checkbox <Checkbox
className="checkbox" className="checkbox"
checked={isChecked} checked={isChecked}
disabled={disabled}
onChange={e => { onChange={e => {
const value = e.target.checked; const value = e.target.checked;
// external `checked` value has more priority then local state // external `checked` value has more priority then local state
@ -78,7 +84,12 @@ const CollapsibleControl = (props: CollapsibleControlProps) => {
onChange(value); onChange(value);
}} }}
> >
{title} <>
{title}&nbsp;
{tooltip && (
<InfoTooltipWithTrigger placement="top" tooltip={tooltip} />
)}
</>
</Checkbox> </Checkbox>
{isChecked && children} {isChecked && children}
</StyledContainer> </StyledContainer>

View File

@ -129,6 +129,20 @@ export const StyledRowFormItem = styled(FormItem)`
} }
`; `;
export const StyledRowSubFormItem = styled(FormItem)`
& .ant-form-item-label {
padding-bottom: 0;
}
.ant-form-item-control-input-content > div > div {
height: auto;
}
& .ant-form-item-control-input {
height: auto;
}
`;
export const StyledLabel = styled.span` export const StyledLabel = styled.span`
color: ${({ theme }) => theme.colors.grayscale.base}; color: ${({ theme }) => theme.colors.grayscale.base};
font-size: ${({ theme }) => theme.typography.sizes.s}px; font-size: ${({ theme }) => theme.typography.sizes.s}px;
@ -454,10 +468,12 @@ const FiltersConfigForm = (
...formFilter, ...formFilter,
}); });
const [hasDefaultValue, setHasDefaultValue] = useDefaultValue( const [
formFilter, hasDefaultValue,
filterToEdit, isRequired,
); defaultValueTooltip,
setHasDefaultValue,
] = useDefaultValue(formFilter, filterToEdit);
useEffect(() => { useEffect(() => {
if (hasDataset && hasFilledDataset && hasDefaultValue && isDataDirty) { if (hasDataset && hasFilledDataset && hasDefaultValue && isDataDirty) {
@ -540,6 +556,8 @@ const FiltersConfigForm = (
const hasAdhoc = formFilter?.adhoc_filters?.length > 0; const hasAdhoc = formFilter?.adhoc_filters?.length > 0;
const defaultToFirstItem = formFilter?.controlValues?.defaultToFirstItem;
const preFilterValidator = () => { const preFilterValidator = () => {
if (hasTimeRange || hasAdhoc) { if (hasTimeRange || hasAdhoc) {
return Promise.resolve(); return Promise.resolve();
@ -713,26 +731,22 @@ const FiltersConfigForm = (
<CollapsibleControl <CollapsibleControl
title={t('Filter has default value')} title={t('Filter has default value')}
initialValue={hasDefaultValue} initialValue={hasDefaultValue}
disabled={isRequired || defaultToFirstItem}
tooltip={defaultValueTooltip}
checked={hasDefaultValue} checked={hasDefaultValue}
onChange={value => setHasDefaultValue(value)} onChange={value => setHasDefaultValue(value)}
> >
<StyledRowFormItem <StyledRowSubFormItem
name={['filters', filterId, 'defaultDataMask']} name={['filters', filterId, 'defaultDataMask']}
initialValue={filterToEdit?.defaultDataMask} initialValue={filterToEdit?.defaultDataMask}
data-test="default-input" data-test="default-input"
label={<StyledLabel>{t('Default Value')}</StyledLabel>} label={<StyledLabel>{t('Default Value')}</StyledLabel>}
required={formFilter?.controlValues?.enableEmptyFilter} required={hasDefaultValue}
rules={[ rules={[
{ {
validator: (rule, value) => { validator: (rule, value) => {
const hasValue = !!value?.filterState?.value; const hasValue = !!value?.filterState?.value;
if ( if (hasValue) {
hasValue ||
// TODO: do more generic
formFilter.controlValues?.defaultToFirstItem ||
// Not marked as required
!formFilter.controlValues?.enableEmptyFilter
) {
return Promise.resolve(); return Promise.resolve();
} }
return Promise.reject( return Promise.reject(
@ -775,7 +789,7 @@ const FiltersConfigForm = (
) : ( ) : (
t('Fill all required fields to enable "Default Value"') t('Fill all required fields to enable "Default Value"')
)} )}
</StyledRowFormItem> </StyledRowSubFormItem>
</CollapsibleControl> </CollapsibleControl>
{Object.keys(controlItems) {Object.keys(controlItems)
.filter(key => BASIC_CONTROL_ITEMS.includes(key)) .filter(key => BASIC_CONTROL_ITEMS.includes(key))
@ -814,7 +828,7 @@ const FiltersConfigForm = (
} }
}} }}
> >
<StyledRowFormItem <StyledRowSubFormItem
name={['filters', filterId, 'parentFilter']} name={['filters', filterId, 'parentFilter']}
label={<StyledLabel>{t('Parent filter')}</StyledLabel>} label={<StyledLabel>{t('Parent filter')}</StyledLabel>}
initialValue={parentFilter} initialValue={parentFilter}
@ -832,7 +846,7 @@ const FiltersConfigForm = (
options={parentFilterOptions} options={parentFilterOptions}
isClearable isClearable
/> />
</StyledRowFormItem> </StyledRowSubFormItem>
</CollapsibleControl> </CollapsibleControl>
)} )}
{Object.keys(controlItems) {Object.keys(controlItems)
@ -848,7 +862,7 @@ const FiltersConfigForm = (
} }
}} }}
> >
<StyledRowFormItem <StyledRowSubFormItem
name={['filters', filterId, 'adhoc_filters']} name={['filters', filterId, 'adhoc_filters']}
initialValue={filterToEdit?.adhoc_filters} initialValue={filterToEdit?.adhoc_filters}
required required
@ -880,7 +894,7 @@ const FiltersConfigForm = (
</span> </span>
} }
/> />
</StyledRowFormItem> </StyledRowSubFormItem>
{showTimeRangePicker && ( {showTimeRangePicker && (
<StyledRowFormItem <StyledRowFormItem
name={['filters', filterId, 'time_range']} name={['filters', filterId, 'time_range']}
@ -976,7 +990,7 @@ const FiltersConfigForm = (
/> />
</StyledFormItem> </StyledFormItem>
{hasMetrics && ( {hasMetrics && (
<StyledRowFormItem <StyledRowSubFormItem
name={['filters', filterId, 'sortMetric']} name={['filters', filterId, 'sortMetric']}
initialValue={filterToEdit?.sortMetric} initialValue={filterToEdit?.sortMetric}
label={ label={
@ -1009,7 +1023,7 @@ const FiltersConfigForm = (
} }
}} }}
/> />
</StyledRowFormItem> </StyledRowSubFormItem>
)} )}
</CollapsibleControl> </CollapsibleControl>
)} )}

View File

@ -116,7 +116,7 @@ export default function getControlItemsMap({
forceUpdate(); forceUpdate();
}} }}
> >
{controlItem.config.label}{' '} {controlItem.config.label}&nbsp;
{controlItem.config.description && ( {controlItem.config.description && (
<InfoTooltipWithTrigger <InfoTooltipWithTrigger
placement="top" placement="top"

View File

@ -18,6 +18,7 @@
*/ */
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { FormInstance } from 'antd/lib/form'; import { FormInstance } from 'antd/lib/form';
import { t } from '@superset-ui/core';
import { NativeFiltersForm, NativeFiltersFormItem } from '../types'; import { NativeFiltersForm, NativeFiltersFormItem } from '../types';
import { setNativeFilterFieldValues, useForceUpdate } from './utils'; import { setNativeFilterFieldValues, useForceUpdate } from './utils';
import { Filter } from '../../types'; import { Filter } from '../../types';
@ -56,20 +57,47 @@ export const useDefaultValue = (
!!filterToEdit?.defaultDataMask?.filterState?.value || !!filterToEdit?.defaultDataMask?.filterState?.value ||
formFilter?.controlValues?.enableEmptyFilter, formFilter?.controlValues?.enableEmptyFilter,
); );
const [isRequired, setisRequired] = useState(
formFilter?.controlValues?.enableEmptyFilter,
);
const [defaultValueTooltip, setDefaultValueTooltip] = useState('');
const defaultToFirstItem = formFilter?.controlValues?.defaultToFirstItem;
const setHasDefaultValue = useCallback( const setHasDefaultValue = useCallback(
(value?) => { (value?) => {
setHasPartialDefaultValue( const required =
value || formFilter?.controlValues?.enableEmptyFilter !!formFilter?.controlValues?.enableEmptyFilter && !defaultToFirstItem;
? true setisRequired(required);
: undefined, setHasPartialDefaultValue(required ? true : value);
);
}, },
[formFilter?.controlValues?.enableEmptyFilter], [formFilter?.controlValues?.enableEmptyFilter, defaultToFirstItem],
); );
useEffect(() => { useEffect(() => {
setHasDefaultValue(); setHasDefaultValue(
}, [setHasDefaultValue]); defaultToFirstItem
? false
: !!formFilter?.defaultDataMask?.filterState?.value,
);
}, [setHasDefaultValue, defaultToFirstItem]);
return [hasDefaultValue, setHasDefaultValue]; useEffect(() => {
let tooltip = '';
if (defaultToFirstItem) {
tooltip = t(
'Default value set automatically when "Default to first item" is checked',
);
} else if (isRequired) {
tooltip = t('Default value must be set when "Required" is checked');
} else if (hasDefaultValue) {
tooltip = t(
'Default value must be set when "Filter has default value" is checked',
);
}
setDefaultValueTooltip(tooltip);
}, [hasDefaultValue, isRequired, defaultToFirstItem]);
return [hasDefaultValue, isRequired, defaultValueTooltip, setHasDefaultValue];
}; };

View File

@ -284,7 +284,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
extra={<Error>{filterState.validateMessage}</Error>} extra={<Error>{filterState.validateMessage}</Error>}
> >
<StyledSelect <StyledSelect
allowClear={!enableEmptyFilter} allowClear
// @ts-ignore // @ts-ignore
value={filterState.value || []} value={filterState.value || []}
disabled={isDisabled} disabled={isDisabled}

View File

@ -77,8 +77,7 @@ const config: ControlPanelConfig = {
default: enableEmptyFilter, default: enableEmptyFilter,
renderTrigger: true, renderTrigger: true,
description: t( description: t(
'User must select a value for this filter when filter is in single select mode. ' + 'User must select a value before applying the filter',
'If selection is empty, an always false filter is emitted.',
), ),
}, },
}, },