This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new fe91974163 chore: Remove unecessary code from async and sync select 
components (#20690)
fe91974163 is described below

commit fe919741632e677ff14cdd35f886c8b2a9f0d4de
Author: cccs-RyanK <[email protected]>
AuthorDate: Thu Jul 28 09:09:37 2022 -0400

    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
---
 .../src/components/DatabaseSelector/index.tsx      |   1 -
 .../src/components/ListView/Filters/Select.tsx     |  42 +++-
 .../src/components/Select/AsyncSelect.test.tsx     | 150 +++++++----
 .../src/components/Select/AsyncSelect.tsx          |  73 +++---
 .../src/components/Select/Select.stories.tsx       |  28 ++-
 .../src/components/Select/Select.test.tsx          |  27 +-
 superset-frontend/src/components/Select/Select.tsx | 280 +--------------------
 .../src/components/TableSelector/index.tsx         |   1 -
 .../src/filters/components/GroupBy/types.ts        |   3 +-
 .../src/filters/components/TimeColumn/types.ts     |   3 +-
 .../src/filters/components/TimeGrain/types.ts      |   3 +-
 11 files changed, 214 insertions(+), 397 deletions(-)

diff --git a/superset-frontend/src/components/DatabaseSelector/index.tsx 
b/superset-frontend/src/components/DatabaseSelector/index.tsx
index e972e95b00..1df7f78a3b 100644
--- a/superset-frontend/src/components/DatabaseSelector/index.tsx
+++ b/superset-frontend/src/components/DatabaseSelector/index.tsx
@@ -302,7 +302,6 @@ export default function DatabaseSelector({
         disabled={!currentDb || readOnly}
         header={<FormLabel>{t('Schema')}</FormLabel>}
         labelInValue
-        lazyLoading={false}
         loading={loadingSchemas}
         name="select-schema"
         placeholder={t('Select schema or type schema name')}
diff --git a/superset-frontend/src/components/ListView/Filters/Select.tsx 
b/superset-frontend/src/components/ListView/Filters/Select.tsx
index 525061fd27..ecda25a81f 100644
--- a/superset-frontend/src/components/ListView/Filters/Select.tsx
+++ b/superset-frontend/src/components/ListView/Filters/Select.tsx
@@ -26,6 +26,7 @@ import { t } from '@superset-ui/core';
 import { Select } from 'src/components';
 import { Filter, SelectOption } from 'src/components/ListView/types';
 import { FormLabel } from 'src/components/Form';
+import AsyncSelect from 'src/components/Select/AsyncSelect';
 import { FilterContainer, BaseFilter, FilterHandler } from './Base';
 
 interface SelectFilterProps extends BaseFilter {
@@ -86,19 +87,34 @@ function SelectFilter(
 
   return (
     <FilterContainer>
-      <Select
-        allowClear
-        ariaLabel={typeof Header === 'string' ? Header : name || t('Filter')}
-        labelInValue
-        data-test="filters-select"
-        header={<FormLabel>{Header}</FormLabel>}
-        onChange={onChange}
-        onClear={onClear}
-        options={fetchSelects ? fetchAndFormatSelects : selects}
-        placeholder={t('Select or type a value')}
-        showSearch
-        value={selectedOption}
-      />
+      {fetchSelects ? (
+        <AsyncSelect
+          allowClear
+          ariaLabel={typeof Header === 'string' ? Header : name || t('Filter')}
+          data-test="filters-select"
+          header={<FormLabel>{Header}</FormLabel>}
+          onChange={onChange}
+          onClear={onClear}
+          options={fetchAndFormatSelects}
+          placeholder={t('Select or type a value')}
+          showSearch
+          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>
   );
 }
diff --git a/superset-frontend/src/components/Select/AsyncSelect.test.tsx 
b/superset-frontend/src/components/Select/AsyncSelect.test.tsx
index dc6eff35d9..8a50002c86 100644
--- a/superset-frontend/src/components/Select/AsyncSelect.test.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.test.tsx
@@ -60,10 +60,16 @@ const loadOptions = async (search: string, page: number, 
pageSize: number) => {
   const start = page * pageSize;
   const deleteCount =
     start + pageSize < totalCount ? pageSize : totalCount - start;
-  const data = OPTIONS.filter(option => option.label.match(search)).splice(
-    start,
-    deleteCount,
-  );
+  const searchValue = search.trim().toLowerCase();
+  const optionFilterProps = ['label', 'value', 'gender'];
+  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 {
     data,
     totalCount: OPTIONS.length,
@@ -74,7 +80,7 @@ const defaultProps = {
   allowClear: true,
   ariaLabel: ARIA_LABEL,
   labelInValue: true,
-  options: OPTIONS,
+  options: loadOptions,
   pageSize: 10,
   showSearch: true,
 };
@@ -129,17 +135,31 @@ test('displays a header', async () => {
   expect(screen.getByText(headerText)).toBeInTheDocument();
 });
 
-test('adds a new option if the value is not in the options', async () => {
-  const { rerender } = render(
-    <AsyncSelect {...defaultProps} options={[]} value={OPTIONS[0]} />,
+test('adds a new option if the value is not in the options, when options are 
empty', async () => {
+  const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 }));
+  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);
+  options.forEach((option, i) =>
+    expect(option).toHaveTextContent(OPTIONS[i].label),
+  );
+});
 
-  rerender(
-    <AsyncSelect {...defaultProps} options={[OPTIONS[1]]} value={OPTIONS[0]} 
/>,
+test('adds a new option if the value is not in the options, when options have 
values', async () => {
+  const loadOptions = jest.fn(async () => ({
+    data: [OPTIONS[1]],
+    totalCount: 1,
+  }));
+  render(
+    <AsyncSelect {...defaultProps} options={loadOptions} value={OPTIONS[0]} />,
   );
   await open();
+  expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument();
+  expect(await findSelectOption(OPTIONS[1].label)).toBeInTheDocument();
   const options = await findAllSelectOptions();
   expect(options).toHaveLength(2);
   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 () => {
   render(<AsyncSelect {...defaultProps} invertSelection />);
   await open();
@@ -155,8 +189,11 @@ test('inverts the selection', async () => {
 });
 
 test('sort the options by label if no sort comparator is provided', async () 
=> {
-  const unsortedOptions = [...OPTIONS].sort(() => Math.random());
-  render(<AsyncSelect {...defaultProps} options={unsortedOptions} />);
+  const loadUnsortedOptions = jest.fn(async () => ({
+    data: [...OPTIONS].sort(() => Math.random()),
+    totalCount: 2,
+  }));
+  render(<AsyncSelect {...defaultProps} options={loadUnsortedOptions} />);
   await open();
   const options = await findAllSelectOptions();
   options.forEach((option, key) =>
@@ -250,20 +287,23 @@ test('searches for label or value', async () => {
   render(<AsyncSelect {...defaultProps} />);
   const search = option.value;
   await type(search.toString());
+  expect(await findSelectOption(option.label)).toBeInTheDocument();
   const options = await findAllSelectOptions();
   expect(options.length).toBe(1);
   expect(options[0]).toHaveTextContent(option.label);
 });
 
 test('search order exact and startWith match first', async () => {
-  render(<AsyncSelect {...defaultProps} />);
+  render(<AsyncSelect {...defaultProps} options={loadOptions} />);
+  await open();
   await type('Her');
+  expect(await findSelectOption('Guilherme')).toBeInTheDocument();
   const options = await findAllSelectOptions();
   expect(options.length).toBe(4);
-  expect(options[0]?.textContent).toEqual('Her');
-  expect(options[1]?.textContent).toEqual('Herme');
-  expect(options[2]?.textContent).toEqual('Cher');
-  expect(options[3]?.textContent).toEqual('Guilherme');
+  expect(options[0]).toHaveTextContent('Her');
+  expect(options[1]).toHaveTextContent('Herme');
+  expect(options[2]).toHaveTextContent('Cher');
+  expect(options[3]).toHaveTextContent('Guilherme');
 });
 
 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 () => {
-  render(
-    <AsyncSelect
-      {...defaultProps}
-      options={[
-        { value: 'Cac' },
-        { value: 'abac' },
-        { value: 'acbc' },
-        { value: 'CAc' },
-      ]}
-    />,
-  );
+  const loadOptions = jest.fn(async () => ({
+    data: [
+      { value: 'Cac' },
+      { value: 'abac' },
+      { value: 'acbc' },
+      { value: 'CAc' },
+    ],
+    totalCount: 4,
+  }));
+  render(<AsyncSelect {...defaultProps} options={loadOptions} />);
   await type('Ac');
   const options = await findAllSelectOptions();
   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 () => {
-  render(<AsyncSelect {...defaultProps} />);
+  render(<AsyncSelect {...defaultProps} options={loadOptions} />);
   await type('{shift}');
   expect(screen.queryByText(LOADING)).not.toBeInTheDocument();
 });
@@ -303,11 +342,16 @@ test('searches for custom fields', async () => {
   render(
     <AsyncSelect {...defaultProps} optionFilterProps={['label', 'gender']} />,
   );
+  await open();
   await type('Liam');
+  // Liam is on the second page. need to wait to fetch options
+  expect(await findSelectOption('Liam')).toBeInTheDocument();
   let options = await findAllSelectOptions();
   expect(options.length).toBe(1);
   expect(options[0]).toHaveTextContent('Liam');
   await type('Female');
+  // Olivia is on the second page. need to wait to fetch options
+  expect(await findSelectOption('Olivia')).toBeInTheDocument();
   options = await findAllSelectOptions();
   expect(options.length).toBe(6);
   expect(options[0]).toHaveTextContent('Ava');
@@ -317,7 +361,7 @@ test('searches for custom fields', async () => {
   expect(options[4]).toHaveTextContent('Nikole');
   expect(options[5]).toHaveTextContent('Olivia');
   await type('1');
-  expect(screen.getByText(NO_DATA)).toBeInTheDocument();
+  expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
 });
 
 test('removes duplicated values', async () => {
@@ -332,12 +376,15 @@ test('removes duplicated values', async () => {
 });
 
 test('renders a custom label', async () => {
-  const options = [
-    { label: 'John', value: 1, customLabel: <h1>John</h1> },
-    { label: 'Liam', value: 2, customLabel: <h1>Liam</h1> },
-    { label: 'Olivia', value: 3, customLabel: <h1>Olivia</h1> },
-  ];
-  render(<AsyncSelect {...defaultProps} options={options} />);
+  const loadOptions = jest.fn(async () => ({
+    data: [
+      { label: 'John', value: 1, customLabel: <h1>John</h1> },
+      { label: 'Liam', value: 2, customLabel: <h1>Liam</h1> },
+      { label: 'Olivia', value: 3, customLabel: <h1>Olivia</h1> },
+    ],
+    totalCount: 3,
+  }));
+  render(<AsyncSelect {...defaultProps} options={loadOptions} />);
   await open();
   expect(screen.getByRole('heading', { name: 'John' })).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 () => {
-  const options = [
-    { label: 'John', value: 1, customLabel: <h1>John</h1> },
-    { label: 'Liam', value: 2, customLabel: <h1>Liam</h1> },
-    { label: 'Olivia', value: 3, customLabel: <h1>Olivia</h1> },
-  ];
-  render(<AsyncSelect {...defaultProps} options={options} />);
+  const loadOptions = jest.fn(async () => ({
+    data: [
+      { label: 'John', value: 1, customLabel: <h1>John</h1> },
+      { label: 'Liam', value: 2, customLabel: <h1>Liam</h1> },
+      { label: 'Olivia', value: 3, customLabel: <h1>Olivia</h1> },
+    ],
+    totalCount: 3,
+  }));
+  render(<AsyncSelect {...defaultProps} options={loadOptions} />);
   await type('Liam');
   const selectOptions = await findAllSelectOptions();
   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 () => {
-  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();
   userEvent.click(await findSelectOption(NULL_OPTION.label));
   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 () => {
+  const loadOptions = jest.fn(async () => ({
+    data: [OPTIONS[0], NULL_OPTION],
+    totalCount: 2,
+  }));
   render(
-    <AsyncSelect
-      {...defaultProps}
-      options={[OPTIONS[0], NULL_OPTION, OPTIONS[2]]}
-      mode="multiple"
-    />,
+    <AsyncSelect {...defaultProps} options={loadOptions} mode="multiple" />,
   );
   await open();
   userEvent.click(await findSelectOption(OPTIONS[0].label));
diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx 
b/superset-frontend/src/components/Select/AsyncSelect.tsx
index 98f146f15f..b95f2d8f0d 100644
--- a/superset-frontend/src/components/Select/AsyncSelect.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.tsx
@@ -55,7 +55,6 @@ type PickedSelectProps = Pick<
   | 'autoFocus'
   | 'disabled'
   | 'filterOption'
-  | 'labelInValue'
   | 'loading'
   | 'notFoundContent'
   | 'onChange'
@@ -129,11 +128,10 @@ export interface AsyncSelectProps extends 
PickedSelectProps {
   optionFilterProps?: string[];
   /**
    * It defines the options of the Select.
-   * The options can be static, an array of options.
-   * The options can also be async, a promise that returns
+   * The options are async, a promise that returns
    * an array of options.
    */
-  options: OptionsType | OptionsPagePromise;
+  options: OptionsPagePromise;
   /**
    * It defines how many results should be included
    * in the query response.
@@ -299,7 +297,6 @@ const AsyncSelect = (
     filterOption = true,
     header = null,
     invertSelection = false,
-    labelInValue = false,
     lazyLoading = true,
     loading,
     mode = 'single',
@@ -322,9 +319,7 @@ const AsyncSelect = (
   }: AsyncSelectProps,
   ref: RefObject<AsyncSelectRef>,
 ) => {
-  const isAsync = typeof options === 'function';
   const isSingleMode = mode === 'single';
-  const shouldShowSearch = isAsync || allowNewOptions ? true : showSearch;
   const [selectValue, setSelectValue] = useState(value);
   const [inputValue, setInputValue] = useState('');
   const [isLoading, setIsLoading] = useState(loading);
@@ -360,8 +355,8 @@ const AsyncSelect = (
       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],
+      sortComparator(a, b, ''),
+    [sortComparator, sortSelectedFirst],
   );
 
   const initialOptions = useMemo(
@@ -528,7 +523,6 @@ const AsyncSelect = (
       setSelectOptions(newOptions);
     }
     if (
-      isAsync &&
       !allValuesLoaded &&
       loadingEnabled &&
       !fetchedQueries.current.has(getQueryCacheKey(searchValue, 0, pageSize))
@@ -546,7 +540,7 @@ const AsyncSelect = (
       vScroll.scrollTop > (vScroll.scrollHeight - vScroll.offsetHeight) * 0.7;
     const hasMoreData = page * pageSize + pageSize < totalCount;
 
-    if (!isLoading && isAsync && hasMoreData && thresholdReached) {
+    if (!isLoading && hasMoreData && thresholdReached) {
       const newPage = page + 1;
       fetchPage(inputValue, newPage);
     }
@@ -575,30 +569,26 @@ const AsyncSelect = (
   const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => {
     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);
-      }
+    // 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
     // `filterSort`.
     if (isDropdownVisible && !inputValue && selectOptions.length > 1) {
-      const sortedOptions = isAsync
-        ? selectOptions.slice().sort(sortComparatorForNoSearch)
-        : // if not in async mode, revert to the original select options
-          // (with selected options still sorted to the top)
-          initialOptionsSorted;
+      const sortedOptions = selectOptions
+        .slice()
+        .sort(sortComparatorForNoSearch);
       if (!isEqual(sortedOptions, selectOptions)) {
         setSelectOptions(sortedOptions);
       }
@@ -627,7 +617,7 @@ const AsyncSelect = (
     if (isLoading) {
       return <StyledSpin size="small" />;
     }
-    if (shouldShowSearch && isDropdownVisible) {
+    if (showSearch && isDropdownVisible) {
       return <SearchOutlined />;
     }
     return <DownOutlined />;
@@ -660,7 +650,7 @@ const AsyncSelect = (
   );
 
   useEffect(() => {
-    if (isAsync && loadingEnabled && allowFetch) {
+    if (loadingEnabled && allowFetch) {
       // trigger fetch every time inputValue changes
       if (inputValue) {
         debouncedFetchPage(inputValue, 0);
@@ -668,14 +658,7 @@ const AsyncSelect = (
         fetchPage('', 0);
       }
     }
-  }, [
-    isAsync,
-    loadingEnabled,
-    fetchPage,
-    allowFetch,
-    inputValue,
-    debouncedFetchPage,
-  ]);
+  }, [loadingEnabled, fetchPage, allowFetch, inputValue, debouncedFetchPage]);
 
   useEffect(() => {
     if (loading !== undefined && loading !== isLoading) {
@@ -706,20 +689,20 @@ const AsyncSelect = (
         getPopupContainer={
           getPopupContainer || (triggerNode => triggerNode.parentNode)
         }
-        labelInValue={isAsync || labelInValue}
+        labelInValue
         maxTagCount={MAX_TAG_COUNT}
         mode={mappedMode}
         notFoundContent={isLoading ? t('Loading...') : notFoundContent}
         onDeselect={handleOnDeselect}
         onDropdownVisibleChange={handleOnDropdownVisibleChange}
-        onPopupScroll={isAsync ? handlePagination : undefined}
-        onSearch={shouldShowSearch ? handleOnSearch : undefined}
+        onPopupScroll={handlePagination}
+        onSearch={showSearch ? handleOnSearch : undefined}
         onSelect={handleOnSelect}
         onClear={handleClear}
         onChange={onChange}
         options={hasCustomLabels ? undefined : fullSelectOptions}
         placeholder={placeholder}
-        showSearch={shouldShowSearch}
+        showSearch={showSearch}
         showArrow
         tokenSeparators={tokenSeparators || TOKEN_SEPARATORS}
         value={selectValue}
diff --git a/superset-frontend/src/components/Select/Select.stories.tsx 
b/superset-frontend/src/components/Select/Select.stories.tsx
index efcd91c0c3..b75e1ff28b 100644
--- a/superset-frontend/src/components/Select/Select.stories.tsx
+++ b/superset-frontend/src/components/Select/Select.stories.tsx
@@ -16,11 +16,22 @@
  * specific language governing permissions and limitations
  * 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 ControlHeader from 'src/explore/components/ControlHeader';
-import AsyncSelect, { AsyncSelectProps, AsyncSelectRef } from './AsyncSelect';
-import Select, { SelectProps, OptionsTypePage, OptionsType } from './Select';
+import AsyncSelect, {
+  AsyncSelectProps,
+  AsyncSelectRef,
+  OptionsTypePage,
+} from './AsyncSelect';
+
+import Select, { SelectProps, OptionsType } from './Select';
 
 export default {
   title: 'Select',
@@ -452,6 +463,11 @@ export const AsynchronousSelect = ({
       reject(new Error('Error while fetching the names from the server'));
     });
 
+  const initialValue = useMemo(
+    () => ({ label: 'Valentina', value: 'Valentina' }),
+    [],
+  );
+
   return (
     <>
       <div
@@ -465,11 +481,7 @@ export const AsynchronousSelect = ({
           fetchOnlyOnSearch={fetchOnlyOnSearch}
           options={withError ? fetchUserListError : fetchUserListPage}
           placeholder={fetchOnlyOnSearch ? 'Type anything' : 'AsyncSelect...'}
-          value={
-            withInitialValue
-              ? { label: 'Valentina', value: 'Valentina' }
-              : undefined
-          }
+          value={withInitialValue ? initialValue : undefined}
         />
       </div>
       <div
diff --git a/superset-frontend/src/components/Select/Select.test.tsx 
b/superset-frontend/src/components/Select/Select.test.tsx
index 0b353bde38..18111f30ca 100644
--- a/superset-frontend/src/components/Select/Select.test.tsx
+++ b/superset-frontend/src/components/Select/Select.test.tsx
@@ -114,17 +114,24 @@ test('displays a header', async () => {
   expect(screen.getByText(headerText)).toBeInTheDocument();
 });
 
-test('adds a new option if the value is not in the options', async () => {
-  const { rerender } = render(
-    <Select {...defaultProps} options={[]} value={OPTIONS[0]} />,
-  );
+test('adds a new option if the value is not in the options, when options are 
empty', async () => {
+  render(<Select {...defaultProps} options={[]} value={OPTIONS[0]} />);
   await open();
   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]} />,
   );
   await open();
+  expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument();
+  expect(await findSelectOption(OPTIONS[1].label)).toBeInTheDocument();
   const options = await findAllSelectOptions();
   expect(options).toHaveLength(2);
   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 () => {
   render(<Select {...defaultProps} invertSelection />);
   await open();
diff --git a/superset-frontend/src/components/Select/Select.tsx 
b/superset-frontend/src/components/Select/Select.tsx
index 9cea5a726d..04eccec83a 100644
--- a/superset-frontend/src/components/Select/Select.tsx
+++ b/superset-frontend/src/components/Select/Select.tsx
@@ -21,13 +21,10 @@ import React, {
   ReactElement,
   ReactNode,
   RefObject,
-  UIEvent,
   useEffect,
   useMemo,
   useState,
-  useRef,
   useCallback,
-  useImperativeHandle,
 } from 'react';
 import { ensureIsArray, styled, t } from '@superset-ui/core';
 import AntdSelect, {
@@ -37,11 +34,8 @@ import AntdSelect, {
 } from 'antd/lib/select';
 import { DownOutlined, SearchOutlined } from '@ant-design/icons';
 import { Spin } from 'antd';
-import debounce from 'lodash/debounce';
 import { isEqual } from 'lodash';
 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 { getValue, hasOption, isLabeledValue } from './utils';
 
@@ -72,19 +66,6 @@ type PickedSelectProps = Pick<
 
 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 {
   /**
    * It enables the user to create new options.
@@ -103,13 +84,6 @@ export interface SelectProps extends PickedSelectProps {
    * Can be any 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
    * 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
    * an array of options.
    */
-  options: OptionsType | OptionsPagePromise;
-  /**
-   * It defines how many results should be included
-   * in the query response.
-   * Works in async mode only (See the options property).
-   */
-  pageSize?: number;
+  options: OptionsType;
   /**
    * It shows a stop-outlined icon at the far right of a selected
    * option instead of the default checkmark.
@@ -148,19 +116,6 @@ export interface SelectProps extends PickedSelectProps {
    * False by default.
    */
   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.
    * Will not apply to predefined `options` array when users are not searching.
@@ -195,25 +150,6 @@ const StyledCheckOutlined = styled(Icons.CheckOutlined)`
   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)`
   margin-top: ${({ theme }) => -theme.gridUnit}px;
 `;
@@ -228,15 +164,8 @@ const StyledLoadingText = styled.div`
 
 const MAX_TAG_COUNT = 4;
 const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
-const DEFAULT_PAGE_SIZE = 100;
 const EMPTY_OPTIONS: OptionsType = [];
 
-const Error = ({ error }: { error: string }) => (
-  <StyledError>
-    <Icons.ErrorSolid /> <StyledErrorMessage>{error}</StyledErrorMessage>
-  </StyledError>
-);
-
 export const DEFAULT_SORT_COMPARATOR = (
   a: AntdLabeledValue,
   b: AntdLabeledValue,
@@ -273,9 +202,6 @@ export const propertyComparator =
     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
  * https://ant.design/components/select/.
@@ -295,23 +221,19 @@ const Select = (
     allowClear,
     allowNewOptions = false,
     ariaLabel,
-    fetchOnlyOnSearch,
     filterOption = true,
     header = null,
     invertSelection = false,
     labelInValue = false,
-    lazyLoading = true,
     loading,
     mode = 'single',
     name,
     notFoundContent,
-    onError,
     onChange,
     onClear,
     onDropdownVisibleChange,
     optionFilterProps = ['label', 'value'],
     options,
-    pageSize = DEFAULT_PAGE_SIZE,
     placeholder = t('Select ...'),
     showSearch = true,
     sortComparator = DEFAULT_SORT_COMPARATOR,
@@ -320,27 +242,19 @@ const Select = (
     getPopupContainer,
     ...props
   }: SelectProps,
-  ref: RefObject<SelectRef>,
+  ref: RefObject<HTMLInputElement>,
 ) => {
-  const isAsync = typeof options === 'function';
   const isSingleMode = mode === 'single';
-  const shouldShowSearch = isAsync || allowNewOptions ? true : showSearch;
+  const shouldShowSearch = allowNewOptions ? true : showSearch;
   const [selectValue, setSelectValue] = useState(value);
   const [inputValue, setInputValue] = useState('');
   const [isLoading, setIsLoading] = useState(loading);
-  const [error, setError] = useState('');
   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
     ? undefined
     : allowNewOptions
     ? 'tags'
     : 'multiple';
-  const allowFetch = !fetchOnlyOnSearch || inputValue;
 
   const sortSelectedFirst = useCallback(
     (a: AntdLabeledValue, b: AntdLabeledValue) =>
@@ -355,22 +269,14 @@ const Select = (
       sortSelectedFirst(a, b) || sortComparator(a, b, inputValue),
     [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(
     () => (options && Array.isArray(options) ? options.slice() : 
EMPTY_OPTIONS),
     [options],
   );
   const initialOptionsSorted = useMemo(
-    () => initialOptions.slice().sort(sortComparatorForNoSearch),
-    [initialOptions, sortComparatorForNoSearch],
+    () => initialOptions.slice().sort(sortSelectedFirst),
+    [initialOptions, sortSelectedFirst],
   );
 
   const [selectOptions, setSelectOptions] =
@@ -427,89 +333,6 @@ const Select = (
     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 searchValue = search.trim();
     if (allowNewOptions && isSingleMode) {
@@ -527,31 +350,9 @@ const Select = (
         : cleanSelectOptions;
       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);
   };
 
-  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) => {
     if (typeof filterOption === 'function') {
       return filterOption(search, option);
@@ -575,35 +376,13 @@ const Select = (
   const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => {
     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
     // `filterSort`.
     if (isDropdownVisible && !inputValue && selectOptions.length > 1) {
-      const sortedOptions = isAsync
-        ? selectOptions.slice().sort(sortComparatorForNoSearch)
-        : // 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 (!isEqual(initialOptionsSorted, selectOptions)) {
+        setSelectOptions(initialOptionsSorted);
       }
     }
-
     if (onDropdownVisibleChange) {
       onDropdownVisibleChange(isDropdownVisible);
     }
@@ -618,7 +397,7 @@ const Select = (
     if (isLoading && fullSelectOptions.length === 0) {
       return <StyledLoadingText>{t('Loading...')}</StyledLoadingText>;
     }
-    return error ? <Error error={error} /> : originNode;
+    return originNode;
   };
 
   // use a function instead of component since every rerender of the
@@ -642,8 +421,6 @@ const Select = (
 
   useEffect(() => {
     // when `options` list is updated from component prop, reset states
-    fetchedQueries.current.clear();
-    setAllValuesLoaded(false);
     setSelectOptions(initialOptions);
   }, [initialOptions]);
 
@@ -651,49 +428,12 @@ const Select = (
     setSelectValue(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(() => {
     if (loading !== undefined && loading !== isLoading) {
       setIsLoading(loading);
     }
   }, [isLoading, loading]);
 
-  const clearCache = () => fetchedQueries.current.clear();
-
-  useImperativeHandle(
-    ref,
-    () => ({
-      ...(ref.current as HTMLInputElement),
-      clearCache,
-    }),
-    [ref],
-  );
-
   return (
     <StyledContainer>
       {header}
@@ -706,13 +446,13 @@ const Select = (
         getPopupContainer={
           getPopupContainer || (triggerNode => triggerNode.parentNode)
         }
-        labelInValue={isAsync || labelInValue}
+        labelInValue={labelInValue}
         maxTagCount={MAX_TAG_COUNT}
         mode={mappedMode}
         notFoundContent={isLoading ? t('Loading...') : notFoundContent}
         onDeselect={handleOnDeselect}
         onDropdownVisibleChange={handleOnDropdownVisibleChange}
-        onPopupScroll={isAsync ? handlePagination : undefined}
+        onPopupScroll={undefined}
         onSearch={shouldShowSearch ? handleOnSearch : undefined}
         onSelect={handleOnSelect}
         onClear={handleClear}
diff --git a/superset-frontend/src/components/TableSelector/index.tsx 
b/superset-frontend/src/components/TableSelector/index.tsx
index d7b867347a..4c07e256cc 100644
--- a/superset-frontend/src/components/TableSelector/index.tsx
+++ b/superset-frontend/src/components/TableSelector/index.tsx
@@ -336,7 +336,6 @@ const TableSelector: FunctionComponent<TableSelectorProps> 
= ({
         filterOption={handleFilterOption}
         header={header}
         labelInValue
-        lazyLoading={false}
         loading={loadingTables}
         name="select-table"
         onChange={(options: TableOption | TableOption[]) =>
diff --git a/superset-frontend/src/filters/components/GroupBy/types.ts 
b/superset-frontend/src/filters/components/GroupBy/types.ts
index 1e6d756497..5775edaa2f 100644
--- a/superset-frontend/src/filters/components/GroupBy/types.ts
+++ b/superset-frontend/src/filters/components/GroupBy/types.ts
@@ -23,7 +23,6 @@ import {
   QueryFormData,
 } from '@superset-ui/core';
 import { RefObject } from 'react';
-import { SelectRef } from 'src/components/Select/Select';
 import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
 
 interface PluginFilterGroupByCustomizeProps {
@@ -41,7 +40,7 @@ export type PluginFilterGroupByProps = 
PluginFilterStylesProps & {
   data: DataRecord[];
   filterState: FilterState;
   formData: PluginFilterGroupByQueryFormData;
-  inputRef: RefObject<SelectRef>;
+  inputRef: RefObject<HTMLInputElement>;
 } & PluginFilterHooks;
 
 export const DEFAULT_FORM_DATA: PluginFilterGroupByCustomizeProps = {
diff --git a/superset-frontend/src/filters/components/TimeColumn/types.ts 
b/superset-frontend/src/filters/components/TimeColumn/types.ts
index bc12cb8716..95b1e5edfd 100644
--- a/superset-frontend/src/filters/components/TimeColumn/types.ts
+++ b/superset-frontend/src/filters/components/TimeColumn/types.ts
@@ -23,7 +23,6 @@ import {
   QueryFormData,
 } from '@superset-ui/core';
 import { RefObject } from 'react';
-import { SelectRef } from 'src/components/Select/Select';
 import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
 
 interface PluginFilterTimeColumnCustomizeProps {
@@ -40,7 +39,7 @@ export type PluginFilterTimeColumnProps = 
PluginFilterStylesProps & {
   data: DataRecord[];
   filterState: FilterState;
   formData: PluginFilterTimeColumnQueryFormData;
-  inputRef: RefObject<SelectRef>;
+  inputRef: RefObject<HTMLInputElement>;
 } & PluginFilterHooks;
 
 export const DEFAULT_FORM_DATA: PluginFilterTimeColumnCustomizeProps = {
diff --git a/superset-frontend/src/filters/components/TimeGrain/types.ts 
b/superset-frontend/src/filters/components/TimeGrain/types.ts
index 64a7166573..3dfa9a99d8 100644
--- a/superset-frontend/src/filters/components/TimeGrain/types.ts
+++ b/superset-frontend/src/filters/components/TimeGrain/types.ts
@@ -18,7 +18,6 @@
  */
 import { FilterState, QueryFormData, DataRecord } from '@superset-ui/core';
 import { RefObject } from 'react';
-import { SelectRef } from 'src/components/Select/Select';
 import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
 
 interface PluginFilterTimeGrainCustomizeProps {
@@ -34,7 +33,7 @@ export type PluginFilterTimeGrainProps = 
PluginFilterStylesProps & {
   data: DataRecord[];
   filterState: FilterState;
   formData: PluginFilterTimeGrainQueryFormData;
-  inputRef: RefObject<SelectRef>;
+  inputRef: RefObject<HTMLInputElement>;
 } & PluginFilterHooks;
 
 export const DEFAULT_FORM_DATA: PluginFilterTimeGrainCustomizeProps = {

Reply via email to