You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2022/09/15 12:57:50 UTC
[superset] branch master updated: chore: Extract common select component code (#21094)
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 4fcc1d952f chore: Extract common select component code (#21094)
4fcc1d952f is described below
commit 4fcc1d952f69ba35fed538a24d88f06317621ca6
Author: cccs-RyanK <10...@users.noreply.github.com>
AuthorDate: Thu Sep 15 08:57:37 2022 -0400
chore: Extract common select component code (#21094)
---
.../src/components/Select/AsyncSelect.tsx | 385 +++++-------------
.../src/components/Select/Select.stories.tsx | 17 +-
superset-frontend/src/components/Select/Select.tsx | 318 +++------------
superset-frontend/src/components/Select/utils.ts | 99 -----
superset-frontend/src/components/Select/utils.tsx | 443 +++++++++++++++++++++
.../dashboard/components/RefreshIntervalModal.tsx | 3 +-
.../controls/SelectAsyncControl/index.tsx | 7 +-
.../components/Select/SelectFilterPlugin.tsx | 2 +-
.../src/views/CRUD/alert/AlertReportModal.tsx | 2 +-
9 files changed, 608 insertions(+), 668 deletions(-)
diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx b/superset-frontend/src/components/Select/AsyncSelect.tsx
index beeb7b262f..e3118f2284 100644
--- a/superset-frontend/src/components/Select/AsyncSelect.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.tsx
@@ -19,7 +19,6 @@
import React, {
forwardRef,
ReactElement,
- ReactNode,
RefObject,
UIEvent,
useEffect,
@@ -30,83 +29,62 @@ import React, {
useImperativeHandle,
} from 'react';
import { ensureIsArray, styled, t } from '@superset-ui/core';
-import AntdSelect, {
- SelectProps as AntdSelectProps,
- SelectValue as AntdSelectValue,
- LabeledValue as AntdLabeledValue,
-} from 'antd/lib/select';
-import { DownOutlined, SearchOutlined } from '@ant-design/icons';
-import { Spin } from 'antd';
+import { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
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';
-
-const { Option } = AntdSelect;
-
-type AntdSelectAllProps = AntdSelectProps<AntdSelectValue>;
-
-type PickedSelectProps = Pick<
- AntdSelectAllProps,
- | 'allowClear'
- | 'autoFocus'
- | 'disabled'
- | 'filterOption'
- | 'loading'
- | 'notFoundContent'
- | 'onChange'
- | 'onClear'
- | 'onFocus'
- | 'onBlur'
- | 'onDropdownVisibleChange'
- | 'placeholder'
- | 'showSearch'
- | 'tokenSeparators'
- | 'value'
- | 'getPopupContainer'
->;
-
-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>;
+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,
+ getSuffixIcon,
+ 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 PickedSelectProps {
- /**
- * 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;
- /**
- * 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;
+export interface AsyncSelectProps extends BaseSelectProps {
/**
* It fires a request against the server after
* the first interaction and not on render.
@@ -114,43 +92,18 @@ export interface AsyncSelectProps extends PickedSelectProps {
* True by default.
*/
lazyLoading?: boolean;
- /**
- * 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 defines the options of the Select.
* The options are async, a promise that returns
* an array of options.
*/
- options: OptionsPagePromise;
+ 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 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;
/**
* It fires a request against the server only after
* searching.
@@ -164,131 +117,14 @@ export interface AsyncSelectProps extends PickedSelectProps {
* 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.
- */
- sortComparator?: typeof DEFAULT_SORT_COMPARATOR;
}
-const StyledContainer = styled.div`
- display: flex;
- flex-direction: column;
- width: 100%;
-`;
-
-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;
- }
- .ant-select-dropdown {
- padding: 0;
- }
- `}
-`;
-
-const StyledStopOutlined = styled(Icons.StopOutlined)`
- vertical-align: 0;
-`;
-
-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;
-`;
-
-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};
- `}
-`;
-
-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,
- 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.
- * */
-export const propertyComparator =
- (property: string) => (a: AntdLabeledValue, b: AntdLabeledValue) => {
- if (typeof a[property] === 'string' && typeof b[property] === 'string') {
- return a[property].localeCompare(b[property]);
- }
- return (a[property] as number) - (b[property] as number);
- };
-
const getQueryCacheKey = (value: string, page: number, pageSize: number) =>
`${value};${page};${pageSize}`;
@@ -359,23 +195,30 @@ const AsyncSelect = forwardRef(
const sortSelectedFirst = useCallback(
(a: AntdLabeledValue, b: AntdLabeledValue) =>
- selectValue && a.value !== undefined && b.value !== undefined
- ? Number(hasOption(b.value, selectValue)) -
- Number(hasOption(a.value, selectValue))
- : 0,
+ sortSelectedFirstHelper(a, b, selectValue),
[selectValue],
);
+
const sortComparatorWithSearch = useCallback(
(a: AntdLabeledValue, b: AntdLabeledValue) =>
- sortSelectedFirst(a, b) || sortComparator(a, b, inputValue),
+ sortComparatorWithSearchHelper(
+ a,
+ b,
+ inputValue,
+ sortSelectedFirst,
+ sortComparator,
+ ),
[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.
- sortComparator(a, b, ''),
+ sortComparatorForNoSearchHelper(
+ a,
+ b,
+ sortSelectedFirst,
+ sortComparator,
+ ),
[sortComparator, sortSelectedFirst],
);
@@ -390,11 +233,11 @@ const AsyncSelect = forwardRef(
);
const [selectOptions, setSelectOptions] =
- useState<OptionsType>(initialOptionsSorted);
+ useState<SelectOptionsType>(initialOptionsSorted);
// add selected values to options list if they are not in it
const fullSelectOptions = useMemo(() => {
- const missingValues: OptionsType = ensureIsArray(selectValue)
+ const missingValues: SelectOptionsType = ensureIsArray(selectValue)
.filter(opt => !hasOption(getValue(opt), selectOptions))
.map(opt =>
isLabeledValue(opt) ? opt : { value: opt, label: String(opt) },
@@ -404,8 +247,6 @@ const AsyncSelect = forwardRef(
: selectOptions;
}, [selectOptions, selectValue]);
- const hasCustomLabels = fullSelectOptions.some(opt => !!opt?.customLabel);
-
const handleOnSelect = (
selectedItem: string | number | AntdLabeledValue | undefined,
) => {
@@ -459,8 +300,8 @@ const AsyncSelect = forwardRef(
);
const mergeData = useCallback(
- (data: OptionsType) => {
- let mergedData: OptionsType = [];
+ (data: SelectOptionsType) => {
+ let mergedData: SelectOptionsType = [];
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));
@@ -493,9 +334,9 @@ const AsyncSelect = forwardRef(
return;
}
setIsLoading(true);
- const fetchOptions = options as OptionsPagePromise;
+ const fetchOptions = options as SelectOptionsPagePromise;
fetchOptions(search, page, pageSize)
- .then(({ data, totalCount }: OptionsTypePage) => {
+ .then(({ data, totalCount }: SelectOptionsTypePage) => {
const mergedData = mergeData(data);
fetchedQueries.current.set(key, totalCount);
setTotalCount(totalCount);
@@ -569,25 +410,8 @@ const AsyncSelect = forwardRef(
}
};
- const handleFilterOption = (search: string, option: AntdLabeledValue) => {
- if (typeof filterOption === 'function') {
- return filterOption(search, option);
- }
-
- if (filterOption) {
- const searchValue = search.trim().toLowerCase();
- if (optionFilterProps && optionFilterProps.length) {
- return optionFilterProps.some(prop => {
- const optionProp = option?.[prop]
- ? String(option[prop]).trim().toLowerCase()
- : '';
- return optionProp.includes(searchValue);
- });
- }
- }
-
- return false;
- };
+ const handleFilterOption = (search: string, option: AntdLabeledValue) =>
+ handleFilterOptionHelper(search, option, optionFilterProps, filterOption);
const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => {
setIsDropdownVisible(isDropdownVisible);
@@ -624,36 +448,15 @@ const AsyncSelect = forwardRef(
const dropdownRender = (
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
- ) => {
- if (!isDropdownVisible) {
- originNode.ref?.current?.scrollTo({ top: 0 });
- }
- if (isLoading && fullSelectOptions.length === 0) {
- return <StyledLoadingText>{t('Loading...')}</StyledLoadingText>;
- }
- return error ? (
- <Error error={error} />
- ) : (
- <>
- {helperText && (
- <StyledHelperText role="note">{helperText}</StyledHelperText>
- )}
- {originNode}
- </>
+ ) =>
+ dropDownRenderHelper(
+ originNode,
+ isDropdownVisible,
+ isLoading,
+ fullSelectOptions.length,
+ helperText,
+ error ? <Error error={error} /> : undefined,
);
- };
-
- // use a function instead of component since every rerender of the
- // Select component will create a new component
- const getSuffixIcon = () => {
- if (isLoading) {
- return <StyledSpin size="small" />;
- }
- if (showSearch && isDropdownVisible) {
- return <SearchOutlined />;
- }
- return <DownOutlined />;
- };
const handleClear = () => {
setSelectValue(undefined);
@@ -709,6 +512,10 @@ const AsyncSelect = forwardRef(
[ref],
);
+ useEffect(() => {
+ setSelectValue(value);
+ }, [value]);
+
return (
<StyledContainer>
{header}
@@ -732,13 +539,15 @@ const AsyncSelect = forwardRef(
onSelect={handleOnSelect}
onClear={handleClear}
onChange={onChange}
- options={hasCustomLabels ? undefined : fullSelectOptions}
+ options={
+ hasCustomLabels(fullSelectOptions) ? undefined : fullSelectOptions
+ }
placeholder={placeholder}
showSearch={showSearch}
showArrow
tokenSeparators={tokenSeparators || TOKEN_SEPARATORS}
value={selectValue}
- suffixIcon={getSuffixIcon()}
+ suffixIcon={getSuffixIcon(isLoading, showSearch, isDropdownVisible)}
menuItemSelectedIcon={
invertSelection ? (
<StyledStopOutlined iconSize="m" />
@@ -746,21 +555,11 @@ const AsyncSelect = forwardRef(
<StyledCheckOutlined iconSize="m" />
)
}
- ref={ref}
{...props}
+ ref={ref}
>
- {hasCustomLabels &&
- fullSelectOptions.map(opt => {
- const isOptObject = typeof opt === 'object';
- const label = isOptObject ? opt?.label || opt.value : opt;
- const value = isOptObject ? opt.value : opt;
- const { customLabel, ...optProps } = opt;
- return (
- <Option {...optProps} key={value} label={label} value={value}>
- {isOptObject && customLabel ? customLabel : label}
- </Option>
- );
- })}
+ {hasCustomLabels(fullSelectOptions) &&
+ renderSelectOptions(fullSelectOptions)}
</StyledSelect>
</StyledContainer>
);
diff --git a/superset-frontend/src/components/Select/Select.stories.tsx b/superset-frontend/src/components/Select/Select.stories.tsx
index b75e1ff28b..e9a03fe563 100644
--- a/superset-frontend/src/components/Select/Select.stories.tsx
+++ b/superset-frontend/src/components/Select/Select.stories.tsx
@@ -25,13 +25,10 @@ import React, {
} from 'react';
import Button from 'src/components/Button';
import ControlHeader from 'src/explore/components/ControlHeader';
-import AsyncSelect, {
- AsyncSelectProps,
- AsyncSelectRef,
- OptionsTypePage,
-} from './AsyncSelect';
+import AsyncSelect, { AsyncSelectProps, AsyncSelectRef } from './AsyncSelect';
+import { SelectOptionsType, SelectOptionsTypePage } from './utils';
-import Select, { SelectProps, OptionsType } from './Select';
+import Select, { SelectProps } from './Select';
export default {
title: 'Select',
@@ -40,7 +37,7 @@ export default {
const DEFAULT_WIDTH = 200;
-const options: OptionsType = [
+const options: SelectOptionsType = [
{
label: 'Such an incredibly awesome long long label',
value: 'Such an incredibly awesome long long label',
@@ -160,7 +157,7 @@ const mountHeader = (type: String) => {
return header;
};
-const generateOptions = (opts: OptionsType, count: number) => {
+const generateOptions = (opts: SelectOptionsType, count: number) => {
let generated = opts.slice();
let iteration = 0;
while (generated.length < count) {
@@ -440,7 +437,7 @@ export const AsynchronousSelect = ({
search: string,
page: number,
pageSize: number,
- ): Promise<OptionsTypePage> => {
+ ): Promise<SelectOptionsTypePage> => {
const username = search.trim().toLowerCase();
return new Promise(resolve => {
let results = getResults(username);
@@ -458,7 +455,7 @@ export const AsynchronousSelect = ({
[responseTime],
);
- const fetchUserListError = async (): Promise<OptionsTypePage> =>
+ const fetchUserListError = async (): Promise<SelectOptionsTypePage> =>
new Promise((_, reject) => {
reject(new Error('Error while fetching the names from the server'));
});
diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx
index e668682b5d..6b6713852d 100644
--- a/superset-frontend/src/components/Select/Select.tsx
+++ b/superset-frontend/src/components/Select/Select.tsx
@@ -19,207 +19,48 @@
import React, {
forwardRef,
ReactElement,
- ReactNode,
RefObject,
useEffect,
useMemo,
useState,
useCallback,
} from 'react';
-import { ensureIsArray, styled, t } from '@superset-ui/core';
-import AntdSelect, {
- SelectProps as AntdSelectProps,
- SelectValue as AntdSelectValue,
- LabeledValue as AntdLabeledValue,
-} from 'antd/lib/select';
-import { DownOutlined, SearchOutlined } from '@ant-design/icons';
-import { Spin } from 'antd';
+import { ensureIsArray, t } from '@superset-ui/core';
+import { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
import { isEqual } from 'lodash';
-import Icons from 'src/components/Icons';
-import { rankedSearchCompare } from 'src/utils/rankedSearchCompare';
-import { getValue, hasOption, isLabeledValue } from './utils';
-
-const { Option } = AntdSelect;
-
-type AntdSelectAllProps = AntdSelectProps<AntdSelectValue>;
-
-type PickedSelectProps = Pick<
- AntdSelectAllProps,
- | 'allowClear'
- | 'autoFocus'
- | 'disabled'
- | 'filterOption'
- | 'labelInValue'
- | 'loading'
- | 'notFoundContent'
- | 'onChange'
- | 'onClear'
- | 'onFocus'
- | 'onBlur'
- | 'onDropdownVisibleChange'
- | 'placeholder'
- | 'showSearch'
- | 'tokenSeparators'
- | 'value'
- | 'getPopupContainer'
->;
-
-export type OptionsType = Exclude<AntdSelectAllProps['options'], undefined>;
-
-export interface SelectProps extends PickedSelectProps {
- /**
- * 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;
- /**
- * 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 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[];
+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: OptionsType;
- /**
- * 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;
+ options: SelectOptionsType;
}
-const StyledContainer = styled.div`
- display: flex;
- flex-direction: column;
- width: 100%;
-`;
-
-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;
- }
- .ant-select-dropdown {
- padding: 0;
- }
- `}
-`;
-
-const StyledStopOutlined = styled(Icons.StopOutlined)`
- vertical-align: 0;
-`;
-
-const StyledCheckOutlined = styled(Icons.CheckOutlined)`
- vertical-align: 0;
-`;
-
-const StyledSpin = styled(Spin)`
- margin-top: ${({ theme }) => -theme.gridUnit}px;
-`;
-
-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};
- `}
-`;
-
-const MAX_TAG_COUNT = 4;
-const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
-const EMPTY_OPTIONS: OptionsType = [];
-
-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.
- * */
-export const propertyComparator =
- (property: string) => (a: AntdLabeledValue, b: AntdLabeledValue) => {
- if (typeof a[property] === 'string' && typeof b[property] === 'string') {
- return a[property].localeCompare(b[property]);
- }
- return (a[property] as number) - (b[property] as number);
- };
-
/**
* This component is a customized version of the Antdesign 4.X Select component
* https://ant.design/components/select/.
@@ -278,15 +119,18 @@ const Select = forwardRef(
const sortSelectedFirst = useCallback(
(a: AntdLabeledValue, b: AntdLabeledValue) =>
- selectValue && a.value !== undefined && b.value !== undefined
- ? Number(hasOption(b.value, selectValue)) -
- Number(hasOption(a.value, selectValue))
- : 0,
+ sortSelectedFirstHelper(a, b, selectValue),
[selectValue],
);
const sortComparatorWithSearch = useCallback(
(a: AntdLabeledValue, b: AntdLabeledValue) =>
- sortSelectedFirst(a, b) || sortComparator(a, b, inputValue),
+ sortComparatorWithSearchHelper(
+ a,
+ b,
+ inputValue,
+ sortSelectedFirst,
+ sortComparator,
+ ),
[inputValue, sortComparator, sortSelectedFirst],
);
@@ -301,11 +145,11 @@ const Select = forwardRef(
);
const [selectOptions, setSelectOptions] =
- useState<OptionsType>(initialOptionsSorted);
+ useState<SelectOptionsType>(initialOptionsSorted);
// add selected values to options list if they are not in it
const fullSelectOptions = useMemo(() => {
- const missingValues: OptionsType = ensureIsArray(selectValue)
+ const missingValues: SelectOptionsType = ensureIsArray(selectValue)
.filter(opt => !hasOption(getValue(opt), selectOptions))
.map(opt =>
isLabeledValue(opt) ? opt : { value: opt, label: String(opt) },
@@ -315,8 +159,6 @@ const Select = forwardRef(
: selectOptions;
}, [selectOptions, selectValue]);
- const hasCustomLabels = fullSelectOptions.some(opt => !!opt?.customLabel);
-
const handleOnSelect = (
selectedItem: string | number | AntdLabeledValue | undefined,
) => {
@@ -376,25 +218,8 @@ const Select = forwardRef(
setInputValue(search);
};
- const handleFilterOption = (search: string, option: AntdLabeledValue) => {
- if (typeof filterOption === 'function') {
- return filterOption(search, option);
- }
-
- if (filterOption) {
- const searchValue = search.trim().toLowerCase();
- if (optionFilterProps && optionFilterProps.length) {
- return optionFilterProps.some(prop => {
- const optionProp = option?.[prop]
- ? String(option[prop]).trim().toLowerCase()
- : '';
- return optionProp.includes(searchValue);
- });
- }
- }
-
- return false;
- };
+ const handleFilterOption = (search: string, option: AntdLabeledValue) =>
+ handleFilterOptionHelper(search, option, optionFilterProps, filterOption);
const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => {
setIsDropdownVisible(isDropdownVisible);
@@ -413,34 +238,14 @@ const Select = forwardRef(
const dropdownRender = (
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
- ) => {
- if (!isDropdownVisible) {
- originNode.ref?.current?.scrollTo({ top: 0 });
- }
- if (isLoading && fullSelectOptions.length === 0) {
- return <StyledLoadingText>{t('Loading...')}</StyledLoadingText>;
- }
- return (
- <>
- {helperText && (
- <StyledHelperText role="note">{helperText}</StyledHelperText>
- )}
- {originNode}
- </>
+ ) =>
+ dropDownRenderHelper(
+ originNode,
+ isDropdownVisible,
+ isLoading,
+ fullSelectOptions.length,
+ helperText,
);
- };
-
- // use a function instead of component since every rerender of the
- // Select component will create a new component
- const getSuffixIcon = () => {
- if (isLoading) {
- return <StyledSpin size="small" />;
- }
- if (shouldShowSearch && isDropdownVisible) {
- return <SearchOutlined />;
- }
- return <DownOutlined />;
- };
const handleClear = () => {
setSelectValue(undefined);
@@ -454,16 +259,16 @@ const Select = forwardRef(
setSelectOptions(initialOptions);
}, [initialOptions]);
- useEffect(() => {
- setSelectValue(value);
- }, [value]);
-
useEffect(() => {
if (loading !== undefined && loading !== isLoading) {
setIsLoading(loading);
}
}, [isLoading, loading]);
+ useEffect(() => {
+ setSelectValue(value);
+ }, [value]);
+
return (
<StyledContainer>
{header}
@@ -487,13 +292,17 @@ const Select = forwardRef(
onSelect={handleOnSelect}
onClear={handleClear}
onChange={onChange}
- options={hasCustomLabels ? undefined : fullSelectOptions}
+ options={hasCustomLabels(options) ? undefined : fullSelectOptions}
placeholder={placeholder}
showSearch={shouldShowSearch}
showArrow
tokenSeparators={tokenSeparators || TOKEN_SEPARATORS}
value={selectValue}
- suffixIcon={getSuffixIcon()}
+ suffixIcon={getSuffixIcon(
+ isLoading,
+ shouldShowSearch,
+ isDropdownVisible,
+ )}
menuItemSelectedIcon={
invertSelection ? (
<StyledStopOutlined iconSize="m" />
@@ -501,21 +310,10 @@ const Select = forwardRef(
<StyledCheckOutlined iconSize="m" />
)
}
- ref={ref}
{...props}
+ ref={ref}
>
- {hasCustomLabels &&
- fullSelectOptions.map(opt => {
- const isOptObject = typeof opt === 'object';
- const label = isOptObject ? opt?.label || opt.value : opt;
- const value = isOptObject ? opt.value : opt;
- const { customLabel, ...optProps } = opt;
- return (
- <Option {...optProps} key={value} label={label} value={value}>
- {isOptObject && customLabel ? customLabel : label}
- </Option>
- );
- })}
+ {hasCustomLabels(options) && renderSelectOptions(fullSelectOptions)}
</StyledSelect>
</StyledContainer>
);
diff --git a/superset-frontend/src/components/Select/utils.ts b/superset-frontend/src/components/Select/utils.ts
deleted file mode 100644
index 9836b9ddd2..0000000000
--- a/superset-frontend/src/components/Select/utils.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * 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 { ReactNode } from 'react';
-import { ensureIsArray } from '@superset-ui/core';
-import {
- OptionTypeBase,
- ValueType,
- OptionsType,
- GroupedOptionsType,
-} from 'react-select';
-import { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
-
-export function isObject(value: unknown): value is Record<string, unknown> {
- return (
- value !== null &&
- typeof value === 'object' &&
- Array.isArray(value) === false
- );
-}
-
-/**
- * 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<OptionType extends OptionTypeBase>(
- value: ValueType<OptionType> | string,
- options: GroupedOptionsType<OptionType> | OptionsType<OptionType> = [],
- valueKey = 'value',
-): OptionType[] {
- if (value === null || value === undefined || value === '') {
- return [];
- }
- const isGroup = Array.isArray((options[0] || {}).options);
- const flatOptions = isGroup
- ? (options as GroupedOptionsType<OptionType>).flatMap(x => x.options || [])
- : (options as OptionsType<OptionType>);
-
- 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;
-}
-
-export function getValue(
- option: string | number | AntdLabeledValue | null | undefined,
-) {
- return isLabeledValue(option) ? option.value : option;
-}
-
-type LabeledValue<V> = { label?: ReactNode; value?: V };
-
-export function hasOption<V>(
- value: V,
- options?: V | LabeledValue<V> | (V | LabeledValue<V>)[],
- checkLabel = false,
-): boolean {
- const optionsArray = ensureIsArray(options);
- return (
- optionsArray.find(
- x =>
- x === value ||
- (isObject(x) &&
- (('value' in x && x.value === value) ||
- (checkLabel && 'label' in x && x.label === value))),
- ) !== undefined
- );
-}
diff --git a/superset-frontend/src/components/Select/utils.tsx b/superset-frontend/src/components/Select/utils.tsx
new file mode 100644
index 0000000000..9916ef07e2
--- /dev/null
+++ b/superset-frontend/src/components/Select/utils.tsx
@@ -0,0 +1,443 @@
+/**
+ * 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 { 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 { DownOutlined, SearchOutlined } from '@ant-design/icons';
+
+declare type RawValue = string | number;
+
+const { Option } = AntdSelect;
+
+export function isObject(value: unknown): value is Record<string, unknown> {
+ return (
+ value !== null &&
+ typeof value === 'object' &&
+ Array.isArray(value) === false
+ );
+}
+
+/**
+ * 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<OptionType extends OptionTypeBase>(
+ value: ValueType<OptionType> | string,
+ options: GroupedOptionsType<OptionType> | OptionsType<OptionType> = [],
+ valueKey = 'value',
+): OptionType[] {
+ if (value === null || value === undefined || value === '') {
+ return [];
+ }
+ const isGroup = Array.isArray((options[0] || {}).options);
+ const flatOptions = isGroup
+ ? (options as GroupedOptionsType<OptionType>).flatMap(x => x.options || [])
+ : (options as OptionsType<OptionType>);
+
+ 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;
+}
+
+export function getValue(
+ option: string | number | AntdLabeledValue | null | undefined,
+) {
+ return isLabeledValue(option) ? option.value : option;
+}
+
+type LabeledValue<V> = { label?: ReactNode; value?: V };
+
+export function hasOption<V>(
+ value: V,
+ options?: V | LabeledValue<V> | (V | LabeledValue<V>)[],
+ checkLabel = false,
+): boolean {
+ const optionsArray = ensureIsArray(options);
+ return (
+ optionsArray.find(
+ x =>
+ x === value ||
+ (isObject(x) &&
+ (('value' in x && x.value === value) ||
+ (checkLabel && 'label' in x && x.label === value))),
+ ) !== undefined
+ );
+}
+
+export type AntdProps = AntdSelectProps<AntdSelectValue>;
+
+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<AntdProps['options'], undefined>;
+
+export type SelectOptionsTypePage = {
+ data: SelectOptionsType;
+ totalCount: number;
+};
+
+export type SelectOptionsPagePromise = (
+ search: string,
+ page: number,
+ pageSize: number,
+) => Promise<SelectOptionsTypePage>;
+
+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.
+ * */
+export const propertyComparator =
+ (property: string) => (a: AntdLabeledValue, b: AntdLabeledValue) => {
+ if (typeof a[property] === 'string' && typeof b[property] === 'string') {
+ return a[property].localeCompare(b[property]);
+ }
+ return (a[property] as number) - (b[property] as number);
+ };
+
+export const sortSelectedFirstHelper = (
+ a: AntdLabeledValue,
+ b: AntdLabeledValue,
+ selectValue:
+ | string
+ | number
+ | RawValue[]
+ | AntdLabeledValue
+ | AntdLabeledValue[]
+ | undefined,
+) =>
+ selectValue && a.value !== undefined && b.value !== undefined
+ ? Number(hasOption(b.value, selectValue)) -
+ Number(hasOption(a.value, selectValue))
+ : 0;
+
+export const sortComparatorWithSearchHelper = (
+ a: AntdLabeledValue,
+ b: AntdLabeledValue,
+ inputValue: string,
+ sortCallback: (a: AntdLabeledValue, b: AntdLabeledValue) => number,
+ sortComparator: (
+ a: AntdLabeledValue,
+ b: AntdLabeledValue,
+ search?: string | undefined,
+ ) => number,
+) => sortCallback(a, b) || sortComparator(a, b, inputValue);
+
+export const sortComparatorForNoSearchHelper = (
+ a: AntdLabeledValue,
+ b: AntdLabeledValue,
+ sortCallback: (a: AntdLabeledValue, b: AntdLabeledValue) => number,
+ sortComparator: (
+ a: AntdLabeledValue,
+ b: AntdLabeledValue,
+ search?: string | undefined,
+ ) => number,
+) => sortCallback(a, b) || sortComparator(a, b, '');
+
+// use a function instead of component since every rerender of the
+// Select component will create a new component
+export const getSuffixIcon = (
+ isLoading: boolean | undefined,
+ showSearch: boolean,
+ isDropdownVisible: boolean,
+) => {
+ if (isLoading) {
+ return <StyledSpin size="small" />;
+ }
+ if (showSearch && isDropdownVisible) {
+ return <SearchOutlined />;
+ }
+ return <DownOutlined />;
+};
+
+export const dropDownRenderHelper = (
+ originNode: ReactElement & { ref?: RefObject<HTMLElement> },
+ isDropdownVisible: boolean,
+ isLoading: boolean | undefined,
+ optionsLength: number,
+ helperText: string | undefined,
+ errorComponent?: JSX.Element,
+) => {
+ if (!isDropdownVisible) {
+ originNode.ref?.current?.scrollTo({ top: 0 });
+ }
+ if (isLoading && optionsLength === 0) {
+ return <StyledLoadingText>{t('Loading...')}</StyledLoadingText>;
+ }
+ if (errorComponent) {
+ return errorComponent;
+ }
+ return (
+ <>
+ {helperText && (
+ <StyledHelperText role="note">{helperText}</StyledHelperText>
+ )}
+ {originNode}
+ </>
+ );
+};
+
+export const handleFilterOptionHelper = (
+ search: string,
+ option: AntdLabeledValue,
+ optionFilterProps: string[],
+ filterOption: boolean | Function,
+) => {
+ if (typeof filterOption === 'function') {
+ return filterOption(search, option);
+ }
+
+ if (filterOption) {
+ const searchValue = search.trim().toLowerCase();
+ if (optionFilterProps && optionFilterProps.length) {
+ return optionFilterProps.some(prop => {
+ const optionProp = option?.[prop]
+ ? String(option[prop]).trim().toLowerCase()
+ : '';
+ return optionProp.includes(searchValue);
+ });
+ }
+ }
+
+ return false;
+};
+
+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<any, string | JSXElementConstructor<any>>,
+ ) => ReactElement<any, string | JSXElementConstructor<any>>;
+ /**
+ * 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<HTMLInputElement>;
+}
+
+export const renderSelectOptions = (options: SelectOptionsType) =>
+ options.map(opt => {
+ const isOptObject = typeof opt === 'object';
+ const label = isOptObject ? opt?.label || opt.value : opt;
+ const value = isOptObject ? opt.value : opt;
+ const { customLabel, ...optProps } = opt;
+ return (
+ <Option {...optProps} key={value} label={label} value={value}>
+ {isOptObject && customLabel ? customLabel : label}
+ </Option>
+ );
+ });
diff --git a/superset-frontend/src/dashboard/components/RefreshIntervalModal.tsx b/superset-frontend/src/dashboard/components/RefreshIntervalModal.tsx
index 54d11bba14..98b763ed85 100644
--- a/superset-frontend/src/dashboard/components/RefreshIntervalModal.tsx
+++ b/superset-frontend/src/dashboard/components/RefreshIntervalModal.tsx
@@ -17,13 +17,14 @@
* under the License.
*/
import React from 'react';
-import Select, { propertyComparator } from 'src/components/Select/Select';
+import Select from 'src/components/Select/Select';
import { t, styled } from '@superset-ui/core';
import Alert from 'src/components/Alert';
import Button from 'src/components/Button';
import ModalTrigger, { ModalTriggerRef } from 'src/components/ModalTrigger';
import { FormLabel } from 'src/components/Form';
+import { propertyComparator } from 'src/components/Select/utils';
export const options = [
[0, t("Don't refresh")],
diff --git a/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx b/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx
index 66d9fb154e..74364fcf98 100644
--- a/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx
+++ b/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx
@@ -20,7 +20,8 @@ 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, OptionsType } from 'src/components/Select/Select';
+import { SelectProps } from 'src/components/Select/Select';
+import { SelectOptionsType } from 'src/components/Select/utils';
import { SelectValue, LabeledValue } from 'antd/lib/select';
import withToasts from 'src/components/MessageToasts/withToasts';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
@@ -32,7 +33,7 @@ interface SelectAsyncControlProps extends SelectAsyncProps {
ariaLabel?: string;
dataEndpoint: string;
default?: SelectValue;
- mutator?: (response: Record<string, any>) => OptionsType;
+ mutator?: (response: Record<string, any>) => SelectOptionsType;
multi?: boolean;
onChange: (val: SelectValue) => void;
// ControlHeader related props
@@ -57,7 +58,7 @@ const SelectAsyncControl = ({
value,
...props
}: SelectAsyncControlProps) => {
- const [options, setOptions] = useState<OptionsType>([]);
+ const [options, setOptions] = useState<SelectOptionsType>([]);
const handleOnChange = (val: SelectValue) => {
let onChangeVal = val;
diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
index 936c6d548a..d1bb3df747 100644
--- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
+++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
@@ -36,7 +36,7 @@ import { Select } from 'src/components';
import debounce from 'lodash/debounce';
import { SLOW_DEBOUNCE } from 'src/constants';
import { useImmerReducer } from 'use-immer';
-import { propertyComparator } from 'src/components/Select/Select';
+import { propertyComparator } from 'src/components/Select/utils';
import { PluginFilterSelectProps, SelectValue } from './types';
import { StyledFormItem, FilterPluginStyle, StatusMessage } from '../common';
import { getDataRecordFormatter, getSelectExtraFormData } from '../../utils';
diff --git a/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx b/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx
index 820a83b8c3..fe34b22e80 100644
--- a/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx
+++ b/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx
@@ -38,7 +38,7 @@ import { Switch } from 'src/components/Switch';
import Modal from 'src/components/Modal';
import TimezoneSelector from 'src/components/TimezoneSelector';
import { Radio } from 'src/components/Radio';
-import { propertyComparator } from 'src/components/Select/Select';
+import { propertyComparator } from 'src/components/Select/utils';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import withToasts from 'src/components/MessageToasts/withToasts';
import Owner from 'src/types/Owner';