mirror of
https://github.com/apache/superset.git
synced 2024-09-17 11:09:47 -04: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:
parent
dafaaaeb28
commit
d578ae9897
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 {
|
export default {
|
||||||
title: 'Select',
|
title: 'DeprecatedSelect',
|
||||||
argTypes: {
|
argTypes: {
|
||||||
options: {
|
options: {
|
||||||
type: 'select',
|
type: 'select',
|
@ -16,8 +16,8 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
export * from './Select';
|
export * from './DeprecatedSelect';
|
||||||
export * from './styles';
|
export * from './styles';
|
||||||
export { default } from './Select';
|
export { default } from './DeprecatedSelect';
|
||||||
export { default as OnPasteSelect } from './OnPasteSelect';
|
export { default as OnPasteSelect } from './OnPasteSelect';
|
||||||
export { NativeSelect, NativeGraySelect } from './NativeSelect';
|
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 { colors as reactSelectColors } from 'react-select/src/theme';
|
||||||
import { DeepNonNullable } from 'react-select/src/components';
|
import { DeepNonNullable } from 'react-select/src/components';
|
||||||
import { OptionType } from 'antd/lib/select';
|
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 = 'Select';
|
||||||
export const DEFAULT_CLASS_NAME_PREFIX = 'Select';
|
export const DEFAULT_CLASS_NAME_PREFIX = 'Select';
|
||||||
|
@ -23,6 +23,8 @@ import {
|
|||||||
GroupedOptionsType,
|
GroupedOptionsType,
|
||||||
} from 'react-select';
|
} from 'react-select';
|
||||||
|
|
||||||
|
import { OptionsType as AntdOptionsType } from './AntdSelect';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find Option value that matches a possibly string value.
|
* 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
|
// empty: https://github.com/JedWatson/react-select/blob/32ad5c040bdd96cd1ca71010c2558842d684629c/packages/react-select/src/utils.js#L64
|
||||||
return (Array.isArray(value) ? value : [value]).map(find);
|
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';
|
Loading…
Reference in New Issue
Block a user