refactor: Organizes the Select files (#21589)

This commit is contained in:
Michael S. Molina 2022-09-27 08:38:06 -03:00 committed by GitHub
parent 24412e282d
commit bb1cf7f145
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 747 additions and 591 deletions

View File

@ -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: <div style={{ color: 'red' }}>JSX Label</div>,
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<ReactNode[]>([]);
const ref = useRef<AsyncSelectRef>(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 <b>{username || 'empty'}</b> ...{' '}
<b>
{results}/{total}
</b>{' '}
results
</>
);
setRequests(requests => [request, ...requests]);
};
const fetchUserListPage = useCallback(
(
search: string,
page: number,
pageSize: number,
): Promise<SelectOptionsTypePage> => {
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<SelectOptionsTypePage> =>
new Promise((_, reject) => {
reject(new Error('Error while fetching the names from the server'));
});
const initialValue = useMemo(
() => ({ label: 'Valentina', value: 'Valentina' }),
[],
);
return (
<>
<div
style={{
width: DEFAULT_WIDTH,
}}
>
<AsyncSelect
{...rest}
ref={ref}
fetchOnlyOnSearch={fetchOnlyOnSearch}
options={withError ? fetchUserListError : fetchUserListPage}
placeholder={fetchOnlyOnSearch ? 'Type anything' : 'AsyncSelect...'}
value={withInitialValue ? initialValue : undefined}
/>
</div>
<div
style={{
position: 'absolute',
top: 32,
left: DEFAULT_WIDTH + 100,
height: 400,
width: 600,
overflowY: 'auto',
border: '1px solid #d9d9d9',
padding: 20,
}}
>
{requests.map((request, index) => (
<p key={`request-${index}`}>{request}</p>
))}
</div>
<Button
style={{
position: 'absolute',
top: 452,
left: DEFAULT_WIDTH + 580,
}}
onClick={() => {
ref.current?.clearCache();
setRequests([]);
}}
>
Clear cache
</Button>
</>
);
};
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,
},
},
};

View File

@ -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 }) => (
<StyledError>

View File

@ -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<ReactNode[]>([]);
const ref = useRef<AsyncSelectRef>(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 <b>{username || 'empty'}</b> ...{' '}
<b>
{results}/{total}
</b>{' '}
results
</>
);
setRequests(requests => [request, ...requests]);
};
const fetchUserListPage = useCallback(
(
search: string,
page: number,
pageSize: number,
): Promise<SelectOptionsTypePage> => {
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<SelectOptionsTypePage> =>
new Promise((_, reject) => {
reject(new Error('Error while fetching the names from the server'));
});
const initialValue = useMemo(
() => ({ label: 'Valentina', value: 'Valentina' }),
[],
);
return (
<>
<div
style={{
width: DEFAULT_WIDTH,
}}
>
<AsyncSelect
{...rest}
ref={ref}
fetchOnlyOnSearch={fetchOnlyOnSearch}
options={withError ? fetchUserListError : fetchUserListPage}
placeholder={fetchOnlyOnSearch ? 'Type anything' : 'AsyncSelect...'}
value={withInitialValue ? initialValue : undefined}
/>
</div>
<div
style={{
position: 'absolute',
top: 32,
left: DEFAULT_WIDTH + 100,
height: 400,
width: 600,
overflowY: 'auto',
border: '1px solid #d9d9d9',
padding: 20,
}}
>
{requests.map((request, index) => (
<p key={`request-${index}`}>{request}</p>
))}
</div>
<Button
style={{
position: 'absolute',
top: 452,
left: DEFAULT_WIDTH + 580,
}}
onClick={() => {
ref.current?.clearCache();
setRequests([]);
}}
>
Clear cache
</Button>
</>
);
};
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,
},
},
};

View File

@ -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

View File

@ -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);
};

View File

@ -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;
`;

View File

@ -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<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 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?: (
a: AntdLabeledValue,
b: AntdLabeledValue,
search?: string,
) => number;
suffixIcon?: ReactNode;
ref: RefObject<HTMLInputElement>;
}
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<SelectOptionsTypePage>;
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;
}

View File

@ -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<string, unknown> {
);
}
/**
* 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;
}
@ -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<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.
@ -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<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';

View File

@ -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';