diff --git a/superset-frontend/src/components/Select/AsyncSelect.stories.tsx b/superset-frontend/src/components/Select/AsyncSelect.stories.tsx
new file mode 100644
index 0000000000..547fc7fa99
--- /dev/null
+++ b/superset-frontend/src/components/Select/AsyncSelect.stories.tsx
@@ -0,0 +1,359 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React, {
+ ReactNode,
+ useState,
+ useCallback,
+ useRef,
+ useMemo,
+} from 'react';
+import Button from 'src/components/Button';
+import AsyncSelect from './AsyncSelect';
+import {
+ SelectOptionsType,
+ AsyncSelectProps,
+ AsyncSelectRef,
+ SelectOptionsTypePage,
+} from './types';
+
+export default {
+ title: 'AsyncSelect',
+ component: AsyncSelect,
+};
+
+const DEFAULT_WIDTH = 200;
+
+const options: SelectOptionsType = [
+ {
+ label: 'Such an incredibly awesome long long label',
+ value: 'Such an incredibly awesome long long label',
+ custom: 'Secret custom prop',
+ },
+ {
+ label: 'Another incredibly awesome long long label',
+ value: 'Another incredibly awesome long long label',
+ },
+ {
+ label: 'JSX Label',
+ customLabel:
JSX Label
,
+ value: 'JSX Label',
+ },
+ { label: 'A', value: 'A' },
+ { label: 'B', value: 'B' },
+ { label: 'C', value: 'C' },
+ { label: 'D', value: 'D' },
+ { label: 'E', value: 'E' },
+ { label: 'F', value: 'F' },
+ { label: 'G', value: 'G' },
+ { label: 'H', value: 'H' },
+ { label: 'I', value: 'I' },
+];
+
+const ARG_TYPES = {
+ options: {
+ defaultValue: options,
+ description: `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 an array of options.
+ `,
+ },
+ ariaLabel: {
+ description: `It adds the aria-label tag for accessibility standards.
+ Must be plain English and localized.
+ `,
+ },
+ labelInValue: {
+ defaultValue: true,
+ table: {
+ disable: true,
+ },
+ },
+ name: {
+ table: {
+ disable: true,
+ },
+ },
+ notFoundContent: {
+ table: {
+ disable: true,
+ },
+ },
+ mode: {
+ description: `It defines whether the Select should allow for
+ the selection of multiple options or single. Single by default.
+ `,
+ defaultValue: 'single',
+ control: {
+ type: 'inline-radio',
+ options: ['single', 'multiple'],
+ },
+ },
+ allowNewOptions: {
+ description: `It enables the user to create new options.
+ Can be used with standard or async select types.
+ Can be used with any mode, single or multiple. False by default.
+ `,
+ },
+ invertSelection: {
+ description: `It shows a stop-outlined icon at the far right of a selected
+ option instead of the default checkmark.
+ Useful to better indicate to the user that by clicking on a selected
+ option it will be de-selected. False by default.
+ `,
+ },
+ optionFilterProps: {
+ description: `It allows to define which properties of the option object
+ should be looked for when searching.
+ By default label and value.
+ `,
+ },
+};
+
+const USERS = [
+ 'John',
+ 'Liam',
+ 'Olivia',
+ 'Emma',
+ 'Noah',
+ 'Ava',
+ 'Oliver',
+ 'Elijah',
+ 'Charlotte',
+ 'Diego',
+ 'Evan',
+ 'Michael',
+ 'Giovanni',
+ 'Luca',
+ 'Paolo',
+ 'Francesca',
+ 'Chiara',
+ 'Sara',
+ 'Valentina',
+ 'Jessica',
+ 'Angelica',
+ 'Mario',
+ 'Marco',
+ 'Andrea',
+ 'Luigi',
+ 'Quarto',
+ 'Quinto',
+ 'Sesto',
+ 'Franco',
+ 'Sandro',
+ 'Alehandro',
+ 'Johnny',
+ 'Nikole',
+ 'Igor',
+ 'Sipatha',
+ 'Thami',
+ 'Munei',
+ 'Guilherme',
+ 'Umair',
+ 'Ashfaq',
+ 'Amna',
+ 'Irfan',
+ 'George',
+ 'Naseer',
+ 'Mohammad',
+ 'Rick',
+ 'Saliya',
+ 'Claire',
+ 'Benedetta',
+ 'Ilenia',
+].sort();
+
+export const AsynchronousSelect = ({
+ fetchOnlyOnSearch,
+ withError,
+ withInitialValue,
+ responseTime,
+ ...rest
+}: AsyncSelectProps & {
+ withError: boolean;
+ withInitialValue: boolean;
+ responseTime: number;
+}) => {
+ const [requests, setRequests] = useState([]);
+ const ref = useRef(null);
+
+ const getResults = (username?: string) => {
+ let results: { label: string; value: string }[] = [];
+
+ if (!username) {
+ results = USERS.map(u => ({
+ label: u,
+ value: u,
+ }));
+ } else {
+ const foundUsers = USERS.filter(u => u.toLowerCase().includes(username));
+ if (foundUsers) {
+ results = foundUsers.map(u => ({ label: u, value: u }));
+ } else {
+ results = [];
+ }
+ }
+ return results;
+ };
+
+ const setRequestLog = (results: number, total: number, username?: string) => {
+ const request = (
+ <>
+ Emulating network request with search {username || 'empty'} ...{' '}
+
+ {results}/{total}
+ {' '}
+ results
+ >
+ );
+
+ setRequests(requests => [request, ...requests]);
+ };
+
+ const fetchUserListPage = useCallback(
+ (
+ search: string,
+ page: number,
+ pageSize: number,
+ ): Promise => {
+ const username = search.trim().toLowerCase();
+ return new Promise(resolve => {
+ let results = getResults(username);
+ const totalCount = results.length;
+ const start = page * pageSize;
+ const deleteCount =
+ start + pageSize < totalCount ? pageSize : totalCount - start;
+ results = results.splice(start, deleteCount);
+ setRequestLog(start + results.length, totalCount, username);
+ setTimeout(() => {
+ resolve({ data: results, totalCount });
+ }, responseTime * 1000);
+ });
+ },
+ [responseTime],
+ );
+
+ const fetchUserListError = async (): Promise =>
+ new Promise((_, reject) => {
+ reject(new Error('Error while fetching the names from the server'));
+ });
+
+ const initialValue = useMemo(
+ () => ({ label: 'Valentina', value: 'Valentina' }),
+ [],
+ );
+
+ return (
+ <>
+
+
+ {requests.map((request, index) => (
+
{request}
+ ))}
+
+ {
+ ref.current?.clearCache();
+ setRequests([]);
+ }}
+ >
+ Clear cache
+
+ >
+ );
+};
+
+AsynchronousSelect.args = {
+ allowClear: false,
+ allowNewOptions: false,
+ fetchOnlyOnSearch: false,
+ pageSize: 10,
+ withError: false,
+ withInitialValue: false,
+ tokenSeparators: ['\n', '\t', ';'],
+};
+
+AsynchronousSelect.argTypes = {
+ ...ARG_TYPES,
+ header: {
+ table: {
+ disable: true,
+ },
+ },
+ invertSelection: {
+ table: {
+ disable: true,
+ },
+ },
+ pageSize: {
+ defaultValue: 10,
+ control: {
+ type: 'range',
+ min: 10,
+ max: 50,
+ step: 10,
+ },
+ },
+ responseTime: {
+ defaultValue: 0.5,
+ name: 'responseTime (seconds)',
+ control: {
+ type: 'range',
+ min: 0.5,
+ max: 5,
+ step: 0.5,
+ },
+ },
+};
+
+AsynchronousSelect.story = {
+ parameters: {
+ knobs: {
+ disable: true,
+ },
+ },
+};
diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx b/superset-frontend/src/components/Select/AsyncSelect.tsx
index 27c62023ce..524ca53e61 100644
--- a/superset-frontend/src/components/Select/AsyncSelect.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.tsx
@@ -28,7 +28,7 @@ import React, {
useCallback,
useImperativeHandle,
} from 'react';
-import { ensureIsArray, styled, t } from '@superset-ui/core';
+import { ensureIsArray, t } from '@superset-ui/core';
import { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
import debounce from 'lodash/debounce';
import { isEqual } from 'lodash';
@@ -39,20 +39,8 @@ import {
getValue,
hasOption,
isLabeledValue,
- DEFAULT_SORT_COMPARATOR,
- EMPTY_OPTIONS,
- MAX_TAG_COUNT,
- SelectOptionsPagePromise,
- SelectOptionsType,
- SelectOptionsTypePage,
- StyledCheckOutlined,
- StyledStopOutlined,
- TOKEN_SEPARATORS,
renderSelectOptions,
- StyledContainer,
- StyledSelect,
hasCustomLabels,
- BaseSelectProps,
sortSelectedFirstHelper,
sortComparatorWithSearchHelper,
sortComparatorForNoSearchHelper,
@@ -60,64 +48,28 @@ import {
dropDownRenderHelper,
handleFilterOptionHelper,
} from './utils';
-
-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 DEFAULT_PAGE_SIZE = 100;
-
-export type AsyncSelectRef = HTMLInputElement & { clearCache: () => void };
-
-export interface AsyncSelectProps extends BaseSelectProps {
- /**
- * 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 the options of the Select.
- * The options are async, a promise that returns
- * an array of options.
- */
- options: SelectOptionsPagePromise;
- /**
- * It defines how many results should be included
- * in the query response.
- * Works in async mode only (See the options property).
- */
- pageSize?: number;
- /**
- * 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;
-}
+import {
+ AsyncSelectProps,
+ AsyncSelectRef,
+ SelectOptionsPagePromise,
+ SelectOptionsType,
+ SelectOptionsTypePage,
+} from './types';
+import {
+ StyledCheckOutlined,
+ StyledContainer,
+ StyledError,
+ StyledErrorMessage,
+ StyledSelect,
+ StyledStopOutlined,
+} from './styles';
+import {
+ DEFAULT_PAGE_SIZE,
+ EMPTY_OPTIONS,
+ MAX_TAG_COUNT,
+ TOKEN_SEPARATORS,
+ DEFAULT_SORT_COMPARATOR,
+} from './constants';
const Error = ({ error }: { error: string }) => (
diff --git a/superset-frontend/src/components/Select/Select.stories.tsx b/superset-frontend/src/components/Select/Select.stories.tsx
index e9a03fe563..c4802a45e0 100644
--- a/superset-frontend/src/components/Select/Select.stories.tsx
+++ b/superset-frontend/src/components/Select/Select.stories.tsx
@@ -16,19 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, {
- ReactNode,
- useState,
- useCallback,
- useRef,
- useMemo,
-} from 'react';
-import Button from 'src/components/Button';
+import React from 'react';
import ControlHeader from 'src/explore/components/ControlHeader';
-import AsyncSelect, { AsyncSelectProps, AsyncSelectRef } from './AsyncSelect';
-import { SelectOptionsType, SelectOptionsTypePage } from './utils';
+import { SelectOptionsType, SelectProps } from './types';
-import Select, { SelectProps } from './Select';
+import Select from './Select';
export default {
title: 'Select',
@@ -331,236 +323,3 @@ PageScroll.story = {
},
},
};
-
-const USERS = [
- 'John',
- 'Liam',
- 'Olivia',
- 'Emma',
- 'Noah',
- 'Ava',
- 'Oliver',
- 'Elijah',
- 'Charlotte',
- 'Diego',
- 'Evan',
- 'Michael',
- 'Giovanni',
- 'Luca',
- 'Paolo',
- 'Francesca',
- 'Chiara',
- 'Sara',
- 'Valentina',
- 'Jessica',
- 'Angelica',
- 'Mario',
- 'Marco',
- 'Andrea',
- 'Luigi',
- 'Quarto',
- 'Quinto',
- 'Sesto',
- 'Franco',
- 'Sandro',
- 'Alehandro',
- 'Johnny',
- 'Nikole',
- 'Igor',
- 'Sipatha',
- 'Thami',
- 'Munei',
- 'Guilherme',
- 'Umair',
- 'Ashfaq',
- 'Amna',
- 'Irfan',
- 'George',
- 'Naseer',
- 'Mohammad',
- 'Rick',
- 'Saliya',
- 'Claire',
- 'Benedetta',
- 'Ilenia',
-].sort();
-
-export const AsynchronousSelect = ({
- fetchOnlyOnSearch,
- withError,
- withInitialValue,
- responseTime,
- ...rest
-}: AsyncSelectProps & {
- withError: boolean;
- withInitialValue: boolean;
- responseTime: number;
-}) => {
- const [requests, setRequests] = useState([]);
- const ref = useRef(null);
-
- const getResults = (username?: string) => {
- let results: { label: string; value: string }[] = [];
-
- if (!username) {
- results = USERS.map(u => ({
- label: u,
- value: u,
- }));
- } else {
- const foundUsers = USERS.filter(u => u.toLowerCase().includes(username));
- if (foundUsers) {
- results = foundUsers.map(u => ({ label: u, value: u }));
- } else {
- results = [];
- }
- }
- return results;
- };
-
- const setRequestLog = (results: number, total: number, username?: string) => {
- const request = (
- <>
- Emulating network request with search {username || 'empty'} ...{' '}
-
- {results}/{total}
- {' '}
- results
- >
- );
-
- setRequests(requests => [request, ...requests]);
- };
-
- const fetchUserListPage = useCallback(
- (
- search: string,
- page: number,
- pageSize: number,
- ): Promise => {
- const username = search.trim().toLowerCase();
- return new Promise(resolve => {
- let results = getResults(username);
- const totalCount = results.length;
- const start = page * pageSize;
- const deleteCount =
- start + pageSize < totalCount ? pageSize : totalCount - start;
- results = results.splice(start, deleteCount);
- setRequestLog(start + results.length, totalCount, username);
- setTimeout(() => {
- resolve({ data: results, totalCount });
- }, responseTime * 1000);
- });
- },
- [responseTime],
- );
-
- const fetchUserListError = async (): Promise =>
- new Promise((_, reject) => {
- reject(new Error('Error while fetching the names from the server'));
- });
-
- const initialValue = useMemo(
- () => ({ label: 'Valentina', value: 'Valentina' }),
- [],
- );
-
- return (
- <>
-
-
- {requests.map((request, index) => (
-
{request}
- ))}
-
- {
- ref.current?.clearCache();
- setRequests([]);
- }}
- >
- Clear cache
-
- >
- );
-};
-
-AsynchronousSelect.args = {
- allowClear: false,
- allowNewOptions: false,
- fetchOnlyOnSearch: false,
- pageSize: 10,
- withError: false,
- withInitialValue: false,
- tokenSeparators: ['\n', '\t', ';'],
-};
-
-AsynchronousSelect.argTypes = {
- ...ARG_TYPES,
- header: {
- table: {
- disable: true,
- },
- },
- invertSelection: {
- table: {
- disable: true,
- },
- },
- pageSize: {
- defaultValue: 10,
- control: {
- type: 'range',
- min: 10,
- max: 50,
- step: 10,
- },
- },
- responseTime: {
- defaultValue: 0.5,
- name: 'responseTime (seconds)',
- control: {
- type: 'range',
- min: 0.5,
- max: 5,
- step: 0.5,
- },
- },
-};
-
-AsynchronousSelect.story = {
- parameters: {
- knobs: {
- disable: true,
- },
- },
-};
diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx
index 6b6713852d..1ad1e6b0a2 100644
--- a/superset-frontend/src/components/Select/Select.tsx
+++ b/superset-frontend/src/components/Select/Select.tsx
@@ -32,34 +32,27 @@ import {
getValue,
hasOption,
isLabeledValue,
- DEFAULT_SORT_COMPARATOR,
- EMPTY_OPTIONS,
- MAX_TAG_COUNT,
- SelectOptionsType,
- StyledCheckOutlined,
- StyledStopOutlined,
- TOKEN_SEPARATORS,
renderSelectOptions,
- StyledSelect,
- StyledContainer,
hasCustomLabels,
- BaseSelectProps,
sortSelectedFirstHelper,
sortComparatorWithSearchHelper,
handleFilterOptionHelper,
dropDownRenderHelper,
getSuffixIcon,
} from './utils';
-
-export interface SelectProps extends BaseSelectProps {
- /**
- * 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
- * an array of options.
- */
- options: SelectOptionsType;
-}
+import { SelectOptionsType, SelectProps } from './types';
+import {
+ StyledCheckOutlined,
+ StyledContainer,
+ StyledSelect,
+ StyledStopOutlined,
+} from './styles';
+import {
+ EMPTY_OPTIONS,
+ MAX_TAG_COUNT,
+ TOKEN_SEPARATORS,
+ DEFAULT_SORT_COMPARATOR,
+} from './constants';
/**
* This component is a customized version of the Antdesign 4.X Select component
diff --git a/superset-frontend/src/components/Select/constants.ts b/superset-frontend/src/components/Select/constants.ts
new file mode 100644
index 0000000000..b8c60e8523
--- /dev/null
+++ b/superset-frontend/src/components/Select/constants.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
+import { rankedSearchCompare } from 'src/utils/rankedSearchCompare';
+
+export const MAX_TAG_COUNT = 4;
+
+export const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
+
+export const EMPTY_OPTIONS = [];
+
+export const DEFAULT_PAGE_SIZE = 100;
+
+export const DEFAULT_SORT_COMPARATOR = (
+ a: AntdLabeledValue,
+ b: AntdLabeledValue,
+ search?: string,
+) => {
+ let aText: string | undefined;
+ let bText: string | undefined;
+ if (typeof a.label === 'string' && typeof b.label === 'string') {
+ aText = a.label;
+ bText = b.label;
+ } else if (typeof a.value === 'string' && typeof b.value === 'string') {
+ aText = a.value;
+ bText = b.value;
+ }
+ // sort selected options first
+ if (typeof aText === 'string' && typeof bText === 'string') {
+ if (search) {
+ return rankedSearchCompare(aText, bText, search);
+ }
+ return aText.localeCompare(bText);
+ }
+ return (a.value as number) - (b.value as number);
+};
diff --git a/superset-frontend/src/components/Select/styles.tsx b/superset-frontend/src/components/Select/styles.tsx
new file mode 100644
index 0000000000..85dbefe88f
--- /dev/null
+++ b/superset-frontend/src/components/Select/styles.tsx
@@ -0,0 +1,90 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { styled } from '@superset-ui/core';
+import Icons from 'src/components/Icons';
+import { Spin } from 'antd';
+import AntdSelect from 'antd/lib/select';
+
+export const StyledContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+`;
+
+export const StyledSelect = styled(AntdSelect)`
+ ${({ theme }) => `
+ && .ant-select-selector {
+ border-radius: ${theme.gridUnit}px;
+ }
+ // Open the dropdown when clicking on the suffix
+ // This is fixed in version 4.16
+ .ant-select-arrow .anticon:not(.ant-select-suffix) {
+ pointer-events: none;
+ }
+ `}
+`;
+
+export const StyledStopOutlined = styled(Icons.StopOutlined)`
+ vertical-align: 0;
+`;
+
+export const StyledCheckOutlined = styled(Icons.CheckOutlined)`
+ vertical-align: 0;
+`;
+
+export const StyledSpin = styled(Spin)`
+ margin-top: ${({ theme }) => -theme.gridUnit}px;
+`;
+
+export const StyledLoadingText = styled.div`
+ ${({ theme }) => `
+ margin-left: ${theme.gridUnit * 3}px;
+ line-height: ${theme.gridUnit * 8}px;
+ color: ${theme.colors.grayscale.light1};
+ `}
+`;
+
+export const StyledHelperText = styled.div`
+ ${({ theme }) => `
+ padding: ${theme.gridUnit * 2}px ${theme.gridUnit * 3}px;
+ color: ${theme.colors.grayscale.base};
+ font-size: ${theme.typography.sizes.s}px;
+ cursor: default;
+ border-bottom: 1px solid ${theme.colors.grayscale.light2};
+ `}
+`;
+
+export 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;
+ }
+ `}
+`;
+
+export const StyledErrorMessage = styled.div`
+ overflow: hidden;
+ text-overflow: ellipsis;
+`;
diff --git a/superset-frontend/src/components/Select/types.ts b/superset-frontend/src/components/Select/types.ts
new file mode 100644
index 0000000000..e2a7d5d1f3
--- /dev/null
+++ b/superset-frontend/src/components/Select/types.ts
@@ -0,0 +1,201 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {
+ JSXElementConstructor,
+ ReactElement,
+ ReactNode,
+ RefObject,
+} from 'react';
+import {
+ SelectProps as AntdSelectProps,
+ SelectValue as AntdSelectValue,
+ LabeledValue as AntdLabeledValue,
+} from 'antd/lib/select';
+
+export type RawValue = string | number;
+
+export type V = string | number | null | undefined;
+
+export type LabeledValue = { label?: ReactNode; value?: V };
+
+export type AntdProps = AntdSelectProps;
+
+export type AntdExposedProps = Pick<
+ AntdProps,
+ | 'allowClear'
+ | 'autoFocus'
+ | 'disabled'
+ | 'filterOption'
+ | 'filterSort'
+ | 'loading'
+ | 'labelInValue'
+ | 'maxTagCount'
+ | 'notFoundContent'
+ | 'onChange'
+ | 'onClear'
+ | 'onDeselect'
+ | 'onSelect'
+ | 'onFocus'
+ | 'onBlur'
+ | 'onPopupScroll'
+ | 'onSearch'
+ | 'onDropdownVisibleChange'
+ | 'placeholder'
+ | 'showArrow'
+ | 'showSearch'
+ | 'tokenSeparators'
+ | 'value'
+ | 'getPopupContainer'
+ | 'menuItemSelectedIcon'
+>;
+
+export type SelectOptionsType = Exclude;
+
+export interface BaseSelectProps extends AntdExposedProps {
+ /**
+ * It enables the user to create new options.
+ * Can be used with standard or async select types.
+ * Can be used with any mode, single or multiple.
+ * False by default.
+ * */
+ allowNewOptions?: boolean;
+ /**
+ * It adds the aria-label tag for accessibility standards.
+ * Must be plain English and localized.
+ */
+ ariaLabel?: string;
+ /**
+ * Renders the dropdown
+ */
+ dropdownRender?: (
+ menu: ReactElement>,
+ ) => ReactElement>;
+ /**
+ * It adds a header on top of the Select.
+ * Can be any ReactNode.
+ */
+ header?: ReactNode;
+ /**
+ * It adds a helper text on top of the Select options
+ * with additional context to help with the interaction.
+ */
+ helperText?: string;
+ /**
+ * It allows to define which properties of the option object
+ * should be looked for when searching.
+ * By default label and value.
+ */
+ mappedMode?: 'multiple' | 'tags';
+ /**
+ * It defines whether the Select should allow for the
+ * selection of multiple options or single.
+ * Single by default.
+ */
+ mode?: 'single' | 'multiple';
+ /**
+ * Deprecated.
+ * Prefer ariaLabel instead.
+ */
+ name?: string; // discourage usage
+ /**
+ * It allows to define which properties of the option object
+ * should be looked for when searching.
+ * By default label and value.
+ */
+ optionFilterProps?: string[];
+ /**
+ * It shows a stop-outlined icon at the far right of a selected
+ * option instead of the default checkmark.
+ * Useful to better indicate to the user that by clicking on a selected
+ * option it will be de-selected.
+ * False by default.
+ */
+ invertSelection?: boolean;
+ /**
+ * Customize how filtered options are sorted while users search.
+ * Will not apply to predefined `options` array when users are not searching.
+ */
+ sortComparator?: (
+ a: AntdLabeledValue,
+ b: AntdLabeledValue,
+ search?: string,
+ ) => number;
+
+ suffixIcon?: ReactNode;
+
+ ref: RefObject;
+}
+
+export interface SelectProps extends BaseSelectProps {
+ /**
+ * 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
+ * an array of options.
+ */
+ options: SelectOptionsType;
+}
+
+export type AsyncSelectRef = HTMLInputElement & { clearCache: () => void };
+
+export type SelectOptionsTypePage = {
+ data: SelectOptionsType;
+ totalCount: number;
+};
+
+export type SelectOptionsPagePromise = (
+ search: string,
+ page: number,
+ pageSize: number,
+) => Promise;
+
+export interface AsyncSelectProps extends BaseSelectProps {
+ /**
+ * 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 the options of the Select.
+ * The options are async, a promise that returns
+ * an array of options.
+ */
+ options: SelectOptionsPagePromise;
+ /**
+ * It defines how many results should be included
+ * in the query response.
+ * Works in async mode only (See the options property).
+ */
+ pageSize?: number;
+ /**
+ * 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;
+}
diff --git a/superset-frontend/src/components/Select/utils.tsx b/superset-frontend/src/components/Select/utils.tsx
index 27a97ac8bd..5ec7e33d10 100644
--- a/superset-frontend/src/components/Select/utils.tsx
+++ b/superset-frontend/src/components/Select/utils.tsx
@@ -16,30 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { ensureIsArray, styled, t } from '@superset-ui/core';
-import { Spin } from 'antd';
-import Icons from 'src/components/Icons';
-import AntdSelect, {
- SelectProps as AntdSelectProps,
- SelectValue as AntdSelectValue,
- LabeledValue as AntdLabeledValue,
-} from 'antd/lib/select';
-import { rankedSearchCompare } from 'src/utils/rankedSearchCompare';
-import {
- OptionTypeBase,
- ValueType,
- OptionsType,
- GroupedOptionsType,
-} from 'react-select';
-import React, {
- ReactElement,
- ReactNode,
- RefObject,
- JSXElementConstructor,
-} from 'react';
+import { ensureIsArray, t } from '@superset-ui/core';
+import AntdSelect, { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
+import React, { ReactElement, RefObject } from 'react';
import { DownOutlined, SearchOutlined } from '@ant-design/icons';
-
-declare type RawValue = string | number;
+import { StyledHelperText, StyledLoadingText, StyledSpin } from './styles';
+import { LabeledValue, RawValue, SelectOptionsType, V } from './types';
const { Option } = AntdSelect;
@@ -51,41 +33,6 @@ export function isObject(value: unknown): value is Record {
);
}
-/**
- * Find Option value that matches a possibly string value.
- *
- * Translate possible string values to `OptionType` objects, fallback to value
- * itself if cannot be found in the options list.
- *
- * Always returns an array.
- */
-export function findValue(
- value: ValueType | string,
- options: GroupedOptionsType | OptionsType = [],
- valueKey = 'value',
-): OptionType[] {
- if (value === null || value === undefined || value === '') {
- return [];
- }
- const isGroup = Array.isArray((options[0] || {}).options);
- const flatOptions = isGroup
- ? (options as GroupedOptionsType).flatMap(x => x.options || [])
- : (options as OptionsType);
-
- const find = (val: OptionType) => {
- const realVal = (value || {}).hasOwnProperty(valueKey)
- ? val[valueKey]
- : val;
- return (
- flatOptions.find(x => x === realVal || x[valueKey] === realVal) || val
- );
- };
-
- // If value is a single string, must return an Array so `cleanValue` won't be
- // empty: https://github.com/JedWatson/react-select/blob/32ad5c040bdd96cd1ca71010c2558842d684629c/packages/react-select/src/utils.js#L64
- return (Array.isArray(value) ? value : [value]).map(find);
-}
-
export function isLabeledValue(value: unknown): value is AntdLabeledValue {
return isObject(value) && 'value' in value && 'label' in value;
}
@@ -96,10 +43,6 @@ export function getValue(
return isLabeledValue(option) ? option.value : option;
}
-type V = string | number | null | undefined;
-
-type LabeledValue = { label?: ReactNode; value?: V };
-
export function hasOption(
value: V,
options?: V | LabeledValue | (V | LabeledValue)[],
@@ -121,127 +64,6 @@ export function hasOption(
);
}
-export type AntdProps = AntdSelectProps;
-
-export type AntdExposedProps = Pick<
- AntdProps,
- | 'allowClear'
- | 'autoFocus'
- | 'disabled'
- | 'filterOption'
- | 'filterSort'
- | 'loading'
- | 'labelInValue'
- | 'maxTagCount'
- | 'notFoundContent'
- | 'onChange'
- | 'onClear'
- | 'onDeselect'
- | 'onSelect'
- | 'onFocus'
- | 'onBlur'
- | 'onPopupScroll'
- | 'onSearch'
- | 'onDropdownVisibleChange'
- | 'placeholder'
- | 'showArrow'
- | 'showSearch'
- | 'tokenSeparators'
- | 'value'
- | 'getPopupContainer'
- | 'menuItemSelectedIcon'
->;
-
-export type SelectOptionsType = Exclude;
-
-export type SelectOptionsTypePage = {
- data: SelectOptionsType;
- totalCount: number;
-};
-
-export type SelectOptionsPagePromise = (
- search: string,
- page: number,
- pageSize: number,
-) => Promise;
-
-export const StyledContainer = styled.div`
- display: flex;
- flex-direction: column;
- width: 100%;
-`;
-
-export const StyledSelect = styled(AntdSelect)`
- ${({ theme }) => `
- && .ant-select-selector {
- border-radius: ${theme.gridUnit}px;
- }
- // Open the dropdown when clicking on the suffix
- // This is fixed in version 4.16
- .ant-select-arrow .anticon:not(.ant-select-suffix) {
- pointer-events: none;
- }
- `}
-`;
-
-export const StyledStopOutlined = styled(Icons.StopOutlined)`
- vertical-align: 0;
-`;
-
-export const StyledCheckOutlined = styled(Icons.CheckOutlined)`
- vertical-align: 0;
-`;
-
-export const StyledSpin = styled(Spin)`
- margin-top: ${({ theme }) => -theme.gridUnit}px;
-`;
-
-export const StyledLoadingText = styled.div`
- ${({ theme }) => `
- margin-left: ${theme.gridUnit * 3}px;
- line-height: ${theme.gridUnit * 8}px;
- color: ${theme.colors.grayscale.light1};
- `}
-`;
-
-const StyledHelperText = styled.div`
- ${({ theme }) => `
- padding: ${theme.gridUnit * 2}px ${theme.gridUnit * 3}px;
- color: ${theme.colors.grayscale.base};
- font-size: ${theme.typography.sizes.s}px;
- cursor: default;
- border-bottom: 1px solid ${theme.colors.grayscale.light2};
- `}
-`;
-
-export const MAX_TAG_COUNT = 4;
-export const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
-export const EMPTY_OPTIONS: SelectOptionsType = [];
-
-export const DEFAULT_SORT_COMPARATOR = (
- a: AntdLabeledValue,
- b: AntdLabeledValue,
- search?: string,
-) => {
- let aText: string | undefined;
- let bText: string | undefined;
- if (typeof a.label === 'string' && typeof b.label === 'string') {
- aText = a.label;
- bText = b.label;
- } else if (typeof a.value === 'string' && typeof b.value === 'string') {
- aText = a.value;
- bText = b.value;
- }
- // sort selected options first
- if (typeof aText === 'string' && typeof bText === 'string') {
- if (search) {
- return rankedSearchCompare(aText, bText, search);
- }
- return aText.localeCompare(bText);
- }
- return (a.value as number) - (b.value as number);
-};
-
/**
* It creates a comparator to check for a specific property.
* Can be used with string and number property values.
@@ -364,77 +186,6 @@ export const handleFilterOptionHelper = (
export const hasCustomLabels = (options: SelectOptionsType) =>
options?.some(opt => !!opt?.customLabel);
-export interface BaseSelectProps extends AntdExposedProps {
- /**
- * It enables the user to create new options.
- * Can be used with standard or async select types.
- * Can be used with any mode, single or multiple.
- * False by default.
- * */
- allowNewOptions?: boolean;
- /**
- * It adds the aria-label tag for accessibility standards.
- * Must be plain English and localized.
- */
- ariaLabel?: string;
- /**
- * Renders the dropdown
- */
- dropdownRender?: (
- menu: ReactElement>,
- ) => ReactElement>;
- /**
- * It adds a header on top of the Select.
- * Can be any ReactNode.
- */
- header?: ReactNode;
- /**
- * It adds a helper text on top of the Select options
- * with additional context to help with the interaction.
- */
- helperText?: string;
- /**
- * It allows to define which properties of the option object
- * should be looked for when searching.
- * By default label and value.
- */
- mappedMode?: 'multiple' | 'tags';
- /**
- * It defines whether the Select should allow for the
- * selection of multiple options or single.
- * Single by default.
- */
- mode?: 'single' | 'multiple';
- /**
- * Deprecated.
- * Prefer ariaLabel instead.
- */
- name?: string; // discourage usage
- /**
- * It allows to define which properties of the option object
- * should be looked for when searching.
- * By default label and value.
- */
- optionFilterProps?: string[];
- /**
- * It shows a stop-outlined icon at the far right of a selected
- * option instead of the default checkmark.
- * Useful to better indicate to the user that by clicking on a selected
- * option it will be de-selected.
- * False by default.
- */
- invertSelection?: boolean;
- /**
- * Customize how filtered options are sorted while users search.
- * Will not apply to predefined `options` array when users are not searching.
- */
- sortComparator?: typeof DEFAULT_SORT_COMPARATOR;
-
- suffixIcon?: ReactNode;
-
- ref: RefObject;
-}
-
export const renderSelectOptions = (options: SelectOptionsType) =>
options.map(opt => {
const isOptObject = typeof opt === 'object';
diff --git a/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx b/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx
index 74364fcf98..ddc242a764 100644
--- a/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx
+++ b/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx
@@ -20,8 +20,7 @@ import React, { useEffect, useState } from 'react';
import { t, SupersetClient } from '@superset-ui/core';
import ControlHeader from 'src/explore/components/ControlHeader';
import { Select } from 'src/components';
-import { SelectProps } from 'src/components/Select/Select';
-import { SelectOptionsType } from 'src/components/Select/utils';
+import { SelectOptionsType, SelectProps } from 'src/components/Select/types';
import { SelectValue, LabeledValue } from 'antd/lib/select';
import withToasts from 'src/components/MessageToasts/withToasts';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';