mirror of
https://github.com/apache/superset.git
synced 2024-09-06 13:57:40 -04:00
refactor: Organizes the Select files (#21589)
This commit is contained in:
parent
24412e282d
commit
bb1cf7f145
359
superset-frontend/src/components/Select/AsyncSelect.stories.tsx
Normal file
359
superset-frontend/src/components/Select/AsyncSelect.stories.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -28,7 +28,7 @@ import React, {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
} from 'react';
|
} 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 { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
@ -39,20 +39,8 @@ import {
|
|||||||
getValue,
|
getValue,
|
||||||
hasOption,
|
hasOption,
|
||||||
isLabeledValue,
|
isLabeledValue,
|
||||||
DEFAULT_SORT_COMPARATOR,
|
|
||||||
EMPTY_OPTIONS,
|
|
||||||
MAX_TAG_COUNT,
|
|
||||||
SelectOptionsPagePromise,
|
|
||||||
SelectOptionsType,
|
|
||||||
SelectOptionsTypePage,
|
|
||||||
StyledCheckOutlined,
|
|
||||||
StyledStopOutlined,
|
|
||||||
TOKEN_SEPARATORS,
|
|
||||||
renderSelectOptions,
|
renderSelectOptions,
|
||||||
StyledContainer,
|
|
||||||
StyledSelect,
|
|
||||||
hasCustomLabels,
|
hasCustomLabels,
|
||||||
BaseSelectProps,
|
|
||||||
sortSelectedFirstHelper,
|
sortSelectedFirstHelper,
|
||||||
sortComparatorWithSearchHelper,
|
sortComparatorWithSearchHelper,
|
||||||
sortComparatorForNoSearchHelper,
|
sortComparatorForNoSearchHelper,
|
||||||
@ -60,64 +48,28 @@ import {
|
|||||||
dropDownRenderHelper,
|
dropDownRenderHelper,
|
||||||
handleFilterOptionHelper,
|
handleFilterOptionHelper,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
import {
|
||||||
const StyledError = styled.div`
|
AsyncSelectProps,
|
||||||
${({ theme }) => `
|
AsyncSelectRef,
|
||||||
display: flex;
|
SelectOptionsPagePromise,
|
||||||
justify-content: center;
|
SelectOptionsType,
|
||||||
align-items: flex-start;
|
SelectOptionsTypePage,
|
||||||
width: 100%;
|
} from './types';
|
||||||
padding: ${theme.gridUnit * 2}px;
|
import {
|
||||||
color: ${theme.colors.error.base};
|
StyledCheckOutlined,
|
||||||
& svg {
|
StyledContainer,
|
||||||
margin-right: ${theme.gridUnit * 2}px;
|
StyledError,
|
||||||
}
|
StyledErrorMessage,
|
||||||
`}
|
StyledSelect,
|
||||||
`;
|
StyledStopOutlined,
|
||||||
|
} from './styles';
|
||||||
const StyledErrorMessage = styled.div`
|
import {
|
||||||
overflow: hidden;
|
DEFAULT_PAGE_SIZE,
|
||||||
text-overflow: ellipsis;
|
EMPTY_OPTIONS,
|
||||||
`;
|
MAX_TAG_COUNT,
|
||||||
|
TOKEN_SEPARATORS,
|
||||||
const DEFAULT_PAGE_SIZE = 100;
|
DEFAULT_SORT_COMPARATOR,
|
||||||
|
} from './constants';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Error = ({ error }: { error: string }) => (
|
const Error = ({ error }: { error: string }) => (
|
||||||
<StyledError>
|
<StyledError>
|
||||||
|
@ -16,19 +16,11 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, {
|
import React from 'react';
|
||||||
ReactNode,
|
|
||||||
useState,
|
|
||||||
useCallback,
|
|
||||||
useRef,
|
|
||||||
useMemo,
|
|
||||||
} from 'react';
|
|
||||||
import Button from 'src/components/Button';
|
|
||||||
import ControlHeader from 'src/explore/components/ControlHeader';
|
import ControlHeader from 'src/explore/components/ControlHeader';
|
||||||
import AsyncSelect, { AsyncSelectProps, AsyncSelectRef } from './AsyncSelect';
|
import { SelectOptionsType, SelectProps } from './types';
|
||||||
import { SelectOptionsType, SelectOptionsTypePage } from './utils';
|
|
||||||
|
|
||||||
import Select, { SelectProps } from './Select';
|
import Select from './Select';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Select',
|
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
@ -32,34 +32,27 @@ import {
|
|||||||
getValue,
|
getValue,
|
||||||
hasOption,
|
hasOption,
|
||||||
isLabeledValue,
|
isLabeledValue,
|
||||||
DEFAULT_SORT_COMPARATOR,
|
|
||||||
EMPTY_OPTIONS,
|
|
||||||
MAX_TAG_COUNT,
|
|
||||||
SelectOptionsType,
|
|
||||||
StyledCheckOutlined,
|
|
||||||
StyledStopOutlined,
|
|
||||||
TOKEN_SEPARATORS,
|
|
||||||
renderSelectOptions,
|
renderSelectOptions,
|
||||||
StyledSelect,
|
|
||||||
StyledContainer,
|
|
||||||
hasCustomLabels,
|
hasCustomLabels,
|
||||||
BaseSelectProps,
|
|
||||||
sortSelectedFirstHelper,
|
sortSelectedFirstHelper,
|
||||||
sortComparatorWithSearchHelper,
|
sortComparatorWithSearchHelper,
|
||||||
handleFilterOptionHelper,
|
handleFilterOptionHelper,
|
||||||
dropDownRenderHelper,
|
dropDownRenderHelper,
|
||||||
getSuffixIcon,
|
getSuffixIcon,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
import { SelectOptionsType, SelectProps } from './types';
|
||||||
export interface SelectProps extends BaseSelectProps {
|
import {
|
||||||
/**
|
StyledCheckOutlined,
|
||||||
* It defines the options of the Select.
|
StyledContainer,
|
||||||
* The options can be static, an array of options.
|
StyledSelect,
|
||||||
* The options can also be async, a promise that returns
|
StyledStopOutlined,
|
||||||
* an array of options.
|
} from './styles';
|
||||||
*/
|
import {
|
||||||
options: SelectOptionsType;
|
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
|
* This component is a customized version of the Antdesign 4.X Select component
|
||||||
|
52
superset-frontend/src/components/Select/constants.ts
Normal file
52
superset-frontend/src/components/Select/constants.ts
Normal 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);
|
||||||
|
};
|
90
superset-frontend/src/components/Select/styles.tsx
Normal file
90
superset-frontend/src/components/Select/styles.tsx
Normal 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;
|
||||||
|
`;
|
201
superset-frontend/src/components/Select/types.ts
Normal file
201
superset-frontend/src/components/Select/types.ts
Normal 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;
|
||||||
|
}
|
@ -16,30 +16,12 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { ensureIsArray, styled, t } from '@superset-ui/core';
|
import { ensureIsArray, t } from '@superset-ui/core';
|
||||||
import { Spin } from 'antd';
|
import AntdSelect, { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
|
||||||
import Icons from 'src/components/Icons';
|
import React, { ReactElement, RefObject } from 'react';
|
||||||
import AntdSelect, {
|
|
||||||
SelectProps as AntdSelectProps,
|
|
||||||
SelectValue as AntdSelectValue,
|
|
||||||
LabeledValue as AntdLabeledValue,
|
|
||||||
} from 'antd/lib/select';
|
|
||||||
import { rankedSearchCompare } from 'src/utils/rankedSearchCompare';
|
|
||||||
import {
|
|
||||||
OptionTypeBase,
|
|
||||||
ValueType,
|
|
||||||
OptionsType,
|
|
||||||
GroupedOptionsType,
|
|
||||||
} from 'react-select';
|
|
||||||
import React, {
|
|
||||||
ReactElement,
|
|
||||||
ReactNode,
|
|
||||||
RefObject,
|
|
||||||
JSXElementConstructor,
|
|
||||||
} from 'react';
|
|
||||||
import { DownOutlined, SearchOutlined } from '@ant-design/icons';
|
import { DownOutlined, SearchOutlined } from '@ant-design/icons';
|
||||||
|
import { StyledHelperText, StyledLoadingText, StyledSpin } from './styles';
|
||||||
declare type RawValue = string | number;
|
import { LabeledValue, RawValue, SelectOptionsType, V } from './types';
|
||||||
|
|
||||||
const { Option } = AntdSelect;
|
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 {
|
export function isLabeledValue(value: unknown): value is AntdLabeledValue {
|
||||||
return isObject(value) && 'value' in value && 'label' in value;
|
return isObject(value) && 'value' in value && 'label' in value;
|
||||||
}
|
}
|
||||||
@ -96,10 +43,6 @@ export function getValue(
|
|||||||
return isLabeledValue(option) ? option.value : option;
|
return isLabeledValue(option) ? option.value : option;
|
||||||
}
|
}
|
||||||
|
|
||||||
type V = string | number | null | undefined;
|
|
||||||
|
|
||||||
type LabeledValue = { label?: ReactNode; value?: V };
|
|
||||||
|
|
||||||
export function hasOption(
|
export function hasOption(
|
||||||
value: V,
|
value: V,
|
||||||
options?: V | LabeledValue | (V | LabeledValue)[],
|
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.
|
* It creates a comparator to check for a specific property.
|
||||||
* Can be used with string and number property values.
|
* Can be used with string and number property values.
|
||||||
@ -364,77 +186,6 @@ export const handleFilterOptionHelper = (
|
|||||||
export const hasCustomLabels = (options: SelectOptionsType) =>
|
export const hasCustomLabels = (options: SelectOptionsType) =>
|
||||||
options?.some(opt => !!opt?.customLabel);
|
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) =>
|
export const renderSelectOptions = (options: SelectOptionsType) =>
|
||||||
options.map(opt => {
|
options.map(opt => {
|
||||||
const isOptObject = typeof opt === 'object';
|
const isOptObject = typeof opt === 'object';
|
||||||
|
@ -20,8 +20,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { t, SupersetClient } from '@superset-ui/core';
|
import { t, SupersetClient } from '@superset-ui/core';
|
||||||
import ControlHeader from 'src/explore/components/ControlHeader';
|
import ControlHeader from 'src/explore/components/ControlHeader';
|
||||||
import { Select } from 'src/components';
|
import { Select } from 'src/components';
|
||||||
import { SelectProps } from 'src/components/Select/Select';
|
import { SelectOptionsType, SelectProps } from 'src/components/Select/types';
|
||||||
import { SelectOptionsType } from 'src/components/Select/utils';
|
|
||||||
import { SelectValue, LabeledValue } from 'antd/lib/select';
|
import { SelectValue, LabeledValue } from 'antd/lib/select';
|
||||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||||
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||||
|
Loading…
Reference in New Issue
Block a user