You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2022/09/27 11:38:23 UTC
[superset] branch master updated: refactor: Organizes the Select files (#21589)
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new bb1cf7f145 refactor: Organizes the Select files (#21589)
bb1cf7f145 is described below
commit bb1cf7f1456341fe5e6ca9df47555e312f6bc0f6
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Tue Sep 27 08:38:06 2022 -0300
refactor: Organizes the Select files (#21589)
---
...{Select.stories.tsx => AsyncSelect.stories.tsx} | 225 +-----------------
.../src/components/Select/AsyncSelect.tsx | 94 ++------
.../src/components/Select/Select.stories.tsx | 247 +-------------------
superset-frontend/src/components/Select/Select.tsx | 33 ++-
.../src/components/Select/constants.ts | 52 +++++
superset-frontend/src/components/Select/styles.tsx | 90 +++++++
superset-frontend/src/components/Select/types.ts | 201 ++++++++++++++++
superset-frontend/src/components/Select/utils.tsx | 259 +--------------------
.../controls/SelectAsyncControl/index.tsx | 3 +-
9 files changed, 397 insertions(+), 807 deletions(-)
diff --git a/superset-frontend/src/components/Select/Select.stories.tsx b/superset-frontend/src/components/Select/AsyncSelect.stories.tsx
similarity index 62%
copy from superset-frontend/src/components/Select/Select.stories.tsx
copy to superset-frontend/src/components/Select/AsyncSelect.stories.tsx
index e9a03fe563..547fc7fa99 100644
--- a/superset-frontend/src/components/Select/Select.stories.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.stories.tsx
@@ -24,15 +24,17 @@ import React, {
useMemo,
} from 'react';
import Button from 'src/components/Button';
-import ControlHeader from 'src/explore/components/ControlHeader';
-import AsyncSelect, { AsyncSelectProps, AsyncSelectRef } from './AsyncSelect';
-import { SelectOptionsType, SelectOptionsTypePage } from './utils';
-
-import Select, { SelectProps } from './Select';
+import AsyncSelect from './AsyncSelect';
+import {
+ SelectOptionsType,
+ AsyncSelectProps,
+ AsyncSelectRef,
+ SelectOptionsTypePage,
+} from './types';
export default {
- title: 'Select',
- component: Select,
+ title: 'AsyncSelect',
+ component: AsyncSelect,
};
const DEFAULT_WIDTH = 200;
@@ -63,25 +65,6 @@ const options: SelectOptionsType = [
{ 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' },
- },
-];
-
const ARG_TYPES = {
options: {
defaultValue: options,
@@ -142,196 +125,6 @@ const ARG_TYPES = {
},
};
-const mountHeader = (type: String) => {
- let header;
- if (type === 'text') {
- header = 'Text header';
- } else if (type === 'control') {
- header = (
- <ControlHeader
- label="Control header"
- warning="Example of warning messsage"
- />
- );
- }
- return header;
-};
-
-const generateOptions = (opts: SelectOptionsType, count: number) => {
- let generated = opts.slice();
- let iteration = 0;
- while (generated.length < count) {
- iteration += 1;
- generated = generated.concat(
- // eslint-disable-next-line no-loop-func
- generated.map(({ label, value }) => ({
- label: `${label} ${iteration}`,
- value: `${value} ${iteration}`,
- })),
- );
- }
- return generated.slice(0, count);
-};
-
-export const InteractiveSelect = ({
- header,
- options,
- optionsCount,
- ...args
-}: SelectProps & { header: string; optionsCount: number }) => (
- <div
- style={{
- width: DEFAULT_WIDTH,
- }}
- >
- <Select
- {...args}
- options={
- Array.isArray(options)
- ? generateOptions(options, optionsCount)
- : options
- }
- header={mountHeader(header)}
- />
- </div>
-);
-
-InteractiveSelect.args = {
- autoFocus: true,
- allowNewOptions: false,
- allowClear: false,
- showSearch: true,
- disabled: false,
- invertSelection: false,
- placeholder: 'Select ...',
- optionFilterProps: ['value', 'label', 'custom'],
-};
-
-InteractiveSelect.argTypes = {
- ...ARG_TYPES,
- optionsCount: {
- defaultValue: options.length,
- control: {
- type: 'number',
- },
- },
- header: {
- defaultValue: 'none',
- description: `It adds a header on top of the Select. Can be any ReactNode.`,
- control: { type: 'inline-radio', options: ['none', 'text', 'control'] },
- },
- pageSize: {
- description: `It defines how many results should be included in the query response.
- Works in async mode only (See the options property).
- `,
- },
- fetchOnlyOnSearch: {
- description: `It fires a request against the server only after searching.
- Works in async mode only (See the options property).
- Undefined by default.
- `,
- },
-};
-
-InteractiveSelect.story = {
- parameters: {
- knobs: {
- disable: true,
- },
- },
-};
-
-export const AtEveryCorner = () => (
- <>
- {selectPositions.map(position => (
- <div
- key={position.id}
- style={{
- ...position.style,
- margin: 30,
- width: DEFAULT_WIDTH,
- position: 'absolute',
- }}
- >
- <Select
- ariaLabel={`gallery-${position.id}`}
- options={options}
- labelInValue
- />
- </div>
- ))}
- <p style={{ position: 'absolute', top: '40%', left: '33%', width: 500 }}>
- The objective of this panel is to show how the Select behaves when in
- touch with the viewport extremities. In particular, how the drop-down is
- displayed and if the tooltips of truncated items are correctly positioned.
- </p>
- </>
-);
-
-AtEveryCorner.story = {
- parameters: {
- actions: {
- disable: true,
- },
- controls: {
- disable: true,
- },
- knobs: {
- disable: true,
- },
- },
-};
-
-export const PageScroll = () => (
- <div style={{ height: 2000, overflowY: 'auto' }}>
- <div
- style={{
- width: DEFAULT_WIDTH,
- position: 'absolute',
- top: 30,
- right: 30,
- }}
- >
- <Select ariaLabel="page-scroll-select-1" options={options} labelInValue />
- </div>
- <div
- style={{
- width: DEFAULT_WIDTH,
- position: 'absolute',
- bottom: 30,
- right: 30,
- }}
- >
- <Select ariaLabel="page-scroll-select-2" options={options} />
- </div>
- <p
- style={{
- position: 'absolute',
- top: '40%',
- left: 30,
- width: 500,
- }}
- >
- The objective of this panel is to show how the Select behaves when there's
- a scroll on the page. In particular, how the drop-down is displayed.
- </p>
- </div>
-);
-
-PageScroll.story = {
- parameters: {
- actions: {
- disable: true,
- },
- controls: {
- disable: true,
- },
- knobs: {
- disable: true,
- },
- },
-};
-
const USERS = [
'John',
'Liam',
diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx b/superset-frontend/src/components/Select/AsyncSelect.tsx
index 27c62023ce..524ca53e61 100644
--- a/superset-frontend/src/components/Select/AsyncSelect.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.tsx
@@ -28,7 +28,7 @@ import React, {
useCallback,
useImperativeHandle,
} from 'react';
-import { ensureIsArray, styled, t } from '@superset-ui/core';
+import { ensureIsArray, t } from '@superset-ui/core';
import { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
import debounce from 'lodash/debounce';
import { isEqual } from 'lodash';
@@ -39,20 +39,8 @@ import {
getValue,
hasOption,
isLabeledValue,
- DEFAULT_SORT_COMPARATOR,
- EMPTY_OPTIONS,
- MAX_TAG_COUNT,
- SelectOptionsPagePromise,
- SelectOptionsType,
- SelectOptionsTypePage,
- StyledCheckOutlined,
- StyledStopOutlined,
- TOKEN_SEPARATORS,
renderSelectOptions,
- StyledContainer,
- StyledSelect,
hasCustomLabels,
- BaseSelectProps,
sortSelectedFirstHelper,
sortComparatorWithSearchHelper,
sortComparatorForNoSearchHelper,
@@ -60,64 +48,28 @@ import {
dropDownRenderHelper,
handleFilterOptionHelper,
} from './utils';
-
-const StyledError = styled.div`
- ${({ theme }) => `
- display: flex;
- justify-content: center;
- align-items: flex-start;
- width: 100%;
- padding: ${theme.gridUnit * 2}px;
- color: ${theme.colors.error.base};
- & svg {
- margin-right: ${theme.gridUnit * 2}px;
- }
- `}
-`;
-
-const StyledErrorMessage = styled.div`
- overflow: hidden;
- text-overflow: ellipsis;
-`;
-
-const DEFAULT_PAGE_SIZE = 100;
-
-export type AsyncSelectRef = HTMLInputElement & { clearCache: () => void };
-
-export interface AsyncSelectProps extends BaseSelectProps {
- /**
- * It fires a request against the server after
- * the first interaction and not on render.
- * Works in async mode only (See the options property).
- * True by default.
- */
- lazyLoading?: boolean;
- /**
- * It defines the options of the Select.
- * The options are async, a promise that returns
- * an array of options.
- */
- options: SelectOptionsPagePromise;
- /**
- * It defines how many results should be included
- * in the query response.
- * Works in async mode only (See the options property).
- */
- pageSize?: number;
- /**
- * It fires a request against the server only after
- * searching.
- * Works in async mode only (See the options property).
- * Undefined by default.
- */
- fetchOnlyOnSearch?: boolean;
- /**
- * It provides a callback function when an error
- * is generated after a request is fired.
- * Works in async mode only (See the options property).
- */
- onError?: (error: string) => void;
-}
+import {
+ AsyncSelectProps,
+ AsyncSelectRef,
+ SelectOptionsPagePromise,
+ SelectOptionsType,
+ SelectOptionsTypePage,
+} from './types';
+import {
+ StyledCheckOutlined,
+ StyledContainer,
+ StyledError,
+ StyledErrorMessage,
+ StyledSelect,
+ StyledStopOutlined,
+} from './styles';
+import {
+ DEFAULT_PAGE_SIZE,
+ EMPTY_OPTIONS,
+ MAX_TAG_COUNT,
+ TOKEN_SEPARATORS,
+ DEFAULT_SORT_COMPARATOR,
+} from './constants';
const Error = ({ error }: { error: string }) => (
<StyledError>
diff --git a/superset-frontend/src/components/Select/Select.stories.tsx b/superset-frontend/src/components/Select/Select.stories.tsx
index e9a03fe563..c4802a45e0 100644
--- a/superset-frontend/src/components/Select/Select.stories.tsx
+++ b/superset-frontend/src/components/Select/Select.stories.tsx
@@ -16,19 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, {
- ReactNode,
- useState,
- useCallback,
- useRef,
- useMemo,
-} from 'react';
-import Button from 'src/components/Button';
+import React from 'react';
import ControlHeader from 'src/explore/components/ControlHeader';
-import AsyncSelect, { AsyncSelectProps, AsyncSelectRef } from './AsyncSelect';
-import { SelectOptionsType, SelectOptionsTypePage } from './utils';
+import { SelectOptionsType, SelectProps } from './types';
-import Select, { SelectProps } from './Select';
+import Select from './Select';
export default {
title: 'Select',
@@ -331,236 +323,3 @@ PageScroll.story = {
},
},
};
-
-const USERS = [
- 'John',
- 'Liam',
- 'Olivia',
- 'Emma',
- 'Noah',
- 'Ava',
- 'Oliver',
- 'Elijah',
- 'Charlotte',
- 'Diego',
- 'Evan',
- 'Michael',
- 'Giovanni',
- 'Luca',
- 'Paolo',
- 'Francesca',
- 'Chiara',
- 'Sara',
- 'Valentina',
- 'Jessica',
- 'Angelica',
- 'Mario',
- 'Marco',
- 'Andrea',
- 'Luigi',
- 'Quarto',
- 'Quinto',
- 'Sesto',
- 'Franco',
- 'Sandro',
- 'Alehandro',
- 'Johnny',
- 'Nikole',
- 'Igor',
- 'Sipatha',
- 'Thami',
- 'Munei',
- 'Guilherme',
- 'Umair',
- 'Ashfaq',
- 'Amna',
- 'Irfan',
- 'George',
- 'Naseer',
- 'Mohammad',
- 'Rick',
- 'Saliya',
- 'Claire',
- 'Benedetta',
- 'Ilenia',
-].sort();
-
-export const AsynchronousSelect = ({
- fetchOnlyOnSearch,
- withError,
- withInitialValue,
- responseTime,
- ...rest
-}: AsyncSelectProps & {
- withError: boolean;
- withInitialValue: boolean;
- responseTime: number;
-}) => {
- const [requests, setRequests] = useState<ReactNode[]>([]);
- const ref = useRef<AsyncSelectRef>(null);
-
- const getResults = (username?: string) => {
- let results: { label: string; value: string }[] = [];
-
- if (!username) {
- results = USERS.map(u => ({
- label: u,
- value: u,
- }));
- } else {
- const foundUsers = USERS.filter(u => u.toLowerCase().includes(username));
- if (foundUsers) {
- results = foundUsers.map(u => ({ label: u, value: u }));
- } else {
- results = [];
- }
- }
- return results;
- };
-
- const setRequestLog = (results: number, total: number, username?: string) => {
- const request = (
- <>
- Emulating network request with search <b>{username || 'empty'}</b> ...{' '}
- <b>
- {results}/{total}
- </b>{' '}
- results
- </>
- );
-
- setRequests(requests => [request, ...requests]);
- };
-
- const fetchUserListPage = useCallback(
- (
- search: string,
- page: number,
- pageSize: number,
- ): Promise<SelectOptionsTypePage> => {
- const username = search.trim().toLowerCase();
- return new Promise(resolve => {
- let results = getResults(username);
- const totalCount = results.length;
- const start = page * pageSize;
- const deleteCount =
- start + pageSize < totalCount ? pageSize : totalCount - start;
- results = results.splice(start, deleteCount);
- setRequestLog(start + results.length, totalCount, username);
- setTimeout(() => {
- resolve({ data: results, totalCount });
- }, responseTime * 1000);
- });
- },
- [responseTime],
- );
-
- const fetchUserListError = async (): Promise<SelectOptionsTypePage> =>
- new Promise((_, reject) => {
- reject(new Error('Error while fetching the names from the server'));
- });
-
- const initialValue = useMemo(
- () => ({ label: 'Valentina', value: 'Valentina' }),
- [],
- );
-
- return (
- <>
- <div
- style={{
- width: DEFAULT_WIDTH,
- }}
- >
- <AsyncSelect
- {...rest}
- ref={ref}
- fetchOnlyOnSearch={fetchOnlyOnSearch}
- options={withError ? fetchUserListError : fetchUserListPage}
- placeholder={fetchOnlyOnSearch ? 'Type anything' : 'AsyncSelect...'}
- value={withInitialValue ? initialValue : undefined}
- />
- </div>
- <div
- style={{
- position: 'absolute',
- top: 32,
- left: DEFAULT_WIDTH + 100,
- height: 400,
- width: 600,
- overflowY: 'auto',
- border: '1px solid #d9d9d9',
- padding: 20,
- }}
- >
- {requests.map((request, index) => (
- <p key={`request-${index}`}>{request}</p>
- ))}
- </div>
- <Button
- style={{
- position: 'absolute',
- top: 452,
- left: DEFAULT_WIDTH + 580,
- }}
- onClick={() => {
- ref.current?.clearCache();
- setRequests([]);
- }}
- >
- Clear cache
- </Button>
- </>
- );
-};
-
-AsynchronousSelect.args = {
- allowClear: false,
- allowNewOptions: false,
- fetchOnlyOnSearch: false,
- pageSize: 10,
- withError: false,
- withInitialValue: false,
- tokenSeparators: ['\n', '\t', ';'],
-};
-
-AsynchronousSelect.argTypes = {
- ...ARG_TYPES,
- header: {
- table: {
- disable: true,
- },
- },
- invertSelection: {
- table: {
- disable: true,
- },
- },
- pageSize: {
- defaultValue: 10,
- control: {
- type: 'range',
- min: 10,
- max: 50,
- step: 10,
- },
- },
- responseTime: {
- defaultValue: 0.5,
- name: 'responseTime (seconds)',
- control: {
- type: 'range',
- min: 0.5,
- max: 5,
- step: 0.5,
- },
- },
-};
-
-AsynchronousSelect.story = {
- parameters: {
- knobs: {
- disable: true,
- },
- },
-};
diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx
index 6b6713852d..1ad1e6b0a2 100644
--- a/superset-frontend/src/components/Select/Select.tsx
+++ b/superset-frontend/src/components/Select/Select.tsx
@@ -32,34 +32,27 @@ import {
getValue,
hasOption,
isLabeledValue,
- DEFAULT_SORT_COMPARATOR,
- EMPTY_OPTIONS,
- MAX_TAG_COUNT,
- SelectOptionsType,
- StyledCheckOutlined,
- StyledStopOutlined,
- TOKEN_SEPARATORS,
renderSelectOptions,
- StyledSelect,
- StyledContainer,
hasCustomLabels,
- BaseSelectProps,
sortSelectedFirstHelper,
sortComparatorWithSearchHelper,
handleFilterOptionHelper,
dropDownRenderHelper,
getSuffixIcon,
} from './utils';
-
-export interface SelectProps extends BaseSelectProps {
- /**
- * It defines the options of the Select.
- * The options can be static, an array of options.
- * The options can also be async, a promise that returns
- * an array of options.
- */
- options: SelectOptionsType;
-}
+import { SelectOptionsType, SelectProps } from './types';
+import {
+ StyledCheckOutlined,
+ StyledContainer,
+ StyledSelect,
+ StyledStopOutlined,
+} from './styles';
+import {
+ EMPTY_OPTIONS,
+ MAX_TAG_COUNT,
+ TOKEN_SEPARATORS,
+ DEFAULT_SORT_COMPARATOR,
+} from './constants';
/**
* This component is a customized version of the Antdesign 4.X Select component
diff --git a/superset-frontend/src/components/Select/constants.ts b/superset-frontend/src/components/Select/constants.ts
new file mode 100644
index 0000000000..b8c60e8523
--- /dev/null
+++ b/superset-frontend/src/components/Select/constants.ts
@@ -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);
+};
diff --git a/superset-frontend/src/components/Select/styles.tsx b/superset-frontend/src/components/Select/styles.tsx
new file mode 100644
index 0000000000..85dbefe88f
--- /dev/null
+++ b/superset-frontend/src/components/Select/styles.tsx
@@ -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;
+`;
diff --git a/superset-frontend/src/components/Select/types.ts b/superset-frontend/src/components/Select/types.ts
new file mode 100644
index 0000000000..e2a7d5d1f3
--- /dev/null
+++ b/superset-frontend/src/components/Select/types.ts
@@ -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;
+}
diff --git a/superset-frontend/src/components/Select/utils.tsx b/superset-frontend/src/components/Select/utils.tsx
index 27a97ac8bd..5ec7e33d10 100644
--- a/superset-frontend/src/components/Select/utils.tsx
+++ b/superset-frontend/src/components/Select/utils.tsx
@@ -16,30 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { ensureIsArray, styled, t } from '@superset-ui/core';
-import { Spin } from 'antd';
-import Icons from 'src/components/Icons';
-import AntdSelect, {
- SelectProps as AntdSelectProps,
- SelectValue as AntdSelectValue,
- LabeledValue as AntdLabeledValue,
-} from 'antd/lib/select';
-import { rankedSearchCompare } from 'src/utils/rankedSearchCompare';
-import {
- OptionTypeBase,
- ValueType,
- OptionsType,
- GroupedOptionsType,
-} from 'react-select';
-import React, {
- ReactElement,
- ReactNode,
- RefObject,
- JSXElementConstructor,
-} from 'react';
+import { ensureIsArray, t } from '@superset-ui/core';
+import AntdSelect, { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
+import React, { ReactElement, RefObject } from 'react';
import { DownOutlined, SearchOutlined } from '@ant-design/icons';
-
-declare type RawValue = string | number;
+import { StyledHelperText, StyledLoadingText, StyledSpin } from './styles';
+import { LabeledValue, RawValue, SelectOptionsType, V } from './types';
const { Option } = AntdSelect;
@@ -51,41 +33,6 @@ export function isObject(value: unknown): value is Record<string, unknown> {
);
}
-/**
- * Find Option value that matches a possibly string value.
- *
- * Translate possible string values to `OptionType` objects, fallback to value
- * itself if cannot be found in the options list.
- *
- * Always returns an array.
- */
-export function findValue<OptionType extends OptionTypeBase>(
- value: ValueType<OptionType> | string,
- options: GroupedOptionsType<OptionType> | OptionsType<OptionType> = [],
- valueKey = 'value',
-): OptionType[] {
- if (value === null || value === undefined || value === '') {
- return [];
- }
- const isGroup = Array.isArray((options[0] || {}).options);
- const flatOptions = isGroup
- ? (options as GroupedOptionsType<OptionType>).flatMap(x => x.options || [])
- : (options as OptionsType<OptionType>);
-
- const find = (val: OptionType) => {
- const realVal = (value || {}).hasOwnProperty(valueKey)
- ? val[valueKey]
- : val;
- return (
- flatOptions.find(x => x === realVal || x[valueKey] === realVal) || val
- );
- };
-
- // If value is a single string, must return an Array so `cleanValue` won't be
- // empty: https://github.com/JedWatson/react-select/blob/32ad5c040bdd96cd1ca71010c2558842d684629c/packages/react-select/src/utils.js#L64
- return (Array.isArray(value) ? value : [value]).map(find);
-}
-
export function isLabeledValue(value: unknown): value is AntdLabeledValue {
return isObject(value) && 'value' in value && 'label' in value;
}
@@ -96,10 +43,6 @@ export function getValue(
return isLabeledValue(option) ? option.value : option;
}
-type V = string | number | null | undefined;
-
-type LabeledValue = { label?: ReactNode; value?: V };
-
export function hasOption(
value: V,
options?: V | LabeledValue | (V | LabeledValue)[],
@@ -121,127 +64,6 @@ export function hasOption(
);
}
-export type AntdProps = AntdSelectProps<AntdSelectValue>;
-
-export type AntdExposedProps = Pick<
- AntdProps,
- | 'allowClear'
- | 'autoFocus'
- | 'disabled'
- | 'filterOption'
- | 'filterSort'
- | 'loading'
- | 'labelInValue'
- | 'maxTagCount'
- | 'notFoundContent'
- | 'onChange'
- | 'onClear'
- | 'onDeselect'
- | 'onSelect'
- | 'onFocus'
- | 'onBlur'
- | 'onPopupScroll'
- | 'onSearch'
- | 'onDropdownVisibleChange'
- | 'placeholder'
- | 'showArrow'
- | 'showSearch'
- | 'tokenSeparators'
- | 'value'
- | 'getPopupContainer'
- | 'menuItemSelectedIcon'
->;
-
-export type SelectOptionsType = Exclude<AntdProps['options'], undefined>;
-
-export type SelectOptionsTypePage = {
- data: SelectOptionsType;
- totalCount: number;
-};
-
-export type SelectOptionsPagePromise = (
- search: string,
- page: number,
- pageSize: number,
-) => Promise<SelectOptionsTypePage>;
-
-export const StyledContainer = styled.div`
- display: flex;
- flex-direction: column;
- width: 100%;
-`;
-
-export const StyledSelect = styled(AntdSelect)`
- ${({ theme }) => `
- && .ant-select-selector {
- border-radius: ${theme.gridUnit}px;
- }
- // Open the dropdown when clicking on the suffix
- // This is fixed in version 4.16
- .ant-select-arrow .anticon:not(.ant-select-suffix) {
- pointer-events: none;
- }
- `}
-`;
-
-export const StyledStopOutlined = styled(Icons.StopOutlined)`
- vertical-align: 0;
-`;
-
-export const StyledCheckOutlined = styled(Icons.CheckOutlined)`
- vertical-align: 0;
-`;
-
-export const StyledSpin = styled(Spin)`
- margin-top: ${({ theme }) => -theme.gridUnit}px;
-`;
-
-export const StyledLoadingText = styled.div`
- ${({ theme }) => `
- margin-left: ${theme.gridUnit * 3}px;
- line-height: ${theme.gridUnit * 8}px;
- color: ${theme.colors.grayscale.light1};
- `}
-`;
-
-const StyledHelperText = styled.div`
- ${({ theme }) => `
- padding: ${theme.gridUnit * 2}px ${theme.gridUnit * 3}px;
- color: ${theme.colors.grayscale.base};
- font-size: ${theme.typography.sizes.s}px;
- cursor: default;
- border-bottom: 1px solid ${theme.colors.grayscale.light2};
- `}
-`;
-
-export const MAX_TAG_COUNT = 4;
-export const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
-export const EMPTY_OPTIONS: SelectOptionsType = [];
-
-export const DEFAULT_SORT_COMPARATOR = (
- a: AntdLabeledValue,
- b: AntdLabeledValue,
- search?: string,
-) => {
- let aText: string | undefined;
- let bText: string | undefined;
- if (typeof a.label === 'string' && typeof b.label === 'string') {
- aText = a.label;
- bText = b.label;
- } else if (typeof a.value === 'string' && typeof b.value === 'string') {
- aText = a.value;
- bText = b.value;
- }
- // sort selected options first
- if (typeof aText === 'string' && typeof bText === 'string') {
- if (search) {
- return rankedSearchCompare(aText, bText, search);
- }
- return aText.localeCompare(bText);
- }
- return (a.value as number) - (b.value as number);
-};
-
/**
* It creates a comparator to check for a specific property.
* Can be used with string and number property values.
@@ -364,77 +186,6 @@ export const handleFilterOptionHelper = (
export const hasCustomLabels = (options: SelectOptionsType) =>
options?.some(opt => !!opt?.customLabel);
-export interface BaseSelectProps extends AntdExposedProps {
- /**
- * It enables the user to create new options.
- * Can be used with standard or async select types.
- * Can be used with any mode, single or multiple.
- * False by default.
- * */
- allowNewOptions?: boolean;
- /**
- * It adds the aria-label tag for accessibility standards.
- * Must be plain English and localized.
- */
- ariaLabel?: string;
- /**
- * Renders the dropdown
- */
- dropdownRender?: (
- menu: ReactElement<any, string | JSXElementConstructor<any>>,
- ) => ReactElement<any, string | JSXElementConstructor<any>>;
- /**
- * It adds a header on top of the Select.
- * Can be any ReactNode.
- */
- header?: ReactNode;
- /**
- * It adds a helper text on top of the Select options
- * with additional context to help with the interaction.
- */
- helperText?: string;
- /**
- * It allows to define which properties of the option object
- * should be looked for when searching.
- * By default label and value.
- */
- mappedMode?: 'multiple' | 'tags';
- /**
- * It defines whether the Select should allow for the
- * selection of multiple options or single.
- * Single by default.
- */
- mode?: 'single' | 'multiple';
- /**
- * Deprecated.
- * Prefer ariaLabel instead.
- */
- name?: string; // discourage usage
- /**
- * It allows to define which properties of the option object
- * should be looked for when searching.
- * By default label and value.
- */
- optionFilterProps?: string[];
- /**
- * It shows a stop-outlined icon at the far right of a selected
- * option instead of the default checkmark.
- * Useful to better indicate to the user that by clicking on a selected
- * option it will be de-selected.
- * False by default.
- */
- invertSelection?: boolean;
- /**
- * Customize how filtered options are sorted while users search.
- * Will not apply to predefined `options` array when users are not searching.
- */
- sortComparator?: typeof DEFAULT_SORT_COMPARATOR;
-
- suffixIcon?: ReactNode;
-
- ref: RefObject<HTMLInputElement>;
-}
-
export const renderSelectOptions = (options: SelectOptionsType) =>
options.map(opt => {
const isOptObject = typeof opt === 'object';
diff --git a/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx b/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx
index 74364fcf98..ddc242a764 100644
--- a/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx
+++ b/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx
@@ -20,8 +20,7 @@ import React, { useEffect, useState } from 'react';
import { t, SupersetClient } from '@superset-ui/core';
import ControlHeader from 'src/explore/components/ControlHeader';
import { Select } from 'src/components';
-import { SelectProps } from 'src/components/Select/Select';
-import { SelectOptionsType } from 'src/components/Select/utils';
+import { SelectOptionsType, SelectProps } from 'src/components/Select/types';
import { SelectValue, LabeledValue } from 'antd/lib/select';
import withToasts from 'src/components/MessageToasts/withToasts';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';