chore: Remove unecessary code from async and sync select components (#20690)

* Created AsyncSelect Component
Changed files to reference AsyncSelect if needed

* modified import of AsyncSelect, removed async tests and prefixes from select tests

* fixed various import and lint warnings

* fixing lint errors

* fixed frontend test errors

* fixed alertreportmodel tests

* removed accidental import

* fixed lint errors

* updated async select

* removed code from select component

* fixed select test

* fixed async label value and select initial values

* cleaned up async test

* fixed lint errors

* minor fixes to sync select component

* removed unecessary variables and fixed linting

* fixed npm test errors

* fixed linting issues

* fixed showSearch and storybook

* fixed linting
This commit is contained in:
cccs-RyanK 2022-07-28 09:09:37 -04:00 committed by GitHub
parent 718bc3062e
commit fe91974163
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 214 additions and 397 deletions

View File

@ -302,7 +302,6 @@ export default function DatabaseSelector({
disabled={!currentDb || readOnly} disabled={!currentDb || readOnly}
header={<FormLabel>{t('Schema')}</FormLabel>} header={<FormLabel>{t('Schema')}</FormLabel>}
labelInValue labelInValue
lazyLoading={false}
loading={loadingSchemas} loading={loadingSchemas}
name="select-schema" name="select-schema"
placeholder={t('Select schema or type schema name')} placeholder={t('Select schema or type schema name')}

View File

@ -26,6 +26,7 @@ import { t } from '@superset-ui/core';
import { Select } from 'src/components'; import { Select } from 'src/components';
import { Filter, SelectOption } from 'src/components/ListView/types'; import { Filter, SelectOption } from 'src/components/ListView/types';
import { FormLabel } from 'src/components/Form'; import { FormLabel } from 'src/components/Form';
import AsyncSelect from 'src/components/Select/AsyncSelect';
import { FilterContainer, BaseFilter, FilterHandler } from './Base'; import { FilterContainer, BaseFilter, FilterHandler } from './Base';
interface SelectFilterProps extends BaseFilter { interface SelectFilterProps extends BaseFilter {
@ -86,19 +87,34 @@ function SelectFilter(
return ( return (
<FilterContainer> <FilterContainer>
<Select {fetchSelects ? (
allowClear <AsyncSelect
ariaLabel={typeof Header === 'string' ? Header : name || t('Filter')} allowClear
labelInValue ariaLabel={typeof Header === 'string' ? Header : name || t('Filter')}
data-test="filters-select" data-test="filters-select"
header={<FormLabel>{Header}</FormLabel>} header={<FormLabel>{Header}</FormLabel>}
onChange={onChange} onChange={onChange}
onClear={onClear} onClear={onClear}
options={fetchSelects ? fetchAndFormatSelects : selects} options={fetchAndFormatSelects}
placeholder={t('Select or type a value')} placeholder={t('Select or type a value')}
showSearch showSearch
value={selectedOption} value={selectedOption}
/> />
) : (
<Select
allowClear
ariaLabel={typeof Header === 'string' ? Header : name || t('Filter')}
data-test="filters-select"
header={<FormLabel>{Header}</FormLabel>}
labelInValue
onChange={onChange}
onClear={onClear}
options={selects}
placeholder={t('Select or type a value')}
showSearch
value={selectedOption}
/>
)}
</FilterContainer> </FilterContainer>
); );
} }

View File

@ -60,10 +60,16 @@ const loadOptions = async (search: string, page: number, pageSize: number) => {
const start = page * pageSize; const start = page * pageSize;
const deleteCount = const deleteCount =
start + pageSize < totalCount ? pageSize : totalCount - start; start + pageSize < totalCount ? pageSize : totalCount - start;
const data = OPTIONS.filter(option => option.label.match(search)).splice( const searchValue = search.trim().toLowerCase();
start, const optionFilterProps = ['label', 'value', 'gender'];
deleteCount, const data = OPTIONS.filter(option =>
); optionFilterProps.some(prop => {
const optionProp = option?.[prop]
? String(option[prop]).trim().toLowerCase()
: '';
return optionProp.includes(searchValue);
}),
).splice(start, deleteCount);
return { return {
data, data,
totalCount: OPTIONS.length, totalCount: OPTIONS.length,
@ -74,7 +80,7 @@ const defaultProps = {
allowClear: true, allowClear: true,
ariaLabel: ARIA_LABEL, ariaLabel: ARIA_LABEL,
labelInValue: true, labelInValue: true,
options: OPTIONS, options: loadOptions,
pageSize: 10, pageSize: 10,
showSearch: true, showSearch: true,
}; };
@ -129,17 +135,31 @@ test('displays a header', async () => {
expect(screen.getByText(headerText)).toBeInTheDocument(); expect(screen.getByText(headerText)).toBeInTheDocument();
}); });
test('adds a new option if the value is not in the options', async () => { test('adds a new option if the value is not in the options, when options are empty', async () => {
const { rerender } = render( const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 }));
<AsyncSelect {...defaultProps} options={[]} value={OPTIONS[0]} />, render(
<AsyncSelect {...defaultProps} options={loadOptions} value={OPTIONS[0]} />,
); );
await open(); await open();
expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument(); expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument();
const options = await findAllSelectOptions();
expect(options).toHaveLength(1);
options.forEach((option, i) =>
expect(option).toHaveTextContent(OPTIONS[i].label),
);
});
rerender( test('adds a new option if the value is not in the options, when options have values', async () => {
<AsyncSelect {...defaultProps} options={[OPTIONS[1]]} value={OPTIONS[0]} />, const loadOptions = jest.fn(async () => ({
data: [OPTIONS[1]],
totalCount: 1,
}));
render(
<AsyncSelect {...defaultProps} options={loadOptions} value={OPTIONS[0]} />,
); );
await open(); await open();
expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument();
expect(await findSelectOption(OPTIONS[1].label)).toBeInTheDocument();
const options = await findAllSelectOptions(); const options = await findAllSelectOptions();
expect(options).toHaveLength(2); expect(options).toHaveLength(2);
options.forEach((option, i) => options.forEach((option, i) =>
@ -147,6 +167,20 @@ test('adds a new option if the value is not in the options', async () => {
); );
}); });
test('does not add a new option if the value is already in the options', async () => {
const loadOptions = jest.fn(async () => ({
data: [OPTIONS[0]],
totalCount: 1,
}));
render(
<AsyncSelect {...defaultProps} options={loadOptions} value={OPTIONS[0]} />,
);
await open();
expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument();
const options = await findAllSelectOptions();
expect(options).toHaveLength(1);
});
test('inverts the selection', async () => { test('inverts the selection', async () => {
render(<AsyncSelect {...defaultProps} invertSelection />); render(<AsyncSelect {...defaultProps} invertSelection />);
await open(); await open();
@ -155,8 +189,11 @@ test('inverts the selection', async () => {
}); });
test('sort the options by label if no sort comparator is provided', async () => { test('sort the options by label if no sort comparator is provided', async () => {
const unsortedOptions = [...OPTIONS].sort(() => Math.random()); const loadUnsortedOptions = jest.fn(async () => ({
render(<AsyncSelect {...defaultProps} options={unsortedOptions} />); data: [...OPTIONS].sort(() => Math.random()),
totalCount: 2,
}));
render(<AsyncSelect {...defaultProps} options={loadUnsortedOptions} />);
await open(); await open();
const options = await findAllSelectOptions(); const options = await findAllSelectOptions();
options.forEach((option, key) => options.forEach((option, key) =>
@ -250,20 +287,23 @@ test('searches for label or value', async () => {
render(<AsyncSelect {...defaultProps} />); render(<AsyncSelect {...defaultProps} />);
const search = option.value; const search = option.value;
await type(search.toString()); await type(search.toString());
expect(await findSelectOption(option.label)).toBeInTheDocument();
const options = await findAllSelectOptions(); const options = await findAllSelectOptions();
expect(options.length).toBe(1); expect(options.length).toBe(1);
expect(options[0]).toHaveTextContent(option.label); expect(options[0]).toHaveTextContent(option.label);
}); });
test('search order exact and startWith match first', async () => { test('search order exact and startWith match first', async () => {
render(<AsyncSelect {...defaultProps} />); render(<AsyncSelect {...defaultProps} options={loadOptions} />);
await open();
await type('Her'); await type('Her');
expect(await findSelectOption('Guilherme')).toBeInTheDocument();
const options = await findAllSelectOptions(); const options = await findAllSelectOptions();
expect(options.length).toBe(4); expect(options.length).toBe(4);
expect(options[0]?.textContent).toEqual('Her'); expect(options[0]).toHaveTextContent('Her');
expect(options[1]?.textContent).toEqual('Herme'); expect(options[1]).toHaveTextContent('Herme');
expect(options[2]?.textContent).toEqual('Cher'); expect(options[2]).toHaveTextContent('Cher');
expect(options[3]?.textContent).toEqual('Guilherme'); expect(options[3]).toHaveTextContent('Guilherme');
}); });
test('ignores case when searching', async () => { test('ignores case when searching', async () => {
@ -273,17 +313,16 @@ test('ignores case when searching', async () => {
}); });
test('same case should be ranked to the top', async () => { test('same case should be ranked to the top', async () => {
render( const loadOptions = jest.fn(async () => ({
<AsyncSelect data: [
{...defaultProps} { value: 'Cac' },
options={[ { value: 'abac' },
{ value: 'Cac' }, { value: 'acbc' },
{ value: 'abac' }, { value: 'CAc' },
{ value: 'acbc' }, ],
{ value: 'CAc' }, totalCount: 4,
]} }));
/>, render(<AsyncSelect {...defaultProps} options={loadOptions} />);
);
await type('Ac'); await type('Ac');
const options = await findAllSelectOptions(); const options = await findAllSelectOptions();
expect(options.length).toBe(4); expect(options.length).toBe(4);
@ -294,7 +333,7 @@ test('same case should be ranked to the top', async () => {
}); });
test('ignores special keys when searching', async () => { test('ignores special keys when searching', async () => {
render(<AsyncSelect {...defaultProps} />); render(<AsyncSelect {...defaultProps} options={loadOptions} />);
await type('{shift}'); await type('{shift}');
expect(screen.queryByText(LOADING)).not.toBeInTheDocument(); expect(screen.queryByText(LOADING)).not.toBeInTheDocument();
}); });
@ -303,11 +342,16 @@ test('searches for custom fields', async () => {
render( render(
<AsyncSelect {...defaultProps} optionFilterProps={['label', 'gender']} />, <AsyncSelect {...defaultProps} optionFilterProps={['label', 'gender']} />,
); );
await open();
await type('Liam'); await type('Liam');
// Liam is on the second page. need to wait to fetch options
expect(await findSelectOption('Liam')).toBeInTheDocument();
let options = await findAllSelectOptions(); let options = await findAllSelectOptions();
expect(options.length).toBe(1); expect(options.length).toBe(1);
expect(options[0]).toHaveTextContent('Liam'); expect(options[0]).toHaveTextContent('Liam');
await type('Female'); await type('Female');
// Olivia is on the second page. need to wait to fetch options
expect(await findSelectOption('Olivia')).toBeInTheDocument();
options = await findAllSelectOptions(); options = await findAllSelectOptions();
expect(options.length).toBe(6); expect(options.length).toBe(6);
expect(options[0]).toHaveTextContent('Ava'); expect(options[0]).toHaveTextContent('Ava');
@ -317,7 +361,7 @@ test('searches for custom fields', async () => {
expect(options[4]).toHaveTextContent('Nikole'); expect(options[4]).toHaveTextContent('Nikole');
expect(options[5]).toHaveTextContent('Olivia'); expect(options[5]).toHaveTextContent('Olivia');
await type('1'); await type('1');
expect(screen.getByText(NO_DATA)).toBeInTheDocument(); expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
}); });
test('removes duplicated values', async () => { test('removes duplicated values', async () => {
@ -332,12 +376,15 @@ test('removes duplicated values', async () => {
}); });
test('renders a custom label', async () => { test('renders a custom label', async () => {
const options = [ const loadOptions = jest.fn(async () => ({
{ label: 'John', value: 1, customLabel: <h1>John</h1> }, data: [
{ label: 'Liam', value: 2, customLabel: <h1>Liam</h1> }, { label: 'John', value: 1, customLabel: <h1>John</h1> },
{ label: 'Olivia', value: 3, customLabel: <h1>Olivia</h1> }, { label: 'Liam', value: 2, customLabel: <h1>Liam</h1> },
]; { label: 'Olivia', value: 3, customLabel: <h1>Olivia</h1> },
render(<AsyncSelect {...defaultProps} options={options} />); ],
totalCount: 3,
}));
render(<AsyncSelect {...defaultProps} options={loadOptions} />);
await open(); await open();
expect(screen.getByRole('heading', { name: 'John' })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: 'John' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Liam' })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: 'Liam' })).toBeInTheDocument();
@ -345,12 +392,15 @@ test('renders a custom label', async () => {
}); });
test('searches for a word with a custom label', async () => { test('searches for a word with a custom label', async () => {
const options = [ const loadOptions = jest.fn(async () => ({
{ label: 'John', value: 1, customLabel: <h1>John</h1> }, data: [
{ label: 'Liam', value: 2, customLabel: <h1>Liam</h1> }, { label: 'John', value: 1, customLabel: <h1>John</h1> },
{ label: 'Olivia', value: 3, customLabel: <h1>Olivia</h1> }, { label: 'Liam', value: 2, customLabel: <h1>Liam</h1> },
]; { label: 'Olivia', value: 3, customLabel: <h1>Olivia</h1> },
render(<AsyncSelect {...defaultProps} options={options} />); ],
totalCount: 3,
}));
render(<AsyncSelect {...defaultProps} options={loadOptions} />);
await type('Liam'); await type('Liam');
const selectOptions = await findAllSelectOptions(); const selectOptions = await findAllSelectOptions();
expect(selectOptions.length).toBe(1); expect(selectOptions.length).toBe(1);
@ -391,7 +441,11 @@ test('does not add a new option if allowNewOptions is false', async () => {
}); });
test('adds the null option when selected in single mode', async () => { test('adds the null option when selected in single mode', async () => {
render(<AsyncSelect {...defaultProps} options={[OPTIONS[0], NULL_OPTION]} />); const loadOptions = jest.fn(async () => ({
data: [OPTIONS[0], NULL_OPTION],
totalCount: 2,
}));
render(<AsyncSelect {...defaultProps} options={loadOptions} />);
await open(); await open();
userEvent.click(await findSelectOption(NULL_OPTION.label)); userEvent.click(await findSelectOption(NULL_OPTION.label));
const values = await findAllSelectValues(); const values = await findAllSelectValues();
@ -399,12 +453,12 @@ test('adds the null option when selected in single mode', async () => {
}); });
test('adds the null option when selected in multiple mode', async () => { test('adds the null option when selected in multiple mode', async () => {
const loadOptions = jest.fn(async () => ({
data: [OPTIONS[0], NULL_OPTION],
totalCount: 2,
}));
render( render(
<AsyncSelect <AsyncSelect {...defaultProps} options={loadOptions} mode="multiple" />,
{...defaultProps}
options={[OPTIONS[0], NULL_OPTION, OPTIONS[2]]}
mode="multiple"
/>,
); );
await open(); await open();
userEvent.click(await findSelectOption(OPTIONS[0].label)); userEvent.click(await findSelectOption(OPTIONS[0].label));

View File

@ -55,7 +55,6 @@ type PickedSelectProps = Pick<
| 'autoFocus' | 'autoFocus'
| 'disabled' | 'disabled'
| 'filterOption' | 'filterOption'
| 'labelInValue'
| 'loading' | 'loading'
| 'notFoundContent' | 'notFoundContent'
| 'onChange' | 'onChange'
@ -129,11 +128,10 @@ export interface AsyncSelectProps extends PickedSelectProps {
optionFilterProps?: string[]; optionFilterProps?: string[];
/** /**
* It defines the options of the Select. * It defines the options of the Select.
* The options can be static, an array of options. * The options are async, a promise that returns
* The options can also be async, a promise that returns
* an array of options. * an array of options.
*/ */
options: OptionsType | OptionsPagePromise; options: OptionsPagePromise;
/** /**
* It defines how many results should be included * It defines how many results should be included
* in the query response. * in the query response.
@ -299,7 +297,6 @@ const AsyncSelect = (
filterOption = true, filterOption = true,
header = null, header = null,
invertSelection = false, invertSelection = false,
labelInValue = false,
lazyLoading = true, lazyLoading = true,
loading, loading,
mode = 'single', mode = 'single',
@ -322,9 +319,7 @@ const AsyncSelect = (
}: AsyncSelectProps, }: AsyncSelectProps,
ref: RefObject<AsyncSelectRef>, ref: RefObject<AsyncSelectRef>,
) => { ) => {
const isAsync = typeof options === 'function';
const isSingleMode = mode === 'single'; const isSingleMode = mode === 'single';
const shouldShowSearch = isAsync || allowNewOptions ? true : showSearch;
const [selectValue, setSelectValue] = useState(value); const [selectValue, setSelectValue] = useState(value);
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const [isLoading, setIsLoading] = useState(loading); const [isLoading, setIsLoading] = useState(loading);
@ -360,8 +355,8 @@ const AsyncSelect = (
sortSelectedFirst(a, b) || sortSelectedFirst(a, b) ||
// Only apply the custom sorter in async mode because we should // Only apply the custom sorter in async mode because we should
// preserve the options order as much as possible. // preserve the options order as much as possible.
(isAsync ? sortComparator(a, b, '') : 0), sortComparator(a, b, ''),
[isAsync, sortComparator, sortSelectedFirst], [sortComparator, sortSelectedFirst],
); );
const initialOptions = useMemo( const initialOptions = useMemo(
@ -528,7 +523,6 @@ const AsyncSelect = (
setSelectOptions(newOptions); setSelectOptions(newOptions);
} }
if ( if (
isAsync &&
!allValuesLoaded && !allValuesLoaded &&
loadingEnabled && loadingEnabled &&
!fetchedQueries.current.has(getQueryCacheKey(searchValue, 0, pageSize)) !fetchedQueries.current.has(getQueryCacheKey(searchValue, 0, pageSize))
@ -546,7 +540,7 @@ const AsyncSelect = (
vScroll.scrollTop > (vScroll.scrollHeight - vScroll.offsetHeight) * 0.7; vScroll.scrollTop > (vScroll.scrollHeight - vScroll.offsetHeight) * 0.7;
const hasMoreData = page * pageSize + pageSize < totalCount; const hasMoreData = page * pageSize + pageSize < totalCount;
if (!isLoading && isAsync && hasMoreData && thresholdReached) { if (!isLoading && hasMoreData && thresholdReached) {
const newPage = page + 1; const newPage = page + 1;
fetchPage(inputValue, newPage); fetchPage(inputValue, newPage);
} }
@ -575,30 +569,26 @@ const AsyncSelect = (
const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => { const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => {
setIsDropdownVisible(isDropdownVisible); setIsDropdownVisible(isDropdownVisible);
if (isAsync) { // loading is enabled when dropdown is open,
// loading is enabled when dropdown is open, // disabled when dropdown is closed
// disabled when dropdown is closed if (loadingEnabled !== isDropdownVisible) {
if (loadingEnabled !== isDropdownVisible) { setLoadingEnabled(isDropdownVisible);
setLoadingEnabled(isDropdownVisible); }
} // when closing dropdown, always reset loading state
// when closing dropdown, always reset loading state if (!isDropdownVisible && isLoading) {
if (!isDropdownVisible && isLoading) { // delay is for the animation of closing the dropdown
// delay is for the animation of closing the dropdown // so the dropdown doesn't flash between "Loading..." and "No data"
// so the dropdown doesn't flash between "Loading..." and "No data" // before closing.
// before closing. setTimeout(() => {
setTimeout(() => { setIsLoading(false);
setIsLoading(false); }, 250);
}, 250);
}
} }
// if no search input value, force sort options because it won't be sorted by // if no search input value, force sort options because it won't be sorted by
// `filterSort`. // `filterSort`.
if (isDropdownVisible && !inputValue && selectOptions.length > 1) { if (isDropdownVisible && !inputValue && selectOptions.length > 1) {
const sortedOptions = isAsync const sortedOptions = selectOptions
? selectOptions.slice().sort(sortComparatorForNoSearch) .slice()
: // if not in async mode, revert to the original select options .sort(sortComparatorForNoSearch);
// (with selected options still sorted to the top)
initialOptionsSorted;
if (!isEqual(sortedOptions, selectOptions)) { if (!isEqual(sortedOptions, selectOptions)) {
setSelectOptions(sortedOptions); setSelectOptions(sortedOptions);
} }
@ -627,7 +617,7 @@ const AsyncSelect = (
if (isLoading) { if (isLoading) {
return <StyledSpin size="small" />; return <StyledSpin size="small" />;
} }
if (shouldShowSearch && isDropdownVisible) { if (showSearch && isDropdownVisible) {
return <SearchOutlined />; return <SearchOutlined />;
} }
return <DownOutlined />; return <DownOutlined />;
@ -660,7 +650,7 @@ const AsyncSelect = (
); );
useEffect(() => { useEffect(() => {
if (isAsync && loadingEnabled && allowFetch) { if (loadingEnabled && allowFetch) {
// trigger fetch every time inputValue changes // trigger fetch every time inputValue changes
if (inputValue) { if (inputValue) {
debouncedFetchPage(inputValue, 0); debouncedFetchPage(inputValue, 0);
@ -668,14 +658,7 @@ const AsyncSelect = (
fetchPage('', 0); fetchPage('', 0);
} }
} }
}, [ }, [loadingEnabled, fetchPage, allowFetch, inputValue, debouncedFetchPage]);
isAsync,
loadingEnabled,
fetchPage,
allowFetch,
inputValue,
debouncedFetchPage,
]);
useEffect(() => { useEffect(() => {
if (loading !== undefined && loading !== isLoading) { if (loading !== undefined && loading !== isLoading) {
@ -706,20 +689,20 @@ const AsyncSelect = (
getPopupContainer={ getPopupContainer={
getPopupContainer || (triggerNode => triggerNode.parentNode) getPopupContainer || (triggerNode => triggerNode.parentNode)
} }
labelInValue={isAsync || labelInValue} labelInValue
maxTagCount={MAX_TAG_COUNT} maxTagCount={MAX_TAG_COUNT}
mode={mappedMode} mode={mappedMode}
notFoundContent={isLoading ? t('Loading...') : notFoundContent} notFoundContent={isLoading ? t('Loading...') : notFoundContent}
onDeselect={handleOnDeselect} onDeselect={handleOnDeselect}
onDropdownVisibleChange={handleOnDropdownVisibleChange} onDropdownVisibleChange={handleOnDropdownVisibleChange}
onPopupScroll={isAsync ? handlePagination : undefined} onPopupScroll={handlePagination}
onSearch={shouldShowSearch ? handleOnSearch : undefined} onSearch={showSearch ? handleOnSearch : undefined}
onSelect={handleOnSelect} onSelect={handleOnSelect}
onClear={handleClear} onClear={handleClear}
onChange={onChange} onChange={onChange}
options={hasCustomLabels ? undefined : fullSelectOptions} options={hasCustomLabels ? undefined : fullSelectOptions}
placeholder={placeholder} placeholder={placeholder}
showSearch={shouldShowSearch} showSearch={showSearch}
showArrow showArrow
tokenSeparators={tokenSeparators || TOKEN_SEPARATORS} tokenSeparators={tokenSeparators || TOKEN_SEPARATORS}
value={selectValue} value={selectValue}

View File

@ -16,11 +16,22 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import React, { ReactNode, useState, useCallback, useRef } from 'react'; import React, {
ReactNode,
useState,
useCallback,
useRef,
useMemo,
} from 'react';
import Button from 'src/components/Button'; import Button from 'src/components/Button';
import ControlHeader from 'src/explore/components/ControlHeader'; import ControlHeader from 'src/explore/components/ControlHeader';
import AsyncSelect, { AsyncSelectProps, AsyncSelectRef } from './AsyncSelect'; import AsyncSelect, {
import Select, { SelectProps, OptionsTypePage, OptionsType } from './Select'; AsyncSelectProps,
AsyncSelectRef,
OptionsTypePage,
} from './AsyncSelect';
import Select, { SelectProps, OptionsType } from './Select';
export default { export default {
title: 'Select', title: 'Select',
@ -452,6 +463,11 @@ export const AsynchronousSelect = ({
reject(new Error('Error while fetching the names from the server')); reject(new Error('Error while fetching the names from the server'));
}); });
const initialValue = useMemo(
() => ({ label: 'Valentina', value: 'Valentina' }),
[],
);
return ( return (
<> <>
<div <div
@ -465,11 +481,7 @@ export const AsynchronousSelect = ({
fetchOnlyOnSearch={fetchOnlyOnSearch} fetchOnlyOnSearch={fetchOnlyOnSearch}
options={withError ? fetchUserListError : fetchUserListPage} options={withError ? fetchUserListError : fetchUserListPage}
placeholder={fetchOnlyOnSearch ? 'Type anything' : 'AsyncSelect...'} placeholder={fetchOnlyOnSearch ? 'Type anything' : 'AsyncSelect...'}
value={ value={withInitialValue ? initialValue : undefined}
withInitialValue
? { label: 'Valentina', value: 'Valentina' }
: undefined
}
/> />
</div> </div>
<div <div

View File

@ -114,17 +114,24 @@ test('displays a header', async () => {
expect(screen.getByText(headerText)).toBeInTheDocument(); expect(screen.getByText(headerText)).toBeInTheDocument();
}); });
test('adds a new option if the value is not in the options', async () => { test('adds a new option if the value is not in the options, when options are empty', async () => {
const { rerender } = render( render(<Select {...defaultProps} options={[]} value={OPTIONS[0]} />);
<Select {...defaultProps} options={[]} value={OPTIONS[0]} />,
);
await open(); await open();
expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument(); expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument();
const options = await findAllSelectOptions();
expect(options).toHaveLength(1);
options.forEach((option, i) =>
expect(option).toHaveTextContent(OPTIONS[i].label),
);
});
rerender( test('adds a new option if the value is not in the options, when options have values', async () => {
render(
<Select {...defaultProps} options={[OPTIONS[1]]} value={OPTIONS[0]} />, <Select {...defaultProps} options={[OPTIONS[1]]} value={OPTIONS[0]} />,
); );
await open(); await open();
expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument();
expect(await findSelectOption(OPTIONS[1].label)).toBeInTheDocument();
const options = await findAllSelectOptions(); const options = await findAllSelectOptions();
expect(options).toHaveLength(2); expect(options).toHaveLength(2);
options.forEach((option, i) => options.forEach((option, i) =>
@ -132,6 +139,16 @@ test('adds a new option if the value is not in the options', async () => {
); );
}); });
test('does not add a new option if the value is already in the options', async () => {
render(
<Select {...defaultProps} options={[OPTIONS[0]]} value={OPTIONS[0]} />,
);
await open();
expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument();
const options = await findAllSelectOptions();
expect(options).toHaveLength(1);
});
test('inverts the selection', async () => { test('inverts the selection', async () => {
render(<Select {...defaultProps} invertSelection />); render(<Select {...defaultProps} invertSelection />);
await open(); await open();

View File

@ -21,13 +21,10 @@ import React, {
ReactElement, ReactElement,
ReactNode, ReactNode,
RefObject, RefObject,
UIEvent,
useEffect, useEffect,
useMemo, useMemo,
useState, useState,
useRef,
useCallback, useCallback,
useImperativeHandle,
} from 'react'; } from 'react';
import { ensureIsArray, styled, t } from '@superset-ui/core'; import { ensureIsArray, styled, t } from '@superset-ui/core';
import AntdSelect, { import AntdSelect, {
@ -37,11 +34,8 @@ import AntdSelect, {
} from 'antd/lib/select'; } from 'antd/lib/select';
import { DownOutlined, SearchOutlined } from '@ant-design/icons'; import { DownOutlined, SearchOutlined } from '@ant-design/icons';
import { Spin } from 'antd'; import { Spin } from 'antd';
import debounce from 'lodash/debounce';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import Icons from 'src/components/Icons'; import Icons from 'src/components/Icons';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import { SLOW_DEBOUNCE } from 'src/constants';
import { rankedSearchCompare } from 'src/utils/rankedSearchCompare'; import { rankedSearchCompare } from 'src/utils/rankedSearchCompare';
import { getValue, hasOption, isLabeledValue } from './utils'; import { getValue, hasOption, isLabeledValue } from './utils';
@ -72,19 +66,6 @@ type PickedSelectProps = Pick<
export type OptionsType = Exclude<AntdSelectAllProps['options'], undefined>; export type OptionsType = Exclude<AntdSelectAllProps['options'], undefined>;
export type OptionsTypePage = {
data: OptionsType;
totalCount: number;
};
export type OptionsPagePromise = (
search: string,
page: number,
pageSize: number,
) => Promise<OptionsTypePage>;
export type SelectRef = HTMLInputElement & { clearCache: () => void };
export interface SelectProps extends PickedSelectProps { export interface SelectProps extends PickedSelectProps {
/** /**
* It enables the user to create new options. * It enables the user to create new options.
@ -103,13 +84,6 @@ export interface SelectProps extends PickedSelectProps {
* Can be any ReactNode. * Can be any ReactNode.
*/ */
header?: ReactNode; header?: ReactNode;
/**
* It fires a request against the server after
* the first interaction and not on render.
* Works in async mode only (See the options property).
* True by default.
*/
lazyLoading?: boolean;
/** /**
* It defines whether the Select should allow for the * It defines whether the Select should allow for the
* selection of multiple options or single. * selection of multiple options or single.
@ -133,13 +107,7 @@ export interface SelectProps extends PickedSelectProps {
* The options can also be async, a promise that returns * The options can also be async, a promise that returns
* an array of options. * an array of options.
*/ */
options: OptionsType | OptionsPagePromise; options: OptionsType;
/**
* It defines how many results should be included
* in the query response.
* Works in async mode only (See the options property).
*/
pageSize?: number;
/** /**
* It shows a stop-outlined icon at the far right of a selected * It shows a stop-outlined icon at the far right of a selected
* option instead of the default checkmark. * option instead of the default checkmark.
@ -148,19 +116,6 @@ export interface SelectProps extends PickedSelectProps {
* False by default. * False by default.
*/ */
invertSelection?: boolean; invertSelection?: boolean;
/**
* It fires a request against the server only after
* searching.
* Works in async mode only (See the options property).
* Undefined by default.
*/
fetchOnlyOnSearch?: boolean;
/**
* It provides a callback function when an error
* is generated after a request is fired.
* Works in async mode only (See the options property).
*/
onError?: (error: string) => void;
/** /**
* Customize how filtered options are sorted while users search. * Customize how filtered options are sorted while users search.
* Will not apply to predefined `options` array when users are not searching. * Will not apply to predefined `options` array when users are not searching.
@ -195,25 +150,6 @@ const StyledCheckOutlined = styled(Icons.CheckOutlined)`
vertical-align: 0; vertical-align: 0;
`; `;
const StyledError = styled.div`
${({ theme }) => `
display: flex;
justify-content: center;
align-items: flex-start;
width: 100%;
padding: ${theme.gridUnit * 2}px;
color: ${theme.colors.error.base};
& svg {
margin-right: ${theme.gridUnit * 2}px;
}
`}
`;
const StyledErrorMessage = styled.div`
overflow: hidden;
text-overflow: ellipsis;
`;
const StyledSpin = styled(Spin)` const StyledSpin = styled(Spin)`
margin-top: ${({ theme }) => -theme.gridUnit}px; margin-top: ${({ theme }) => -theme.gridUnit}px;
`; `;
@ -228,15 +164,8 @@ const StyledLoadingText = styled.div`
const MAX_TAG_COUNT = 4; const MAX_TAG_COUNT = 4;
const TOKEN_SEPARATORS = [',', '\n', '\t', ';']; const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
const DEFAULT_PAGE_SIZE = 100;
const EMPTY_OPTIONS: OptionsType = []; const EMPTY_OPTIONS: OptionsType = [];
const Error = ({ error }: { error: string }) => (
<StyledError>
<Icons.ErrorSolid /> <StyledErrorMessage>{error}</StyledErrorMessage>
</StyledError>
);
export const DEFAULT_SORT_COMPARATOR = ( export const DEFAULT_SORT_COMPARATOR = (
a: AntdLabeledValue, a: AntdLabeledValue,
b: AntdLabeledValue, b: AntdLabeledValue,
@ -273,9 +202,6 @@ export const propertyComparator =
return (a[property] as number) - (b[property] as number); return (a[property] as number) - (b[property] as number);
}; };
const getQueryCacheKey = (value: string, page: number, pageSize: number) =>
`${value};${page};${pageSize}`;
/** /**
* This component is a customized version of the Antdesign 4.X Select component * This component is a customized version of the Antdesign 4.X Select component
* https://ant.design/components/select/. * https://ant.design/components/select/.
@ -295,23 +221,19 @@ const Select = (
allowClear, allowClear,
allowNewOptions = false, allowNewOptions = false,
ariaLabel, ariaLabel,
fetchOnlyOnSearch,
filterOption = true, filterOption = true,
header = null, header = null,
invertSelection = false, invertSelection = false,
labelInValue = false, labelInValue = false,
lazyLoading = true,
loading, loading,
mode = 'single', mode = 'single',
name, name,
notFoundContent, notFoundContent,
onError,
onChange, onChange,
onClear, onClear,
onDropdownVisibleChange, onDropdownVisibleChange,
optionFilterProps = ['label', 'value'], optionFilterProps = ['label', 'value'],
options, options,
pageSize = DEFAULT_PAGE_SIZE,
placeholder = t('Select ...'), placeholder = t('Select ...'),
showSearch = true, showSearch = true,
sortComparator = DEFAULT_SORT_COMPARATOR, sortComparator = DEFAULT_SORT_COMPARATOR,
@ -320,27 +242,19 @@ const Select = (
getPopupContainer, getPopupContainer,
...props ...props
}: SelectProps, }: SelectProps,
ref: RefObject<SelectRef>, ref: RefObject<HTMLInputElement>,
) => { ) => {
const isAsync = typeof options === 'function';
const isSingleMode = mode === 'single'; const isSingleMode = mode === 'single';
const shouldShowSearch = isAsync || allowNewOptions ? true : showSearch; const shouldShowSearch = allowNewOptions ? true : showSearch;
const [selectValue, setSelectValue] = useState(value); const [selectValue, setSelectValue] = useState(value);
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const [isLoading, setIsLoading] = useState(loading); const [isLoading, setIsLoading] = useState(loading);
const [error, setError] = useState('');
const [isDropdownVisible, setIsDropdownVisible] = useState(false); const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const [page, setPage] = useState(0);
const [totalCount, setTotalCount] = useState(0);
const [loadingEnabled, setLoadingEnabled] = useState(!lazyLoading);
const [allValuesLoaded, setAllValuesLoaded] = useState(false);
const fetchedQueries = useRef(new Map<string, number>());
const mappedMode = isSingleMode const mappedMode = isSingleMode
? undefined ? undefined
: allowNewOptions : allowNewOptions
? 'tags' ? 'tags'
: 'multiple'; : 'multiple';
const allowFetch = !fetchOnlyOnSearch || inputValue;
const sortSelectedFirst = useCallback( const sortSelectedFirst = useCallback(
(a: AntdLabeledValue, b: AntdLabeledValue) => (a: AntdLabeledValue, b: AntdLabeledValue) =>
@ -355,22 +269,14 @@ const Select = (
sortSelectedFirst(a, b) || sortComparator(a, b, inputValue), sortSelectedFirst(a, b) || sortComparator(a, b, inputValue),
[inputValue, sortComparator, sortSelectedFirst], [inputValue, sortComparator, sortSelectedFirst],
); );
const sortComparatorForNoSearch = useCallback(
(a: AntdLabeledValue, b: AntdLabeledValue) =>
sortSelectedFirst(a, b) ||
// Only apply the custom sorter in async mode because we should
// preserve the options order as much as possible.
(isAsync ? sortComparator(a, b, '') : 0),
[isAsync, sortComparator, sortSelectedFirst],
);
const initialOptions = useMemo( const initialOptions = useMemo(
() => (options && Array.isArray(options) ? options.slice() : EMPTY_OPTIONS), () => (options && Array.isArray(options) ? options.slice() : EMPTY_OPTIONS),
[options], [options],
); );
const initialOptionsSorted = useMemo( const initialOptionsSorted = useMemo(
() => initialOptions.slice().sort(sortComparatorForNoSearch), () => initialOptions.slice().sort(sortSelectedFirst),
[initialOptions, sortComparatorForNoSearch], [initialOptions, sortSelectedFirst],
); );
const [selectOptions, setSelectOptions] = const [selectOptions, setSelectOptions] =
@ -427,89 +333,6 @@ const Select = (
setInputValue(''); setInputValue('');
}; };
const internalOnError = useCallback(
(response: Response) =>
getClientErrorObject(response).then(e => {
const { error } = e;
setError(error);
if (onError) {
onError(error);
}
}),
[onError],
);
const mergeData = useCallback(
(data: OptionsType) => {
let mergedData: OptionsType = [];
if (data && Array.isArray(data) && data.length) {
// unique option values should always be case sensitive so don't lowercase
const dataValues = new Set(data.map(opt => opt.value));
// merges with existing and creates unique options
setSelectOptions(prevOptions => {
mergedData = prevOptions
.filter(previousOption => !dataValues.has(previousOption.value))
.concat(data)
.sort(sortComparatorForNoSearch);
return mergedData;
});
}
return mergedData;
},
[sortComparatorForNoSearch],
);
const fetchPage = useMemo(
() => (search: string, page: number) => {
setPage(page);
if (allValuesLoaded) {
setIsLoading(false);
return;
}
const key = getQueryCacheKey(search, page, pageSize);
const cachedCount = fetchedQueries.current.get(key);
if (cachedCount !== undefined) {
setTotalCount(cachedCount);
setIsLoading(false);
return;
}
setIsLoading(true);
const fetchOptions = options as OptionsPagePromise;
fetchOptions(search, page, pageSize)
.then(({ data, totalCount }: OptionsTypePage) => {
const mergedData = mergeData(data);
fetchedQueries.current.set(key, totalCount);
setTotalCount(totalCount);
if (
!fetchOnlyOnSearch &&
value === '' &&
mergedData.length >= totalCount
) {
setAllValuesLoaded(true);
}
})
.catch(internalOnError)
.finally(() => {
setIsLoading(false);
});
},
[
allValuesLoaded,
fetchOnlyOnSearch,
mergeData,
internalOnError,
options,
pageSize,
value,
],
);
const debouncedFetchPage = useMemo(
() => debounce(fetchPage, SLOW_DEBOUNCE),
[fetchPage],
);
const handleOnSearch = (search: string) => { const handleOnSearch = (search: string) => {
const searchValue = search.trim(); const searchValue = search.trim();
if (allowNewOptions && isSingleMode) { if (allowNewOptions && isSingleMode) {
@ -527,31 +350,9 @@ const Select = (
: cleanSelectOptions; : cleanSelectOptions;
setSelectOptions(newOptions); setSelectOptions(newOptions);
} }
if (
isAsync &&
!allValuesLoaded &&
loadingEnabled &&
!fetchedQueries.current.has(getQueryCacheKey(searchValue, 0, pageSize))
) {
// if fetch only on search but search value is empty, then should not be
// in loading state
setIsLoading(!(fetchOnlyOnSearch && !searchValue));
}
setInputValue(search); setInputValue(search);
}; };
const handlePagination = (e: UIEvent<HTMLElement>) => {
const vScroll = e.currentTarget;
const thresholdReached =
vScroll.scrollTop > (vScroll.scrollHeight - vScroll.offsetHeight) * 0.7;
const hasMoreData = page * pageSize + pageSize < totalCount;
if (!isLoading && isAsync && hasMoreData && thresholdReached) {
const newPage = page + 1;
fetchPage(inputValue, newPage);
}
};
const handleFilterOption = (search: string, option: AntdLabeledValue) => { const handleFilterOption = (search: string, option: AntdLabeledValue) => {
if (typeof filterOption === 'function') { if (typeof filterOption === 'function') {
return filterOption(search, option); return filterOption(search, option);
@ -575,35 +376,13 @@ const Select = (
const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => { const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => {
setIsDropdownVisible(isDropdownVisible); setIsDropdownVisible(isDropdownVisible);
if (isAsync) {
// loading is enabled when dropdown is open,
// disabled when dropdown is closed
if (loadingEnabled !== isDropdownVisible) {
setLoadingEnabled(isDropdownVisible);
}
// when closing dropdown, always reset loading state
if (!isDropdownVisible && isLoading) {
// delay is for the animation of closing the dropdown
// so the dropdown doesn't flash between "Loading..." and "No data"
// before closing.
setTimeout(() => {
setIsLoading(false);
}, 250);
}
}
// if no search input value, force sort options because it won't be sorted by // if no search input value, force sort options because it won't be sorted by
// `filterSort`. // `filterSort`.
if (isDropdownVisible && !inputValue && selectOptions.length > 1) { if (isDropdownVisible && !inputValue && selectOptions.length > 1) {
const sortedOptions = isAsync if (!isEqual(initialOptionsSorted, selectOptions)) {
? selectOptions.slice().sort(sortComparatorForNoSearch) setSelectOptions(initialOptionsSorted);
: // if not in async mode, revert to the original select options
// (with selected options still sorted to the top)
initialOptionsSorted;
if (!isEqual(sortedOptions, selectOptions)) {
setSelectOptions(sortedOptions);
} }
} }
if (onDropdownVisibleChange) { if (onDropdownVisibleChange) {
onDropdownVisibleChange(isDropdownVisible); onDropdownVisibleChange(isDropdownVisible);
} }
@ -618,7 +397,7 @@ const Select = (
if (isLoading && fullSelectOptions.length === 0) { if (isLoading && fullSelectOptions.length === 0) {
return <StyledLoadingText>{t('Loading...')}</StyledLoadingText>; return <StyledLoadingText>{t('Loading...')}</StyledLoadingText>;
} }
return error ? <Error error={error} /> : originNode; return originNode;
}; };
// use a function instead of component since every rerender of the // use a function instead of component since every rerender of the
@ -642,8 +421,6 @@ const Select = (
useEffect(() => { useEffect(() => {
// when `options` list is updated from component prop, reset states // when `options` list is updated from component prop, reset states
fetchedQueries.current.clear();
setAllValuesLoaded(false);
setSelectOptions(initialOptions); setSelectOptions(initialOptions);
}, [initialOptions]); }, [initialOptions]);
@ -651,49 +428,12 @@ const Select = (
setSelectValue(value); setSelectValue(value);
}, [value]); }, [value]);
// Stop the invocation of the debounced function after unmounting
useEffect(
() => () => {
debouncedFetchPage.cancel();
},
[debouncedFetchPage],
);
useEffect(() => {
if (isAsync && loadingEnabled && allowFetch) {
// trigger fetch every time inputValue changes
if (inputValue) {
debouncedFetchPage(inputValue, 0);
} else {
fetchPage('', 0);
}
}
}, [
isAsync,
loadingEnabled,
fetchPage,
allowFetch,
inputValue,
debouncedFetchPage,
]);
useEffect(() => { useEffect(() => {
if (loading !== undefined && loading !== isLoading) { if (loading !== undefined && loading !== isLoading) {
setIsLoading(loading); setIsLoading(loading);
} }
}, [isLoading, loading]); }, [isLoading, loading]);
const clearCache = () => fetchedQueries.current.clear();
useImperativeHandle(
ref,
() => ({
...(ref.current as HTMLInputElement),
clearCache,
}),
[ref],
);
return ( return (
<StyledContainer> <StyledContainer>
{header} {header}
@ -706,13 +446,13 @@ const Select = (
getPopupContainer={ getPopupContainer={
getPopupContainer || (triggerNode => triggerNode.parentNode) getPopupContainer || (triggerNode => triggerNode.parentNode)
} }
labelInValue={isAsync || labelInValue} labelInValue={labelInValue}
maxTagCount={MAX_TAG_COUNT} maxTagCount={MAX_TAG_COUNT}
mode={mappedMode} mode={mappedMode}
notFoundContent={isLoading ? t('Loading...') : notFoundContent} notFoundContent={isLoading ? t('Loading...') : notFoundContent}
onDeselect={handleOnDeselect} onDeselect={handleOnDeselect}
onDropdownVisibleChange={handleOnDropdownVisibleChange} onDropdownVisibleChange={handleOnDropdownVisibleChange}
onPopupScroll={isAsync ? handlePagination : undefined} onPopupScroll={undefined}
onSearch={shouldShowSearch ? handleOnSearch : undefined} onSearch={shouldShowSearch ? handleOnSearch : undefined}
onSelect={handleOnSelect} onSelect={handleOnSelect}
onClear={handleClear} onClear={handleClear}

View File

@ -336,7 +336,6 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
filterOption={handleFilterOption} filterOption={handleFilterOption}
header={header} header={header}
labelInValue labelInValue
lazyLoading={false}
loading={loadingTables} loading={loadingTables}
name="select-table" name="select-table"
onChange={(options: TableOption | TableOption[]) => onChange={(options: TableOption | TableOption[]) =>

View File

@ -23,7 +23,6 @@ import {
QueryFormData, QueryFormData,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { RefObject } from 'react'; import { RefObject } from 'react';
import { SelectRef } from 'src/components/Select/Select';
import { PluginFilterHooks, PluginFilterStylesProps } from '../types'; import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
interface PluginFilterGroupByCustomizeProps { interface PluginFilterGroupByCustomizeProps {
@ -41,7 +40,7 @@ export type PluginFilterGroupByProps = PluginFilterStylesProps & {
data: DataRecord[]; data: DataRecord[];
filterState: FilterState; filterState: FilterState;
formData: PluginFilterGroupByQueryFormData; formData: PluginFilterGroupByQueryFormData;
inputRef: RefObject<SelectRef>; inputRef: RefObject<HTMLInputElement>;
} & PluginFilterHooks; } & PluginFilterHooks;
export const DEFAULT_FORM_DATA: PluginFilterGroupByCustomizeProps = { export const DEFAULT_FORM_DATA: PluginFilterGroupByCustomizeProps = {

View File

@ -23,7 +23,6 @@ import {
QueryFormData, QueryFormData,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { RefObject } from 'react'; import { RefObject } from 'react';
import { SelectRef } from 'src/components/Select/Select';
import { PluginFilterHooks, PluginFilterStylesProps } from '../types'; import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
interface PluginFilterTimeColumnCustomizeProps { interface PluginFilterTimeColumnCustomizeProps {
@ -40,7 +39,7 @@ export type PluginFilterTimeColumnProps = PluginFilterStylesProps & {
data: DataRecord[]; data: DataRecord[];
filterState: FilterState; filterState: FilterState;
formData: PluginFilterTimeColumnQueryFormData; formData: PluginFilterTimeColumnQueryFormData;
inputRef: RefObject<SelectRef>; inputRef: RefObject<HTMLInputElement>;
} & PluginFilterHooks; } & PluginFilterHooks;
export const DEFAULT_FORM_DATA: PluginFilterTimeColumnCustomizeProps = { export const DEFAULT_FORM_DATA: PluginFilterTimeColumnCustomizeProps = {

View File

@ -18,7 +18,6 @@
*/ */
import { FilterState, QueryFormData, DataRecord } from '@superset-ui/core'; import { FilterState, QueryFormData, DataRecord } from '@superset-ui/core';
import { RefObject } from 'react'; import { RefObject } from 'react';
import { SelectRef } from 'src/components/Select/Select';
import { PluginFilterHooks, PluginFilterStylesProps } from '../types'; import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
interface PluginFilterTimeGrainCustomizeProps { interface PluginFilterTimeGrainCustomizeProps {
@ -34,7 +33,7 @@ export type PluginFilterTimeGrainProps = PluginFilterStylesProps & {
data: DataRecord[]; data: DataRecord[];
filterState: FilterState; filterState: FilterState;
formData: PluginFilterTimeGrainQueryFormData; formData: PluginFilterTimeGrainQueryFormData;
inputRef: RefObject<SelectRef>; inputRef: RefObject<HTMLInputElement>;
} & PluginFilterHooks; } & PluginFilterHooks;
export const DEFAULT_FORM_DATA: PluginFilterTimeGrainCustomizeProps = { export const DEFAULT_FORM_DATA: PluginFilterTimeGrainCustomizeProps = {