mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
feat: Select component (Iteration 1) (#15121)
* Implement initial structure * Add aria-label * Rename files * Refactor single mode new options * Clean up * Add select at every corner in storybook * Clean up * Add pagination * Move selected options at the top * Clean up * Add license * Refactor * Improve pagination * Fetch when allowNewOptions * Clean up
This commit is contained in:
244
superset-frontend/src/components/Select/AntdSelect.stories.tsx
Normal file
244
superset-frontend/src/components/Select/AntdSelect.stories.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* 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 from 'react';
|
||||
import Select, { OptionsType, SelectProps } from './AntdSelect';
|
||||
|
||||
export default {
|
||||
title: 'Select',
|
||||
component: Select,
|
||||
};
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: 'Such an incredibly awesome long long label',
|
||||
value: 'Such an incredibly awesome long long label',
|
||||
},
|
||||
{
|
||||
label: 'Another incredibly awesome long long label',
|
||||
value: 'Another incredibly awesome long long label',
|
||||
},
|
||||
{ label: 'Just a label', value: 'Just a 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 selectPositions = [
|
||||
{
|
||||
id: 'topLeft',
|
||||
style: { top: '0', left: '0' },
|
||||
},
|
||||
{
|
||||
id: 'topRight',
|
||||
style: { top: '0', right: '0' },
|
||||
},
|
||||
{
|
||||
id: 'bottomLeft',
|
||||
style: { bottom: '0', left: '0' },
|
||||
},
|
||||
{
|
||||
id: 'bottomRight',
|
||||
style: { bottom: '0', right: '0' },
|
||||
},
|
||||
];
|
||||
|
||||
export const AtEveryCorner = () => (
|
||||
<>
|
||||
{selectPositions.map(position => (
|
||||
<div
|
||||
key={position.id}
|
||||
style={{
|
||||
...position.style,
|
||||
width: '120px',
|
||||
position: 'absolute',
|
||||
}}
|
||||
>
|
||||
<Select ariaLabel={`gallery-${position.id}`} options={options} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
AtEveryCorner.story = {
|
||||
parameters: {
|
||||
actions: {
|
||||
disable: true,
|
||||
},
|
||||
controls: {
|
||||
disable: true,
|
||||
},
|
||||
knobs: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async function fetchUserList(search: string, page = 0): Promise<OptionsType> {
|
||||
const username = search.trim().toLowerCase();
|
||||
return new Promise(resolve => {
|
||||
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',
|
||||
];
|
||||
|
||||
let results: { label: string; value: string }[] = [];
|
||||
|
||||
if (!username) {
|
||||
results = users.map(u => ({
|
||||
label: u,
|
||||
value: u,
|
||||
}));
|
||||
} else {
|
||||
const foundUsers = users.find(u => u.toLowerCase().includes(username));
|
||||
if (foundUsers && Array.isArray(foundUsers)) {
|
||||
results = foundUsers.map(u => ({ label: u, value: u }));
|
||||
}
|
||||
if (foundUsers && typeof foundUsers === 'string') {
|
||||
const u = foundUsers;
|
||||
results = [{ label: u, value: u }];
|
||||
}
|
||||
}
|
||||
const offset = !page ? 0 : page * 10;
|
||||
const resultsNum = !page ? 10 : (page + 1) * 10;
|
||||
results = results.length ? results.splice(offset, resultsNum) : [];
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Emulating network request for search string: ${
|
||||
username || '"empty"'
|
||||
} and page: ${page} with results: [${results
|
||||
.map(u => u.value)
|
||||
.join(', ')}]`,
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
resolve(results);
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchUserListError(): Promise<OptionsType> {
|
||||
return new Promise((_, reject) => {
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
reject('This is an error');
|
||||
});
|
||||
}
|
||||
|
||||
export const AsyncSelect = (args: SelectProps & { withError: boolean }) => (
|
||||
<Select
|
||||
{...args}
|
||||
options={args.withError ? fetchUserListError : fetchUserList}
|
||||
/>
|
||||
);
|
||||
|
||||
AsyncSelect.args = {
|
||||
withError: false,
|
||||
allowNewOptions: false,
|
||||
paginatedFetch: false,
|
||||
};
|
||||
|
||||
AsyncSelect.argTypes = {
|
||||
mode: {
|
||||
control: { type: 'select', options: ['single', 'multiple', 'tags'] },
|
||||
},
|
||||
};
|
||||
|
||||
AsyncSelect.story = {
|
||||
parameters: {
|
||||
knobs: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const InteractiveSelect = (args: SelectProps) => <Select {...args} />;
|
||||
|
||||
InteractiveSelect.args = {
|
||||
allowNewOptions: false,
|
||||
options,
|
||||
showSearch: false,
|
||||
};
|
||||
|
||||
InteractiveSelect.argTypes = {
|
||||
mode: {
|
||||
control: { type: 'select', options: ['single', 'multiple', 'tags'] },
|
||||
},
|
||||
};
|
||||
|
||||
InteractiveSelect.story = {
|
||||
parameters: {
|
||||
knobs: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
355
superset-frontend/src/components/Select/AntdSelect.tsx
Normal file
355
superset-frontend/src/components/Select/AntdSelect.tsx
Normal file
@@ -0,0 +1,355 @@
|
||||
/**
|
||||
* 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, {
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
RefObject,
|
||||
UIEvent,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import { Select as AntdSelect } from 'antd';
|
||||
import Icons from 'src/components/Icons';
|
||||
import {
|
||||
SelectProps as AntdSelectProps,
|
||||
SelectValue as AntdSelectValue,
|
||||
LabeledValue as AntdLabeledValue,
|
||||
} from 'antd/lib/select';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||
import { hasOption } from './utils';
|
||||
|
||||
type AntdSelectAllProps = AntdSelectProps<AntdSelectValue>;
|
||||
type PickedSelectProps = Pick<
|
||||
AntdSelectAllProps,
|
||||
| 'allowClear'
|
||||
| 'autoFocus'
|
||||
| 'value'
|
||||
| 'defaultValue'
|
||||
| 'disabled'
|
||||
| 'filterOption'
|
||||
| 'loading'
|
||||
| 'mode'
|
||||
| 'notFoundContent'
|
||||
| 'onChange'
|
||||
| 'placeholder'
|
||||
| 'showSearch'
|
||||
| 'value'
|
||||
>;
|
||||
export type OptionsType = Exclude<AntdSelectAllProps['options'], undefined>;
|
||||
export type OptionsPromise = (
|
||||
search: string,
|
||||
page?: number,
|
||||
) => Promise<OptionsType>;
|
||||
export enum ESelectTypes {
|
||||
MULTIPLE = 'multiple',
|
||||
TAGS = 'tags',
|
||||
SINGLE = '',
|
||||
}
|
||||
export interface SelectProps extends PickedSelectProps {
|
||||
allowNewOptions?: boolean;
|
||||
ariaLabel: string;
|
||||
header?: ReactNode;
|
||||
name?: string; // discourage usage
|
||||
notFoundContent?: ReactNode;
|
||||
options: OptionsType | OptionsPromise;
|
||||
paginatedFetch?: boolean;
|
||||
}
|
||||
|
||||
// unexposed default behaviors
|
||||
const MAX_TAG_COUNT = 4;
|
||||
const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
|
||||
const DEBOUNCE_TIMEOUT = 800;
|
||||
|
||||
const Error = ({ error }: { error: string }) => {
|
||||
const StyledError = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
color: ${({ theme }) => theme.colors.error};
|
||||
`;
|
||||
return (
|
||||
<StyledError>
|
||||
<Icons.Error /> {error}
|
||||
</StyledError>
|
||||
);
|
||||
};
|
||||
|
||||
const DropdownContent = ({
|
||||
content,
|
||||
error,
|
||||
}: {
|
||||
content: ReactElement;
|
||||
error?: string;
|
||||
loading?: boolean;
|
||||
}) => {
|
||||
if (error) {
|
||||
return <Error error={error} />;
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
const SelectComponent = ({
|
||||
allowNewOptions = false,
|
||||
ariaLabel,
|
||||
filterOption,
|
||||
header = null,
|
||||
loading,
|
||||
mode,
|
||||
name,
|
||||
notFoundContent,
|
||||
paginatedFetch = false,
|
||||
placeholder = t('Select ...'),
|
||||
options,
|
||||
showSearch,
|
||||
value,
|
||||
...props
|
||||
}: SelectProps) => {
|
||||
const isAsync = typeof options === 'function';
|
||||
const isSingleMode =
|
||||
mode !== ESelectTypes.TAGS && mode !== ESelectTypes.MULTIPLE;
|
||||
const shouldShowSearch = isAsync || allowNewOptions ? true : showSearch;
|
||||
const initialOptions = options && Array.isArray(options) ? options : [];
|
||||
const [selectOptions, setOptions] = useState<OptionsType>(initialOptions);
|
||||
const [selectValue, setSelectValue] = useState(value);
|
||||
const [searchedValue, setSearchedValue] = useState('');
|
||||
const [isLoading, setLoading] = useState(loading);
|
||||
const [error, setError] = useState('');
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
|
||||
const fetchRef = useRef(0);
|
||||
|
||||
const handleSelectMode = () => {
|
||||
if (allowNewOptions && mode === ESelectTypes.MULTIPLE) {
|
||||
return ESelectTypes.TAGS;
|
||||
}
|
||||
if (!allowNewOptions && mode === ESelectTypes.TAGS) {
|
||||
return ESelectTypes.MULTIPLE;
|
||||
}
|
||||
return mode;
|
||||
};
|
||||
|
||||
const handleTopOptions = (selectedValue: any) => {
|
||||
// bringing selected options to the top of the list
|
||||
if (selectedValue) {
|
||||
const currentValue = selectedValue as string[] | string;
|
||||
const topOptions = selectOptions.filter(opt =>
|
||||
currentValue?.includes(opt.value),
|
||||
);
|
||||
const otherOptions = selectOptions.filter(
|
||||
opt => !topOptions.find(tOpt => tOpt.value === opt.value),
|
||||
);
|
||||
// fallback for custom options in tags mode as they
|
||||
// do not appear in the selectOptions state
|
||||
if (!isSingleMode && Array.isArray(currentValue)) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const val of currentValue) {
|
||||
if (!topOptions.find(tOpt => tOpt.value === val)) {
|
||||
topOptions.push({ label: val, value: val });
|
||||
}
|
||||
}
|
||||
}
|
||||
setOptions([...topOptions, ...otherOptions]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnSelect = (selectedValue: any) => {
|
||||
if (!isSingleMode) {
|
||||
const currentSelected = Array.isArray(selectValue) ? selectValue : [];
|
||||
setSelectValue([...currentSelected, selectedValue]);
|
||||
} else {
|
||||
setSelectValue(selectedValue);
|
||||
// in single mode the sorting must happen on selection
|
||||
handleTopOptions(selectedValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnDeselect = (value: any) => {
|
||||
if (Array.isArray(selectValue)) {
|
||||
const selectedValues = [
|
||||
...(selectValue as []).filter(opt => opt !== value),
|
||||
];
|
||||
setSelectValue(selectedValues);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFetch = useMemo(() => {
|
||||
const fetchOptions = options as OptionsPromise;
|
||||
const loadOptions = (value: string, paginate?: 'paginate') => {
|
||||
if (paginate) {
|
||||
fetchRef.current += 1;
|
||||
} else {
|
||||
fetchRef.current = 0;
|
||||
}
|
||||
const fetchId = fetchRef.current;
|
||||
const page = paginatedFetch ? fetchId : undefined;
|
||||
|
||||
fetchOptions(value, page)
|
||||
.then((newOptions: OptionsType) => {
|
||||
if (fetchId !== fetchRef.current) return;
|
||||
if (newOptions && Array.isArray(newOptions) && newOptions.length) {
|
||||
// merges with existing and creates unique options
|
||||
setOptions(prevOptions => [
|
||||
...prevOptions,
|
||||
...newOptions.filter(
|
||||
newOpt =>
|
||||
!prevOptions.find(prevOpt => prevOpt.value === newOpt.value),
|
||||
),
|
||||
]);
|
||||
}
|
||||
})
|
||||
.catch(response =>
|
||||
getClientErrorObject(response).then(e => {
|
||||
const { error } = e;
|
||||
setError(error);
|
||||
}),
|
||||
)
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
return debounce(loadOptions, DEBOUNCE_TIMEOUT);
|
||||
}, [options, paginatedFetch]);
|
||||
|
||||
const handleOnSearch = (search: string) => {
|
||||
const searchValue = search.trim();
|
||||
// enables option creation
|
||||
if (allowNewOptions && isSingleMode) {
|
||||
const lastOption = selectOptions[selectOptions.length - 1].value;
|
||||
// replaces the last search value entered with the new one
|
||||
// only when the value wasn't part of the original options
|
||||
if (
|
||||
lastOption === searchedValue &&
|
||||
!initialOptions.find(o => o.value === searchedValue)
|
||||
) {
|
||||
selectOptions.pop();
|
||||
setOptions(selectOptions);
|
||||
}
|
||||
if (searchValue && !hasOption(searchValue, selectOptions)) {
|
||||
const newOption = {
|
||||
label: searchValue,
|
||||
value: searchValue,
|
||||
};
|
||||
// adds a custom option
|
||||
const newOptions = [...selectOptions, newOption];
|
||||
setOptions(newOptions);
|
||||
}
|
||||
}
|
||||
setSearchedValue(searchValue);
|
||||
};
|
||||
|
||||
const handlePagination = (e: UIEvent<HTMLElement>) => {
|
||||
const vScroll = e.currentTarget;
|
||||
if (
|
||||
isAsync &&
|
||||
paginatedFetch &&
|
||||
vScroll.scrollTop === vScroll.scrollHeight - vScroll.offsetHeight
|
||||
) {
|
||||
handleFetch(searchedValue, 'paginate');
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilterOption = (search: string, option: AntdLabeledValue) => {
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
if (filterOption && typeof filterOption === 'boolean') return filterOption;
|
||||
if (filterOption && typeof filterOption === 'function') {
|
||||
return filterOption(search, option);
|
||||
}
|
||||
const { value, label } = option;
|
||||
if (
|
||||
value &&
|
||||
label &&
|
||||
typeof value === 'string' &&
|
||||
typeof label === 'string'
|
||||
) {
|
||||
return (
|
||||
value.toLowerCase().includes(searchValue) ||
|
||||
label.toLowerCase().includes(searchValue)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => {
|
||||
setIsDropdownVisible(isDropdownVisible);
|
||||
// multiple or tags mode keep the dropdown visible while selecting options
|
||||
// this waits for the dropdown to be closed before sorting the top options
|
||||
if (!isSingleMode && !isDropdownVisible) {
|
||||
handleTopOptions(selectValue);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const foundOption = hasOption(searchedValue, selectOptions);
|
||||
if (isAsync && !foundOption && !allowNewOptions) {
|
||||
setLoading(true);
|
||||
handleFetch(searchedValue);
|
||||
}
|
||||
}, [allowNewOptions, isAsync, handleFetch, searchedValue, selectOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAsync && allowNewOptions) {
|
||||
setLoading(true);
|
||||
handleFetch(searchedValue);
|
||||
}
|
||||
}, [allowNewOptions, isAsync, handleFetch, searchedValue]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{header}
|
||||
<AntdSelect
|
||||
aria-label={ariaLabel || name}
|
||||
dropdownRender={(
|
||||
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
|
||||
) => {
|
||||
if (!isDropdownVisible) {
|
||||
originNode.ref?.current?.scrollTo({ top: 0 });
|
||||
}
|
||||
return <DropdownContent content={originNode} error={error} />;
|
||||
}}
|
||||
filterOption={handleFilterOption as any}
|
||||
getPopupContainer={triggerNode => triggerNode.parentNode}
|
||||
loading={isLoading}
|
||||
maxTagCount={MAX_TAG_COUNT}
|
||||
mode={handleSelectMode()}
|
||||
notFoundContent={isLoading ? null : notFoundContent}
|
||||
onDeselect={handleOnDeselect}
|
||||
onDropdownVisibleChange={handleOnDropdownVisibleChange}
|
||||
onPopupScroll={handlePagination}
|
||||
onSearch={handleOnSearch}
|
||||
onSelect={handleOnSelect}
|
||||
options={selectOptions}
|
||||
placeholder={shouldShowSearch ? t('Search ...') : placeholder}
|
||||
showSearch={shouldShowSearch}
|
||||
tokenSeparators={TOKEN_SEPARATORS}
|
||||
value={selectValue}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Select = styled((
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
{ ...props }: SelectProps,
|
||||
) => <SelectComponent {...props} />)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export default Select;
|
||||
@@ -28,7 +28,7 @@ const OPTIONS = [
|
||||
];
|
||||
|
||||
export default {
|
||||
title: 'Select',
|
||||
title: 'DeprecatedSelect',
|
||||
argTypes: {
|
||||
options: {
|
||||
type: 'select',
|
||||
@@ -16,8 +16,8 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export * from './Select';
|
||||
export * from './DeprecatedSelect';
|
||||
export * from './styles';
|
||||
export { default } from './Select';
|
||||
export { default } from './DeprecatedSelect';
|
||||
export { default as OnPasteSelect } from './OnPasteSelect';
|
||||
export { NativeSelect, NativeGraySelect } from './NativeSelect';
|
||||
|
||||
@@ -30,7 +30,7 @@ import { Props as SelectProps } from 'react-select/src/Select';
|
||||
import { colors as reactSelectColors } from 'react-select/src/theme';
|
||||
import { DeepNonNullable } from 'react-select/src/components';
|
||||
import { OptionType } from 'antd/lib/select';
|
||||
import { SupersetStyledSelectProps } from './Select';
|
||||
import { SupersetStyledSelectProps } from './DeprecatedSelect';
|
||||
|
||||
export const DEFAULT_CLASS_NAME = 'Select';
|
||||
export const DEFAULT_CLASS_NAME_PREFIX = 'Select';
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
GroupedOptionsType,
|
||||
} from 'react-select';
|
||||
|
||||
import { OptionsType as AntdOptionsType } from './AntdSelect';
|
||||
|
||||
/**
|
||||
* Find Option value that matches a possibly string value.
|
||||
*
|
||||
@@ -57,3 +59,13 @@ export function findValue<OptionType extends OptionTypeBase>(
|
||||
// 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 hasOption(search: string, options: AntdOptionsType) {
|
||||
const searchOption = search.trim().toLowerCase();
|
||||
return options.find(
|
||||
opt =>
|
||||
opt.value.toLowerCase().includes(searchOption) ||
|
||||
(typeof opt.label === 'string' &&
|
||||
opt.label.toLowerCase().includes(searchOption)),
|
||||
);
|
||||
}
|
||||
|
||||
20
superset-frontend/src/components/index.ts
Normal file
20
superset-frontend/src/components/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { default as Select } from './Select/AntdSelect';
|
||||
Reference in New Issue
Block a user