mirror of
https://github.com/apache/superset.git
synced 2024-09-12 00:29:39 -04:00
Enhance Select (#15550)
This commit is contained in:
parent
0af5a3d600
commit
314d49c13b
@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
import React, { ReactNode, useState, useCallback } from 'react';
|
import React, { ReactNode, useState, useCallback } from 'react';
|
||||||
import ControlHeader from 'src/explore/components/ControlHeader';
|
import ControlHeader from 'src/explore/components/ControlHeader';
|
||||||
import Select, { SelectProps, OptionsType, OptionsTypePage } from './Select';
|
import Select, { SelectProps, OptionsTypePage } from './Select';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Select',
|
title: 'Select',
|
||||||
@ -144,11 +144,6 @@ InteractiveSelect.argTypes = {
|
|||||||
disable: true,
|
disable: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
paginatedFetch: {
|
|
||||||
table: {
|
|
||||||
disable: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
InteractiveSelect.story = {
|
InteractiveSelect.story = {
|
||||||
@ -302,7 +297,6 @@ const USERS = [
|
|||||||
export const AsyncSelect = ({
|
export const AsyncSelect = ({
|
||||||
withError,
|
withError,
|
||||||
responseTime,
|
responseTime,
|
||||||
paginatedFetch,
|
|
||||||
...rest
|
...rest
|
||||||
}: SelectProps & {
|
}: SelectProps & {
|
||||||
withError: boolean;
|
withError: boolean;
|
||||||
@ -310,7 +304,7 @@ export const AsyncSelect = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [requests, setRequests] = useState<ReactNode[]>([]);
|
const [requests, setRequests] = useState<ReactNode[]>([]);
|
||||||
|
|
||||||
const getResults = (username: string) => {
|
const getResults = (username?: string) => {
|
||||||
let results: { label: string; value: string }[] = [];
|
let results: { label: string; value: string }[] = [];
|
||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
@ -329,7 +323,7 @@ export const AsyncSelect = ({
|
|||||||
return results;
|
return results;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setRequestLog = (username: string, results: number, total: number) => {
|
const setRequestLog = (results: number, total: number, username?: string) => {
|
||||||
const request = (
|
const request = (
|
||||||
<>
|
<>
|
||||||
Emulating network request with search <b>{username || 'empty'}</b> ...{' '}
|
Emulating network request with search <b>{username || 'empty'}</b> ...{' '}
|
||||||
@ -343,20 +337,6 @@ export const AsyncSelect = ({
|
|||||||
setRequests(requests => [request, ...requests]);
|
setRequests(requests => [request, ...requests]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchUserList = useCallback(
|
|
||||||
(search: string): Promise<OptionsType> => {
|
|
||||||
const username = search.trim().toLowerCase();
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const results = getResults(username);
|
|
||||||
setRequestLog(username, results.length, results.length);
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve(results);
|
|
||||||
}, responseTime * 1000);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[responseTime],
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchUserListPage = useCallback(
|
const fetchUserListPage = useCallback(
|
||||||
(
|
(
|
||||||
search: string,
|
search: string,
|
||||||
@ -367,16 +347,14 @@ export const AsyncSelect = ({
|
|||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
let results = getResults(username);
|
let results = getResults(username);
|
||||||
const totalCount = results.length;
|
const totalCount = results.length;
|
||||||
if (paginatedFetch) {
|
results = results.splice(offset, limit);
|
||||||
results = results.splice(offset, limit);
|
setRequestLog(offset + results.length, totalCount, username);
|
||||||
}
|
|
||||||
setRequestLog(username, offset + results.length, totalCount);
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve({ data: results, totalCount });
|
resolve({ data: results, totalCount });
|
||||||
}, responseTime * 1000);
|
}, responseTime * 1000);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[paginatedFetch, responseTime],
|
[responseTime],
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchUserListError = async (): Promise<OptionsTypePage> =>
|
const fetchUserListError = async (): Promise<OptionsTypePage> =>
|
||||||
@ -393,14 +371,7 @@ export const AsyncSelect = ({
|
|||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
{...rest}
|
{...rest}
|
||||||
paginatedFetch={paginatedFetch}
|
options={withError ? fetchUserListError : fetchUserListPage}
|
||||||
options={
|
|
||||||
withError
|
|
||||||
? fetchUserListError
|
|
||||||
: paginatedFetch
|
|
||||||
? fetchUserListPage
|
|
||||||
: fetchUserList
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -425,7 +396,6 @@ export const AsyncSelect = ({
|
|||||||
|
|
||||||
AsyncSelect.args = {
|
AsyncSelect.args = {
|
||||||
withError: false,
|
withError: false,
|
||||||
paginatedFetch: false,
|
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
allowNewOptions: false,
|
allowNewOptions: false,
|
||||||
};
|
};
|
||||||
|
@ -50,6 +50,7 @@ type PickedSelectProps = Pick<
|
|||||||
| 'defaultValue'
|
| 'defaultValue'
|
||||||
| 'disabled'
|
| 'disabled'
|
||||||
| 'filterOption'
|
| 'filterOption'
|
||||||
|
| 'notFoundContent'
|
||||||
| 'onChange'
|
| 'onChange'
|
||||||
| 'placeholder'
|
| 'placeholder'
|
||||||
| 'showSearch'
|
| 'showSearch'
|
||||||
@ -63,8 +64,6 @@ export type OptionsTypePage = {
|
|||||||
totalCount: number;
|
totalCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OptionsPromise = (search: string) => Promise<OptionsType>;
|
|
||||||
|
|
||||||
export type OptionsPagePromise = (
|
export type OptionsPagePromise = (
|
||||||
search: string,
|
search: string,
|
||||||
offset: number,
|
offset: number,
|
||||||
@ -77,8 +76,7 @@ export interface SelectProps extends PickedSelectProps {
|
|||||||
header?: ReactNode;
|
header?: ReactNode;
|
||||||
mode?: 'single' | 'multiple';
|
mode?: 'single' | 'multiple';
|
||||||
name?: string; // discourage usage
|
name?: string; // discourage usage
|
||||||
options: OptionsType | OptionsPromise | OptionsPagePromise;
|
options: OptionsType | OptionsPagePromise;
|
||||||
paginatedFetch?: boolean;
|
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
invertSelection?: boolean;
|
invertSelection?: boolean;
|
||||||
}
|
}
|
||||||
@ -94,6 +92,10 @@ const StyledSelect = styled(AntdSelect, {
|
|||||||
${({ theme, hasHeader }) => `
|
${({ theme, hasHeader }) => `
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: ${hasHeader ? theme.gridUnit : 0}px;
|
margin-top: ${hasHeader ? theme.gridUnit : 0}px;
|
||||||
|
|
||||||
|
&& .ant-select-selector {
|
||||||
|
border-radius: ${theme.gridUnit}px;
|
||||||
|
}
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -120,11 +122,11 @@ const StyledError = styled.div`
|
|||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// default behaviors
|
|
||||||
const MAX_TAG_COUNT = 4;
|
const MAX_TAG_COUNT = 4;
|
||||||
const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
|
const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
|
||||||
const DEBOUNCE_TIMEOUT = 500;
|
const DEBOUNCE_TIMEOUT = 500;
|
||||||
const DEFAULT_PAGE_SIZE = 50;
|
const DEFAULT_PAGE_SIZE = 50;
|
||||||
|
const EMPTY_OPTIONS: OptionsType = [];
|
||||||
|
|
||||||
const Error = ({ error }: { error: string }) => (
|
const Error = ({ error }: { error: string }) => (
|
||||||
<StyledError>
|
<StyledError>
|
||||||
@ -135,11 +137,10 @@ const Error = ({ error }: { error: string }) => (
|
|||||||
const Select = ({
|
const Select = ({
|
||||||
allowNewOptions = false,
|
allowNewOptions = false,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
filterOption,
|
filterOption = true,
|
||||||
header = null,
|
header = null,
|
||||||
mode = 'single',
|
mode = 'single',
|
||||||
name,
|
name,
|
||||||
paginatedFetch,
|
|
||||||
pageSize = DEFAULT_PAGE_SIZE,
|
pageSize = DEFAULT_PAGE_SIZE,
|
||||||
placeholder = t('Select ...'),
|
placeholder = t('Select ...'),
|
||||||
options,
|
options,
|
||||||
@ -151,8 +152,11 @@ const Select = ({
|
|||||||
const isAsync = typeof options === 'function';
|
const isAsync = typeof options === 'function';
|
||||||
const isSingleMode = mode === 'single';
|
const isSingleMode = mode === 'single';
|
||||||
const shouldShowSearch = isAsync || allowNewOptions ? true : showSearch;
|
const shouldShowSearch = isAsync || allowNewOptions ? true : showSearch;
|
||||||
const initialOptions = options && Array.isArray(options) ? options : [];
|
const initialOptions =
|
||||||
const [selectOptions, setOptions] = useState<OptionsType>(initialOptions);
|
options && Array.isArray(options) ? options : EMPTY_OPTIONS;
|
||||||
|
const [selectOptions, setSelectOptions] = useState<OptionsType>(
|
||||||
|
initialOptions,
|
||||||
|
);
|
||||||
const [selectValue, setSelectValue] = useState(value);
|
const [selectValue, setSelectValue] = useState(value);
|
||||||
const [searchedValue, setSearchedValue] = useState('');
|
const [searchedValue, setSearchedValue] = useState('');
|
||||||
const [isLoading, setLoading] = useState(false);
|
const [isLoading, setLoading] = useState(false);
|
||||||
@ -167,6 +171,16 @@ const Select = ({
|
|||||||
? 'tags'
|
? 'tags'
|
||||||
: 'multiple';
|
: 'multiple';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectOptions(
|
||||||
|
options && Array.isArray(options) ? options : EMPTY_OPTIONS,
|
||||||
|
);
|
||||||
|
}, [options]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectValue(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
const handleTopOptions = useCallback(
|
const handleTopOptions = useCallback(
|
||||||
(selectedValue: AntdSelectValue | undefined) => {
|
(selectedValue: AntdSelectValue | undefined) => {
|
||||||
// bringing selected options to the top of the list
|
// bringing selected options to the top of the list
|
||||||
@ -193,7 +207,7 @@ const Select = ({
|
|||||||
|
|
||||||
const sortedOptions = [...topOptions, ...otherOptions];
|
const sortedOptions = [...topOptions, ...otherOptions];
|
||||||
if (!isEqual(sortedOptions, selectOptions)) {
|
if (!isEqual(sortedOptions, selectOptions)) {
|
||||||
setOptions(sortedOptions);
|
setSelectOptions(sortedOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -244,7 +258,7 @@ const Select = ({
|
|||||||
const handleData = (data: OptionsType) => {
|
const handleData = (data: OptionsType) => {
|
||||||
if (data && Array.isArray(data) && data.length) {
|
if (data && Array.isArray(data) && data.length) {
|
||||||
// merges with existing and creates unique options
|
// merges with existing and creates unique options
|
||||||
setOptions(prevOptions => [
|
setSelectOptions(prevOptions => [
|
||||||
...prevOptions,
|
...prevOptions,
|
||||||
...data.filter(
|
...data.filter(
|
||||||
newOpt =>
|
newOpt =>
|
||||||
@ -254,24 +268,6 @@ const Select = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFetch = useMemo(
|
|
||||||
() => (value: string) => {
|
|
||||||
if (fetchedQueries.current.has(value)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLoading(true);
|
|
||||||
const fetchOptions = options as OptionsPromise;
|
|
||||||
fetchOptions(value)
|
|
||||||
.then((data: OptionsType) => {
|
|
||||||
handleData(data);
|
|
||||||
fetchedQueries.current.add(value);
|
|
||||||
})
|
|
||||||
.catch(onError)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
},
|
|
||||||
[options],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlePaginatedFetch = useMemo(
|
const handlePaginatedFetch = useMemo(
|
||||||
() => (value: string, offset: number, limit: number) => {
|
() => (value: string, offset: number, limit: number) => {
|
||||||
const key = `${value};${offset};${limit}`;
|
const key = `${value};${offset};${limit}`;
|
||||||
@ -305,7 +301,7 @@ const Select = ({
|
|||||||
!initialOptions.find(o => o.value === searchedValue)
|
!initialOptions.find(o => o.value === searchedValue)
|
||||||
) {
|
) {
|
||||||
selectOptions.shift();
|
selectOptions.shift();
|
||||||
setOptions(selectOptions);
|
setSelectOptions(selectOptions);
|
||||||
}
|
}
|
||||||
if (searchValue && !hasOption(searchValue, selectOptions)) {
|
if (searchValue && !hasOption(searchValue, selectOptions)) {
|
||||||
const newOption = {
|
const newOption = {
|
||||||
@ -314,7 +310,7 @@ const Select = ({
|
|||||||
};
|
};
|
||||||
// adds a custom option
|
// adds a custom option
|
||||||
const newOptions = [...selectOptions, newOption];
|
const newOptions = [...selectOptions, newOption];
|
||||||
setOptions(newOptions);
|
setSelectOptions(newOptions);
|
||||||
setSelectValue(searchValue);
|
setSelectValue(searchValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -337,24 +333,22 @@ const Select = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterOption = (search: string, option: AntdLabeledValue) => {
|
const handleFilterOption = (search: string, option: AntdLabeledValue) => {
|
||||||
const searchValue = search.trim().toLowerCase();
|
if (typeof filterOption === 'function') {
|
||||||
if (filterOption && typeof filterOption === 'boolean') return filterOption;
|
|
||||||
if (filterOption && typeof filterOption === 'function') {
|
|
||||||
return filterOption(search, option);
|
return filterOption(search, option);
|
||||||
}
|
}
|
||||||
const { value, label } = option;
|
|
||||||
if (
|
if (filterOption) {
|
||||||
value &&
|
const searchValue = search.trim().toLowerCase();
|
||||||
label &&
|
const { value, label } = option;
|
||||||
typeof value === 'string' &&
|
const valueText = String(value);
|
||||||
typeof label === 'string'
|
const labelText = String(label);
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
value.toLowerCase().includes(searchValue) ||
|
valueText.toLowerCase().includes(searchValue) ||
|
||||||
label.toLowerCase().includes(searchValue)
|
labelText.toLowerCase().includes(searchValue)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => {
|
const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => {
|
||||||
@ -369,23 +363,11 @@ const Select = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const foundOption = hasOption(searchedValue, selectOptions);
|
const foundOption = hasOption(searchedValue, selectOptions);
|
||||||
if (isAsync && !foundOption) {
|
if (isAsync && !foundOption) {
|
||||||
if (paginatedFetch) {
|
const offset = 0;
|
||||||
const offset = 0;
|
handlePaginatedFetch(searchedValue, offset, pageSize);
|
||||||
handlePaginatedFetch(searchedValue, offset, pageSize);
|
setOffset(offset);
|
||||||
setOffset(offset);
|
|
||||||
} else {
|
|
||||||
handleFetch(searchedValue);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [isAsync, searchedValue, selectOptions, pageSize, handlePaginatedFetch]);
|
||||||
isAsync,
|
|
||||||
handleFetch,
|
|
||||||
searchedValue,
|
|
||||||
selectOptions,
|
|
||||||
pageSize,
|
|
||||||
paginatedFetch,
|
|
||||||
handlePaginatedFetch,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSingleMode) {
|
if (isSingleMode) {
|
||||||
@ -416,7 +398,7 @@ const Select = ({
|
|||||||
mode={mappedMode}
|
mode={mappedMode}
|
||||||
onDeselect={handleOnDeselect}
|
onDeselect={handleOnDeselect}
|
||||||
onDropdownVisibleChange={handleOnDropdownVisibleChange}
|
onDropdownVisibleChange={handleOnDropdownVisibleChange}
|
||||||
onPopupScroll={paginatedFetch ? handlePagination : undefined}
|
onPopupScroll={isAsync ? handlePagination : undefined}
|
||||||
onSearch={handleOnSearch}
|
onSearch={handleOnSearch}
|
||||||
onSelect={handleOnSelect}
|
onSelect={handleOnSelect}
|
||||||
onClear={() => setSelectValue(undefined)}
|
onClear={() => setSelectValue(undefined)}
|
||||||
|
Loading…
Reference in New Issue
Block a user