diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/nativeFilters.noInitState.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/nativeFilters.noInitState.test.ts index b90c8f6d170..9778ae74459 100644 --- a/superset-frontend/cypress-base/cypress/e2e/dashboard/nativeFilters.noInitState.test.ts +++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/nativeFilters.noInitState.test.ts @@ -1,4 +1,5 @@ /** + * * 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 @@ -192,23 +193,34 @@ describe('Native filters', () => { testItems.filterNumericalColumn, ); saveNativeFilterSettings([]); - // assertions - cy.get(nativeFilters.slider.slider).should('be.visible').click('center'); + + // Assertions + cy.get('[data-test="range-filter-from-input"]') + .should('be.visible') + .click(); + + cy.get('[data-test="range-filter-from-input"]').type('{selectall}5'); + + cy.get('[data-test="range-filter-to-input"]') + .should('be.visible') + .click(); + + cy.get('[data-test="range-filter-to-input"]').type('{selectall}50'); cy.get(nativeFilters.applyFilter).click(); - // assert that the url contains 'native_filters' in the url + + // Assert that the URL contains 'native_filters' cy.url().then(u => { const ur = new URL(u); expect(ur.search).to.include('native_filters'); - // assert that the start handle has a value - cy.get(nativeFilters.slider.startHandle) - .invoke('attr', 'aria-valuenow') - .should('exist'); - // assert that the end handle has a value - cy.get(nativeFilters.slider.endHandle) - .invoke('attr', 'aria-valuenow') - .should('exist'); - // assert slider text matches what we should have - cy.get(nativeFilters.slider.sliderText).should('have.text', '49'); + + cy.get('[data-test="range-filter-from-input"]') + .invoke('val') + .should('equal', '5'); + + // Assert that the "To" input has the correct value + cy.get('[data-test="range-filter-to-input"]') + .invoke('val') + .should('equal', '50'); }); }); diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts index c39a9b63346..506858c74fd 100644 --- a/superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts +++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts @@ -489,7 +489,7 @@ export function inputNativeFilterDefaultValue( ) { if (!multiple) { cy.contains('Filter has default value').click(); - cy.contains('Default value is required').should('be.visible'); + cy.contains('Please choose a valid value').should('be.visible'); cy.get(nativeFilters.modal.container).within(() => { cy.get( nativeFilters.filterConfigurationSections.filterPlaceholder, diff --git a/superset-frontend/src/components/Metadata/index.tsx b/superset-frontend/src/components/Metadata/index.tsx new file mode 100644 index 00000000000..05c2f572698 --- /dev/null +++ b/superset-frontend/src/components/Metadata/index.tsx @@ -0,0 +1,47 @@ +/** + * 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 { styled } from '@superset-ui/core'; + +const MetadataWrapper = styled.div` + display: flex; + width: 100%; + position: absolute; + left: 0; + top: 100%; + margin-top: ${({ theme }) => theme.gridUnit}px; +`; + +const MetadataText = styled.span` + font-size: ${({ theme }) => theme.typography.sizes.xs}px; + color: ${({ theme }) => theme.colors.grayscale.light1}; + font-weight: ${({ theme }) => theme.typography.weights.medium}; +`; + +export type MetadataProps = { + value: string; +}; + +const Metadata: React.FC = ({ value }) => ( + + {value} + +); + +export default Metadata; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx index 1757cb6d5fc..31722a63ead 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx @@ -288,8 +288,8 @@ const FilterValue: FC = ({ const filterState = useMemo( () => ({ - ...filter.dataMask?.filterState, validateStatus, + ...filter.dataMask?.filterState, }), [filter.dataMask?.filterState, validateStatus], ); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts index 6a6ca3a3e3a..fb5a4f6abbe 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts @@ -42,11 +42,19 @@ export const checkIsMissingRequiredValue = ( ); }; +export const checkIsValidateError = (dataMask: DataMaskStateWithId) => { + const values = Object.values(dataMask); + return values.every(value => value.filterState?.validateStatus !== 'error'); +}; + export const checkIsApplyDisabled = ( dataMaskSelected: DataMaskStateWithId, dataMaskApplied: DataMaskStateWithId, filters: Filter[], ) => { + if (!checkIsValidateError(dataMaskSelected)) { + return true; + } const dataSelectedValues = Object.values(dataMaskSelected); const dataAppliedValues = Object.values(dataMaskApplied); return ( diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx index 5b843a370b7..ff247daa297 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx @@ -1275,7 +1275,7 @@ const FiltersConfigForm = ( return [...prevErroredFilters, filterId]; }); return Promise.reject( - new Error(t('Default value is required')), + new Error(t('Please choose a valid value')), ); }, }, diff --git a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx index f7a19de2fdd..855482027ef 100644 --- a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx +++ b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx @@ -17,7 +17,7 @@ * under the License. */ import { AppSection, GenericDataType } from '@superset-ui/core'; -import { render, screen } from 'spec/helpers/testing-library'; +import { fireEvent, render, screen } from 'spec/helpers/testing-library'; import RangeFilterPlugin from './RangeFilterPlugin'; import { SingleValueType } from './SingleValueType'; import transformProps from './transformProps'; @@ -83,14 +83,15 @@ const rangeProps = { describe('RangeFilterPlugin', () => { const setDataMask = jest.fn(); - const getWrapper = (props = {}) => + const getWrapper = (props: any = {}) => render( // @ts-ignore , @@ -100,11 +101,50 @@ describe('RangeFilterPlugin', () => { jest.clearAllMocks(); }); + it('should render two numerical inputs', () => { + getWrapper(); + + const inputs = screen.getAllByRole('spinbutton'); + expect(inputs).toHaveLength(2); + + expect(inputs[0]).toHaveValue('10'); + expect(inputs[1]).toHaveValue('70'); + }); + + it('should set the data mask to error when the range is incorrect', () => { + getWrapper({ filterState: { value: [null, null] } }); + + const inputs = screen.getAllByRole('spinbutton'); + const fromInput = inputs[0]; + const toInput = inputs[1]; + + fireEvent.change(fromInput, { target: { value: 20 } }); + + fireEvent.change(toInput, { target: { value: 10 } }); + + fireEvent.blur(toInput); + + expect(setDataMask).toHaveBeenCalledWith({ + extraFormData: {}, + filterState: { + label: '', + validateMessage: 'Please provide a valid range', + validateStatus: 'error', + value: null, + }, + }); + }); + it('should call setDataMask with correct filter', () => { getWrapper(); expect(setDataMask).toHaveBeenCalledWith({ extraFormData: { filters: [ + { + col: 'SP_POP_TOTL', + op: '>=', + val: 10, + }, { col: 'SP_POP_TOTL', op: '<=', @@ -113,16 +153,21 @@ describe('RangeFilterPlugin', () => { ], }, filterState: { - label: 'x ≤ 70', + label: '10 ≤ x ≤ 70', value: [10, 70], + validateMessage: '', + validateStatus: undefined, }, }); }); it('should call setDataMask with correct greater than filter', () => { getWrapper({ - enableSingleValue: SingleValueType.Minimum, - defaultValue: [20, 60], + filterState: { value: [20, null] }, + formData: { + enableSingleValue: SingleValueType.Minimum, + defaultValue: undefined, + }, }); expect(setDataMask).toHaveBeenCalledWith({ extraFormData: { @@ -135,17 +180,22 @@ describe('RangeFilterPlugin', () => { ], }, filterState: { + validateStatus: undefined, + validateMessage: '', label: 'x ≥ 20', - value: [20, 100], + value: [20, null], }, }); - expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '20'); + const input = screen.getByRole('spinbutton'); + expect(input).toHaveValue('20'); }); it('should call setDataMask with correct less than filter', () => { getWrapper({ - enableSingleValue: SingleValueType.Maximum, - defaultValue: [20, 60], + filterState: { value: [null, 60] }, + formData: { + enableSingleValue: SingleValueType.Maximum, + }, }); expect(setDataMask).toHaveBeenCalledWith({ extraFormData: { @@ -159,14 +209,22 @@ describe('RangeFilterPlugin', () => { }, filterState: { label: 'x ≤ 60', - value: [10, 60], + value: [null, 60], + validateMessage: '', + validateStatus: undefined, }, }); - expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '60'); + const input = screen.getByRole('spinbutton'); + expect(input).toHaveValue('60'); }); it('should call setDataMask with correct exact filter', () => { - getWrapper({ enableSingleValue: SingleValueType.Exact }); + getWrapper({ + formData: { + enableSingleValue: SingleValueType.Exact, + }, + filterState: { value: [10, 10] }, + }); expect(setDataMask).toHaveBeenCalledWith({ extraFormData: { filters: [ @@ -180,6 +238,8 @@ describe('RangeFilterPlugin', () => { filterState: { label: 'x = 10', value: [10, 10], + validateStatus: undefined, + validateMessage: '', }, }); }); diff --git a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx index 09bd2a81491..c717d8c267e 100644 --- a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx @@ -20,120 +20,53 @@ import { ensureIsArray, getColumnLabel, getNumberFormatter, + isEqualArray, NumberFormats, styled, t, } from '@superset-ui/core'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { rgba } from 'emotion-rgba'; -import { AntdSlider } from 'src/components'; +import { InputNumber } from 'src/components/Input'; import { FilterBarOrientation } from 'src/dashboard/types'; +import Metadata from 'src/components/Metadata'; +import { isNumber } from 'lodash'; import { PluginFilterRangeProps } from './types'; import { StatusMessage, StyledFormItem, FilterPluginStyle } from '../common'; import { getRangeExtraFormData } from '../../utils'; import { SingleValueType } from './SingleValueType'; -const LIGHT_BLUE = '#99e7f0'; -const DARK_BLUE = '#6dd3e3'; -const LIGHT_GRAY = '#f5f5f5'; -const DARK_GRAY = '#e1e1e1'; +type InputValue = number | null; +type RangeValue = [InputValue, InputValue]; -const StyledMinSlider = styled(AntdSlider)<{ - validateStatus?: 'error' | 'warning' | 'info'; -}>` - ${({ theme, validateStatus }) => ` - .ant-slider-rail { - background-color: ${ - validateStatus ? theme.colors[validateStatus]?.light1 : LIGHT_BLUE - }; - } - - .ant-slider-track { - background-color: ${LIGHT_GRAY}; - } - - &:hover { - .ant-slider-rail { - background-color: ${ - validateStatus ? theme.colors[validateStatus]?.base : DARK_BLUE - }; - } - - .ant-slider-track { - background-color: ${DARK_GRAY}; - } - } - `} +const StyledDivider = styled.span` + margin: 0 ${({ theme }) => theme.gridUnit * 3}px; + color: ${({ theme }) => theme.colors.grayscale.light1}; + font-weight: ${({ theme }) => theme.typography.weights.bold}; + font-size: ${({ theme }) => theme.typography.sizes.m}px; + align-content: center; `; -const Wrapper = styled.div<{ - validateStatus?: 'error' | 'warning' | 'info'; - orientation?: FilterBarOrientation; - isOverflowing?: boolean; -}>` - ${({ theme, validateStatus, orientation, isOverflowing }) => ` - border: 1px solid transparent; - &:focus { - border: 1px solid - ${theme.colors[validateStatus || 'primary']?.base}; - outline: 0; - box-shadow: 0 0 0 3px - ${rgba(theme.colors[validateStatus || 'primary']?.base, 0.2)}; - } - & .ant-slider { - margin-top: ${ - orientation === FilterBarOrientation.Horizontal ? 0 : theme.gridUnit - }px; - margin-bottom: ${ - orientation === FilterBarOrientation.Horizontal ? 0 : theme.gridUnit * 5 - }px; +const Wrapper = styled.div` + display: flex; + justify-content: space-between; - ${ - orientation === FilterBarOrientation.Horizontal && - !isOverflowing && - `line-height: 1.2;` - } - - & .ant-slider-track { - background-color: ${ - validateStatus && theme.colors[validateStatus]?.light1 - }; - } - & .ant-slider-handle { - border: ${ - validateStatus && `2px solid ${theme.colors[validateStatus]?.light1}` - }; - &:focus { - box-shadow: 0 0 0 3px - ${rgba(theme.colors[validateStatus || 'primary']?.base, 0.2)}; - } - } - & .ant-slider-mark { - font-size: ${theme.typography.sizes.s}px; - } - - &:hover { - & .ant-slider-track { - background-color: ${ - validateStatus && theme.colors[validateStatus]?.base - }; - } - & .ant-slider-handle { - border: ${ - validateStatus && `2px solid ${theme.colors[validateStatus]?.base}` - }; - } - } - } - `} + .antd5-input-number { + width: 100%; + position: relative; + } `; const numberFormatter = getNumberFormatter(NumberFormats.SMART_NUMBER); -const tipFormatter = (value: number) => numberFormatter(value); - -const getLabel = (lower: number | null, upper: number | null): string => { - if (lower !== null && upper !== null && lower === upper) { +const getLabel = ( + lower: number | null, + upper: number | null, + enableSingleExactValue = false, +): string => { + if ( + (enableSingleExactValue && lower !== null) || + (lower !== null && lower === upper) + ) { return `x = ${numberFormatter(lower)}`; } if (lower !== null && upper !== null) { @@ -148,18 +81,58 @@ const getLabel = (lower: number | null, upper: number | null): string => { return ''; }; -const getMarks = ( - lower: number | null, - upper: number | null, -): { [key: number]: string } => { - const newMarks: { [key: number]: string } = {}; - if (lower !== null) { - newMarks[lower] = numberFormatter(lower); +const validateRange = ( + values: RangeValue, + min: number, + max: number, + enableEmptyFilter: boolean, + enableSingleValue?: SingleValueType, +): { isValid: boolean; errorMessage: string | null } => { + const [inputMin, inputMax] = values; + const requiredError = t('Filter value is required'); + const rangeError = t('Please provide a value within range'); + if (enableSingleValue !== undefined) { + const isSingleMin = + enableSingleValue === SingleValueType.Minimum || + enableSingleValue === SingleValueType.Exact; + const value = isSingleMin ? inputMin : inputMax; + + if (!value && enableEmptyFilter) { + return { isValid: false, errorMessage: requiredError }; + } + + if (isNumber(value) && (value < min || value > max)) { + return { isValid: false, errorMessage: rangeError }; + } + + return { isValid: true, errorMessage: null }; } - if (upper !== null) { - newMarks[upper] = numberFormatter(upper); + + // Range validation + if (enableEmptyFilter && (inputMin === null || inputMax === null)) { + return { isValid: false, errorMessage: t('Please provide a valid range') }; } - return newMarks; + + if (!enableEmptyFilter && (inputMin !== null) !== (inputMax !== null)) { + return { isValid: false, errorMessage: t('Please provide a valid range') }; + } + + if (inputMin !== null && inputMax !== null) { + if (inputMin > inputMax) { + return { + isValid: false, + errorMessage: t('Minimum value cannot be higher than maximum value'), + }; + } + if (inputMin < min || inputMax > max) { + return { + isValid: false, + errorMessage: t('Your range is not within the dataset range'), + }; + } + } + + return { isValid: true, errorMessage: null }; }; export default function RangeFilterPlugin(props: PluginFilterRangeProps) { @@ -176,139 +149,191 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) { setFilterActive, filterState, inputRef, - filterBarOrientation, - isOverflowingFilterBar, + filterBarOrientation = FilterBarOrientation.Vertical, } = props; const [row] = data; // @ts-ignore const { min, max }: { min: number; max: number } = row; - const { groupby, defaultValue, enableSingleValue } = formData; - - const enableSingleMinValue = enableSingleValue === SingleValueType.Minimum; - const enableSingleMaxValue = enableSingleValue === SingleValueType.Maximum; - const enableSingleExactValue = enableSingleValue === SingleValueType.Exact; - const rangeValue = enableSingleValue === undefined; - - const [col = ''] = ensureIsArray(groupby).map(getColumnLabel); - const [value, setValue] = useState<[number, number]>( - defaultValue ?? [min, enableSingleExactValue ? min : max], - ); - const [marks, setMarks] = useState<{ [key: number]: string }>({}); + const { groupby, enableSingleValue, enableEmptyFilter, defaultValue } = + formData; const minIndex = 0; const maxIndex = 1; - const minMax = value ?? [min, max]; + const enableSingleExactValue = enableSingleValue === SingleValueType.Exact; + const rangeInput = enableSingleValue === undefined; - const getBounds = useCallback( - ( - value: [number, number], - ): { lower: number | null; upper: number | null } => { - const [lowerRaw, upperRaw] = value; + const [col = ''] = ensureIsArray(groupby).map(getColumnLabel); - if (enableSingleExactValue) { - return { lower: lowerRaw, upper: upperRaw }; - } - - return { - lower: lowerRaw > min ? lowerRaw : null, - upper: upperRaw < max ? upperRaw : null, - }; - }, - [max, min, enableSingleExactValue], + const [inputValue, setInputValue] = useState( + filterState.value || defaultValue || [null, null], ); + const [error, setError] = useState(null); - const handleAfterChange = useCallback( - (value: [number, number]): void => { - setValue(value); - const { lower, upper } = getBounds(value); - setMarks(getMarks(lower, upper)); - + const updateDataMaskError = useCallback( + (errorMessage: string | null) => { setDataMask({ - extraFormData: getRangeExtraFormData(col, lower, upper), + extraFormData: {}, filterState: { - value: lower !== null || upper !== null ? value : null, - label: getLabel(lower, upper), + value: null, + label: '', + validateStatus: 'error', + validateMessage: errorMessage || '', }, }); }, - [col, getBounds, setDataMask], + [setDataMask], ); - const handleChange = useCallback((value: [number, number]) => { - setValue(value); - }, []); + const updateDataMaskValue = useCallback( + (value: RangeValue) => { + const [inputMin, inputMax] = value; + setDataMask({ + extraFormData: getRangeExtraFormData(col, inputMin, inputMax), + filterState: { + value: enableSingleExactValue + ? [inputMin, inputMin] + : [inputMin, inputMax], + label: getLabel(inputMin, inputMax, enableSingleExactValue), + validateStatus: undefined, + validateMessage: '', + }, + }); + }, + [setDataMask], + ); useEffect(() => { - // when switch filter type and queriesData still not updated we need ignore this case (in FilterBar) if (row?.min === undefined && row?.max === undefined) { return; } - let filterStateValue = filterState.value ?? [min, max]; - if (enableSingleMaxValue) { - const filterStateMax = - filterStateValue[maxIndex] <= minMax[maxIndex] - ? filterStateValue[maxIndex] - : minMax[maxIndex]; + if ( + filterState.validateStatus === 'error' && + error !== filterState.validateMessage + ) { + setError(filterState.validateMessage); - filterStateValue = [min, filterStateMax]; - } else if (enableSingleMinValue) { - const filterStateMin = - filterStateValue[minIndex] >= minMax[minIndex] - ? filterStateValue[minIndex] - : minMax[minIndex]; + const [inputMin, inputMax] = inputValue; - filterStateValue = [filterStateMin, max]; - } else if (enableSingleExactValue) { - filterStateValue = [minMax[minIndex], minMax[minIndex]]; + const { isValid, errorMessage } = validateRange( + inputValue, + min, + max, + enableEmptyFilter, + enableSingleValue, + ); + + const isDefaultError = + inputMin === null && + inputMax === null && + filterState.validateStatus === 'error'; + + if (!isValid || isDefaultError) { + setError(errorMessage); + updateDataMaskError(errorMessage); + return; + } + + setError(null); + updateDataMaskValue(inputValue); + return; + } + if (filterState.validateStatus === 'error') { + setError(filterState.validateMessage); + return; } - handleAfterChange(filterStateValue); - }, [ - enableSingleMaxValue, - enableSingleMinValue, - enableSingleExactValue, - JSON.stringify(filterState.value), - JSON.stringify(data), - ]); + // Clear all case + if (filterState.value === undefined && !filterState.validateStatus) { + setInputValue([null, null]); + updateDataMaskValue([null, null]); + return; + } + + if (isEqualArray(defaultValue, inputValue)) { + updateDataMaskValue(defaultValue); + return; + } + + // Filter state is pre-set case + if (filterState.value && !filterState.validateStatus) { + setInputValue(filterState.value); + updateDataMaskValue(filterState.value); + } + }, [JSON.stringify(filterState.value)]); + + const metadataText = useMemo(() => { + switch (enableSingleValue) { + case SingleValueType.Minimum: + return t('Filters for values greater than or equal.'); + case SingleValueType.Maximum: + return t('Filters for values less than or equal.'); + case SingleValueType.Exact: + return t('Filters for values equal to this exact value.'); + default: + return ''; + } + }, [enableSingleValue]); + + const handleChange = useCallback( + (newValue: number | null, index: 0 | 1) => { + if (row?.min === undefined && row?.max === undefined) { + return; + } + const newInputValue: [number | null, number | null] = + index === minIndex + ? [newValue, inputValue[maxIndex]] + : [inputValue[minIndex], newValue]; + + setInputValue(newInputValue); + + const { isValid, errorMessage } = validateRange( + newInputValue, + min, + max, + enableEmptyFilter, + enableSingleValue, + ); + + if (!isValid) { + setError(errorMessage); + updateDataMaskError(errorMessage); + return; + } + + setError(null); + updateDataMaskValue(newInputValue); + }, + [col, min, max, enableEmptyFilter, enableSingleValue, setDataMask], + ); const formItemExtra = useMemo(() => { - if (filterState.validateMessage) { - return ( - - {filterState.validateMessage} - - ); + if (error) { + return {error}; } return undefined; - }, [filterState.validateMessage, filterState.validateStatus]); + }, [error]); useEffect(() => { - if (enableSingleMaxValue) { - handleAfterChange([min, minMax[maxIndex]]); + switch (enableSingleValue) { + case SingleValueType.Minimum: + case SingleValueType.Exact: + handleChange(null, maxIndex); + break; + case SingleValueType.Maximum: + handleChange(null, minIndex); + break; + default: + break; } - }, [enableSingleMaxValue]); - useEffect(() => { - if (enableSingleMinValue) { - handleAfterChange([minMax[minIndex], max]); - } - }, [enableSingleMinValue]); - - useEffect(() => { - if (enableSingleExactValue) { - handleAfterChange([minMax[minIndex], minMax[minIndex]]); - } - }, [enableSingleExactValue]); - - const MIN_NUM_STEPS = 20; - const stepHeuristic = (min: number, max: number) => { - const maxStepSize = (max - min) / MIN_NUM_STEPS; - // normalizedStepSize: .06 -> .01, .003 -> .001 - const normalizedStepSize = `1E${Math.floor(Math.log10(maxStepSize))}`; - return Math.min(1, parseFloat(normalizedStepSize)); - }; - - const step = max - min <= 1 ? stepHeuristic(min, max) : 1; + setDataMask({ + extraFormData: {}, + filterState: { + value: null, + label: '', + }, + }); + }, [enableSingleValue]); return ( @@ -322,9 +347,6 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) { setFilterActive(true)} onMouseUp={() => setFilterActive(false)} > - {enableSingleMaxValue && ( - handleAfterChange([min, value])} - onChange={value => handleChange([min, value])} + {(enableSingleValue === SingleValueType.Minimum || + enableSingleValue === SingleValueType.Exact || + enableSingleValue === undefined) && ( + handleChange(val, minIndex)} + placeholder={`${min}`} + status={filterState.validateStatus} + data-test="range-filter-from-input" /> )} - {enableSingleMinValue && ( - handleAfterChange([value, max])} - onChange={value => handleChange([value, max])} - /> + {enableSingleValue === undefined && ( + - )} - {enableSingleExactValue && ( - handleAfterChange([value, value])} - onChange={value => handleChange([value, value])} - /> - )} - {rangeValue && ( - handleChange(val, maxIndex)} + placeholder={`${max}`} + data-test="range-filter-to-input" + status={filterState.validateStatus} /> )} + {(rangeInput || + filterBarOrientation === FilterBarOrientation.Vertical) && + !filterState.validateStatus && } )}