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 2023/01/18 12:42:11 UTC
[superset] branch master updated: feat: Select all for synchronous select (#22084)
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 02c9242d68 feat: Select all for synchronous select (#22084)
02c9242d68 is described below
commit 02c9242d680a67dca18ae05b2ca585c0cf385ad0
Author: cccs-RyanK <10...@users.noreply.github.com>
AuthorDate: Wed Jan 18 07:41:58 2023 -0500
feat: Select all for synchronous select (#22084)
Co-authored-by: GITHUB_USERNAME <EMAIL>
---
.../integration/dashboard/nativeFilters.test.ts | 4 +-
.../src/components/Select/AsyncSelect.tsx | 4 +-
.../src/components/Select/CustomTag.tsx | 21 +-
.../src/components/Select/Select.stories.tsx | 8 +
.../src/components/Select/Select.test.tsx | 225 ++++++++++++++++++++-
superset-frontend/src/components/Select/Select.tsx | 191 +++++++++++++++--
superset-frontend/src/components/Select/styles.tsx | 14 +-
superset-frontend/src/components/Select/types.ts | 3 +-
superset-frontend/src/components/Select/utils.tsx | 6 +
.../components/Select/SelectFilterPlugin.tsx | 3 -
10 files changed, 429 insertions(+), 50 deletions(-)
diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts
index 365a7e4ecb..ff6939e9af 100644
--- a/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts
+++ b/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts
@@ -404,9 +404,9 @@ describe('Horizontal FilterBar', () => {
saveNativeFilterSettings([SAMPLE_CHART]);
cy.getBySel('filter-bar').within(() => {
cy.get(nativeFilters.filterItem).contains('Albania').should('be.visible');
- cy.get(nativeFilters.filterItem).contains('+1').should('be.visible');
+ cy.get(nativeFilters.filterItem).contains('+ 1 ...').should('be.visible');
cy.get('.ant-select-selection-search-input').click();
- cy.get(nativeFilters.filterItem).contains('+2').should('be.visible');
+ cy.get(nativeFilters.filterItem).contains('+ 2 ...').should('be.visible');
});
});
});
diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx b/superset-frontend/src/components/Select/AsyncSelect.tsx
index 4863095467..1e1307a588 100644
--- a/superset-frontend/src/components/Select/AsyncSelect.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.tsx
@@ -71,7 +71,7 @@ import {
TOKEN_SEPARATORS,
DEFAULT_SORT_COMPARATOR,
} from './constants';
-import { oneLineTagRender } from './CustomTag';
+import { customTagRender } from './CustomTag';
const Error = ({ error }: { error: string }) => (
<StyledError>
@@ -517,7 +517,7 @@ const AsyncSelect = forwardRef(
)
}
oneLine={oneLine}
- tagRender={oneLine ? oneLineTagRender : undefined}
+ tagRender={customTagRender}
{...props}
ref={ref}
>
diff --git a/superset-frontend/src/components/Select/CustomTag.tsx b/superset-frontend/src/components/Select/CustomTag.tsx
index 57aa37c81b..a7ffe10f6d 100644
--- a/superset-frontend/src/components/Select/CustomTag.tsx
+++ b/superset-frontend/src/components/Select/CustomTag.tsx
@@ -22,6 +22,8 @@ import { styled } from '@superset-ui/core';
import { useCSSTextTruncation } from 'src/hooks/useTruncation';
import { Tooltip } from '../Tooltip';
import { CustomTagProps } from './types';
+import { SELECT_ALL_VALUE } from './utils';
+import { NoElement } from './styles';
const StyledTag = styled(AntdTag)`
& .ant-tag-close-icon {
@@ -51,10 +53,10 @@ const Tag = (props: any) => {
};
/**
- * Custom tag renderer dedicated for oneLine mode
+ * Custom tag renderer
*/
-export const oneLineTagRender = (props: CustomTagProps) => {
- const { label } = props;
+export const customTagRender = (props: CustomTagProps) => {
+ const { label, value } = props;
const onPreventMouseDown = (event: React.MouseEvent<HTMLElement>) => {
// if close icon is clicked, stop propagation to avoid opening the dropdown
@@ -69,9 +71,12 @@ export const oneLineTagRender = (props: CustomTagProps) => {
}
};
- return (
- <Tag onMouseDown={onPreventMouseDown} {...props}>
- {label}
- </Tag>
- );
+ if (value !== SELECT_ALL_VALUE) {
+ return (
+ <Tag onMouseDown={onPreventMouseDown} {...(props as object)}>
+ {label}
+ </Tag>
+ );
+ }
+ return <NoElement />;
};
diff --git a/superset-frontend/src/components/Select/Select.stories.tsx b/superset-frontend/src/components/Select/Select.stories.tsx
index 4a5d3551f0..6c774fe169 100644
--- a/superset-frontend/src/components/Select/Select.stories.tsx
+++ b/superset-frontend/src/components/Select/Select.stories.tsx
@@ -141,6 +141,13 @@ const ARG_TYPES = {
Requires '"mode=multiple"'.
`,
},
+ maxTagCount: {
+ defaultValue: 4,
+ description: `Sets maxTagCount attribute. The overflow tag is displayed in
+ place of the remaining items.
+ Requires '"mode=multiple"'.
+ `,
+ },
};
const mountHeader = (type: String) => {
@@ -207,6 +214,7 @@ InteractiveSelect.args = {
placeholder: 'Select ...',
optionFilterProps: ['value', 'label', 'custom'],
oneLine: false,
+ maxTagCount: 4,
};
InteractiveSelect.argTypes = {
diff --git a/superset-frontend/src/components/Select/Select.test.tsx b/superset-frontend/src/components/Select/Select.test.tsx
index cb4548633e..52f834d7cd 100644
--- a/superset-frontend/src/components/Select/Select.test.tsx
+++ b/superset-frontend/src/components/Select/Select.test.tsx
@@ -19,7 +19,8 @@
import React from 'react';
import { render, screen, waitFor, within } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
-import { Select } from 'src/components';
+import Select from 'src/components/Select/Select';
+import { SELECT_ALL_VALUE } from './utils';
const ARIA_LABEL = 'Test';
const NEW_OPTION = 'Kyle';
@@ -64,6 +65,9 @@ const defaultProps = {
showSearch: true,
};
+const selectAllOptionLabel = (numOptions: number) =>
+ `${String(SELECT_ALL_VALUE)} (${numOptions})`;
+
const getElementByClassName = (className: string) =>
document.querySelector(className)! as HTMLElement;
@@ -89,7 +93,12 @@ const findSelectValue = () =>
waitFor(() => getElementByClassName('.ant-select-selection-item'));
const findAllSelectValues = () =>
- waitFor(() => getElementsByClassName('.ant-select-selection-item'));
+ waitFor(() => [...getElementsByClassName('.ant-select-selection-item')]);
+
+const findAllCheckedValues = () =>
+ waitFor(() => [
+ ...getElementsByClassName('.ant-select-item-option-selected'),
+ ]);
const clearAll = () => userEvent.click(screen.getByLabelText('close-circle'));
@@ -209,26 +218,37 @@ test('should sort selected to the top when in multi mode', async () => {
let labels = originalLabels.slice();
await open();
- userEvent.click(await findSelectOption(labels[1]));
- expect(await matchOrder(labels)).toBe(true);
+ userEvent.click(await findSelectOption(labels[2]));
+ expect(
+ await matchOrder([selectAllOptionLabel(originalLabels.length), ...labels]),
+ ).toBe(true);
await type('{esc}');
await open();
- labels = labels.splice(1, 1).concat(labels);
- expect(await matchOrder(labels)).toBe(true);
+ labels = labels.splice(2, 1).concat(labels);
+ expect(
+ await matchOrder([selectAllOptionLabel(originalLabels.length), ...labels]),
+ ).toBe(true);
await open();
userEvent.click(await findSelectOption(labels[5]));
await type('{esc}');
await open();
labels = [labels.splice(0, 1)[0], labels.splice(4, 1)[0]].concat(labels);
- expect(await matchOrder(labels)).toBe(true);
+ expect(
+ await matchOrder([selectAllOptionLabel(originalLabels.length), ...labels]),
+ ).toBe(true);
// should revert to original order
clearAll();
await type('{esc}');
await open();
- expect(await matchOrder(originalLabels)).toBe(true);
+ expect(
+ await matchOrder([
+ selectAllOptionLabel(originalLabels.length),
+ ...originalLabels,
+ ]),
+ ).toBe(true);
});
test('searches for label or value', async () => {
@@ -440,7 +460,7 @@ test('changes the selected item in single mode', async () => {
label: firstOption.label,
value: firstOption.value,
}),
- firstOption,
+ expect.objectContaining(firstOption),
);
userEvent.click(await findSelectOption(secondOption.label));
expect(onChange).toHaveBeenCalledWith(
@@ -448,7 +468,7 @@ test('changes the selected item in single mode', async () => {
label: secondOption.label,
value: secondOption.value,
}),
- secondOption,
+ expect.objectContaining(secondOption),
);
expect(await findSelectValue()).toHaveTextContent(secondOption.label);
});
@@ -566,6 +586,136 @@ test('finds an element with a numeric value and does not duplicate the options',
expect(await querySelectOption('11')).not.toBeInTheDocument();
});
+test('render "Select all" for multi select', async () => {
+ render(<Select {...defaultProps} mode="multiple" options={OPTIONS} />);
+ await open();
+ const options = await findAllSelectOptions();
+ expect(options[0]).toHaveTextContent(selectAllOptionLabel(OPTIONS.length));
+});
+
+test('does not render "Select all" for single select', async () => {
+ render(<Select {...defaultProps} options={OPTIONS} mode="single" />);
+ await open();
+ expect(
+ screen.queryByText(selectAllOptionLabel(OPTIONS.length)),
+ ).not.toBeInTheDocument();
+});
+
+test('does not render "Select all" for an empty multiple select', async () => {
+ render(<Select {...defaultProps} options={[]} mode="multiple" />);
+ await open();
+ expect(
+ screen.queryByText(selectAllOptionLabel(OPTIONS.length)),
+ ).not.toBeInTheDocument();
+});
+
+test('does not render "Select all" when searching', async () => {
+ render(<Select {...defaultProps} options={OPTIONS} mode="multiple" />);
+ await open();
+ await type('Select');
+ expect(
+ screen.queryByText(selectAllOptionLabel(OPTIONS.length)),
+ ).not.toBeInTheDocument();
+});
+
+test('does not render "Select all" as one of the tags after selection', async () => {
+ render(<Select {...defaultProps} options={OPTIONS} mode="multiple" />);
+ await open();
+ userEvent.click(await findSelectOption(selectAllOptionLabel(OPTIONS.length)));
+ const values = await findAllSelectValues();
+ expect(values[0]).not.toHaveTextContent(selectAllOptionLabel(OPTIONS.length));
+});
+
+test('keeps "Select all" at the top after a selection', async () => {
+ const selected = OPTIONS[2];
+ render(
+ <Select
+ {...defaultProps}
+ options={OPTIONS.slice(0, 10)}
+ mode="multiple"
+ value={[selected]}
+ />,
+ );
+ await open();
+ const options = await findAllSelectOptions();
+ expect(options[0]).toHaveTextContent(selectAllOptionLabel(10));
+ expect(options[1]).toHaveTextContent(selected.label);
+});
+
+test('selects all values', async () => {
+ render(
+ <Select
+ {...defaultProps}
+ options={OPTIONS}
+ mode="multiple"
+ maxTagCount={0}
+ />,
+ );
+ await open();
+ userEvent.click(await findSelectOption(selectAllOptionLabel(OPTIONS.length)));
+ const values = await findAllSelectValues();
+ expect(values.length).toBe(1);
+ expect(values[0]).toHaveTextContent(`+ ${OPTIONS.length} ...`);
+});
+
+test('unselects all values', async () => {
+ render(
+ <Select
+ {...defaultProps}
+ options={OPTIONS}
+ mode="multiple"
+ maxTagCount={0}
+ />,
+ );
+ await open();
+ userEvent.click(await findSelectOption(selectAllOptionLabel(OPTIONS.length)));
+ let values = await findAllSelectValues();
+ expect(values.length).toBe(1);
+ expect(values[0]).toHaveTextContent(`+ ${OPTIONS.length} ...`);
+ userEvent.click(await findSelectOption(selectAllOptionLabel(OPTIONS.length)));
+ values = await findAllSelectValues();
+ expect(values.length).toBe(0);
+});
+
+test('deselecting a value also deselects "Select all"', async () => {
+ render(
+ <Select
+ {...defaultProps}
+ options={OPTIONS.slice(0, 10)}
+ mode="multiple"
+ maxTagCount={0}
+ />,
+ );
+ await open();
+ userEvent.click(await findSelectOption(selectAllOptionLabel(10)));
+ let values = await findAllCheckedValues();
+ expect(values[0]).toHaveTextContent(selectAllOptionLabel(10));
+ userEvent.click(await findSelectOption(OPTIONS[0].label));
+ values = await findAllCheckedValues();
+ expect(values[0]).not.toHaveTextContent(selectAllOptionLabel(10));
+});
+
+test('selecting all values also selects "Select all"', async () => {
+ render(
+ <Select
+ {...defaultProps}
+ options={OPTIONS.slice(0, 10)}
+ mode="multiple"
+ maxTagCount={0}
+ />,
+ );
+ await open();
+ const options = await findAllSelectOptions();
+ options.forEach((option, index) => {
+ // skip select all
+ if (index > 0) {
+ userEvent.click(option);
+ }
+ });
+ const values = await findAllSelectValues();
+ expect(values[0]).toHaveTextContent(`+ 10 ...`);
+});
+
test('Renders only 1 tag and an overflow tag in oneLine mode', () => {
render(
<Select
@@ -614,6 +764,61 @@ test('Renders only an overflow tag if dropdown is open in oneLine mode', async (
expect(withinSelector.getByText('+ 2 ...')).toBeVisible();
});
+test('+N tag does not count the "Select All" option', async () => {
+ render(
+ <Select
+ {...defaultProps}
+ options={OPTIONS.slice(0, 10)}
+ mode="multiple"
+ maxTagCount={0}
+ />,
+ );
+ await open();
+ userEvent.click(await findSelectOption(selectAllOptionLabel(10)));
+ const values = await findAllSelectValues();
+ // maxTagCount is 0 so the +N tag should be + 10 ...
+ expect(values[0]).toHaveTextContent('+ 10 ...');
+});
+
+test('"Select All" is checked when unchecking a newly added option and all the other options are still selected', async () => {
+ render(
+ <Select
+ {...defaultProps}
+ options={OPTIONS.slice(0, 10)}
+ mode="multiple"
+ allowNewOptions
+ />,
+ );
+ await open();
+ userEvent.click(await findSelectOption(selectAllOptionLabel(10)));
+ expect(await findSelectOption(selectAllOptionLabel(10))).toBeInTheDocument();
+ // add a new option
+ await type(`${NEW_OPTION}{enter}`);
+ expect(await findSelectOption(selectAllOptionLabel(11))).toBeInTheDocument();
+ expect(await findSelectOption(NEW_OPTION)).toBeInTheDocument();
+ // select all should be selected
+ let values = await findAllCheckedValues();
+ expect(values[0]).toHaveTextContent(selectAllOptionLabel(11));
+ // remove new option
+ userEvent.click(await findSelectOption(NEW_OPTION));
+ // select all should still be selected
+ values = await findAllCheckedValues();
+ expect(values[0]).toHaveTextContent(selectAllOptionLabel(10));
+ expect(await findSelectOption(selectAllOptionLabel(10))).toBeInTheDocument();
+});
+
+test('does not render "Select All" when there are 0 or 1 options', async () => {
+ render(
+ <Select {...defaultProps} options={[]} mode="multiple" allowNewOptions />,
+ );
+ await open();
+ expect(screen.queryByText(selectAllOptionLabel(0))).not.toBeInTheDocument();
+ await type(`${NEW_OPTION}{enter}`);
+ expect(screen.queryByText(selectAllOptionLabel(1))).not.toBeInTheDocument();
+ await type(`Kyle2{enter}`);
+ expect(screen.queryByText(selectAllOptionLabel(2))).toBeInTheDocument();
+});
+
/*
TODO: Add tests that require scroll interaction. Needs further investigation.
- Fetches more data when scrolling and more data is available
diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx
index 5550463492..70ca4e6903 100644
--- a/superset-frontend/src/components/Select/Select.tsx
+++ b/superset-frontend/src/components/Select/Select.tsx
@@ -25,20 +25,26 @@ import React, {
useState,
useCallback,
} from 'react';
-import { ensureIsArray, t } from '@superset-ui/core';
-import { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
+import {
+ ensureIsArray,
+ formatNumber,
+ NumberFormats,
+ t,
+} from '@superset-ui/core';
+import AntdSelect, { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
import { isEqual } from 'lodash';
import {
getValue,
hasOption,
isLabeledValue,
renderSelectOptions,
- hasCustomLabels,
sortSelectedFirstHelper,
sortComparatorWithSearchHelper,
handleFilterOptionHelper,
dropDownRenderHelper,
getSuffixIcon,
+ SELECT_ALL_VALUE,
+ selectAllOption,
} from './utils';
import { SelectOptionsType, SelectProps } from './types';
import {
@@ -54,7 +60,7 @@ import {
TOKEN_SEPARATORS,
DEFAULT_SORT_COMPARATOR,
} from './constants';
-import { oneLineTagRender } from './CustomTag';
+import { customTagRender } from './CustomTag';
/**
* This component is a customized version of the Antdesign 4.X Select component
@@ -125,6 +131,8 @@ const Select = forwardRef(
? 'tags'
: 'multiple';
+ const { Option } = AntdSelect;
+
const sortSelectedFirst = useCallback(
(a: AntdLabeledValue, b: AntdLabeledValue) =>
sortSelectedFirstHelper(a, b, selectValue),
@@ -162,11 +170,23 @@ const Select = forwardRef(
.map(opt =>
isLabeledValue(opt) ? opt : { value: opt, label: String(opt) },
);
- return missingValues.length > 0
- ? missingValues.concat(selectOptions)
- : selectOptions;
+ const result =
+ missingValues.length > 0
+ ? missingValues.concat(selectOptions)
+ : selectOptions;
+ return result.filter(opt => opt.value !== SELECT_ALL_VALUE);
}, [selectOptions, selectValue]);
+ const selectAllEnabled = useMemo(
+ () => !isSingleMode && fullSelectOptions.length > 1 && !inputValue,
+ [fullSelectOptions, isSingleMode, inputValue],
+ );
+
+ const selectAllMode = useMemo(
+ () => ensureIsArray(selectValue).length === fullSelectOptions.length + 1,
+ [selectValue, fullSelectOptions],
+ );
+
const handleOnSelect = (
selectedItem: string | number | AntdLabeledValue | undefined,
) => {
@@ -177,11 +197,29 @@ const Select = forwardRef(
const array = ensureIsArray(previousState);
const value = getValue(selectedItem);
// Tokenized values can contain duplicated values
+ if (value === getValue(SELECT_ALL_VALUE)) {
+ if (isLabeledValue(selectedItem)) {
+ return [
+ ...fullSelectOptions,
+ selectAllOption,
+ ] as AntdLabeledValue[];
+ }
+ return [
+ SELECT_ALL_VALUE,
+ ...fullSelectOptions.map(opt => opt.value),
+ ] as AntdLabeledValue[];
+ }
if (!hasOption(value, array)) {
const result = [...array, selectedItem];
- return isLabeledValue(selectedItem)
- ? (result as AntdLabeledValue[])
- : (result as (string | number)[]);
+ if (
+ result.length === fullSelectOptions.length &&
+ selectAllEnabled
+ ) {
+ return isLabeledValue(selectedItem)
+ ? ([...result, selectAllOption] as AntdLabeledValue[])
+ : ([...result, SELECT_ALL_VALUE] as (string | number)[]);
+ }
+ return result as AntdLabeledValue[];
}
return previousState;
});
@@ -193,14 +231,23 @@ const Select = forwardRef(
value: string | number | AntdLabeledValue | undefined,
) => {
if (Array.isArray(selectValue)) {
- if (isLabeledValue(value)) {
- const array = selectValue as AntdLabeledValue[];
- setSelectValue(
- array.filter(element => element.value !== value.value),
- );
+ if (getValue(value) === getValue(SELECT_ALL_VALUE)) {
+ setSelectValue(undefined);
} else {
- const array = selectValue as (string | number)[];
- setSelectValue(array.filter(element => element !== value));
+ let array = selectValue as AntdLabeledValue[];
+ array = array.filter(
+ element => getValue(element) !== getValue(value),
+ );
+ // if this was not a new item, deselect select all option
+ if (
+ selectAllMode &&
+ selectOptions.some(opt => opt.value === getValue(value))
+ ) {
+ array = array.filter(
+ element => getValue(element) !== SELECT_ALL_VALUE,
+ );
+ }
+ setSelectValue(array);
}
}
setInputValue('');
@@ -215,7 +262,7 @@ const Select = forwardRef(
value: searchValue,
isNewOption: true,
};
- const cleanSelectOptions = fullSelectOptions.filter(
+ const cleanSelectOptions = ensureIsArray(fullSelectOptions).filter(
opt => !opt.isNewOption || hasOption(opt.value, selectValue),
);
const newOptions = newOption
@@ -277,6 +324,97 @@ const Select = forwardRef(
setSelectValue(value);
}, [value]);
+ useEffect(() => {
+ // if all values are selected, add select all to value
+ if (
+ !isSingleMode &&
+ ensureIsArray(value).length === fullSelectOptions.length &&
+ fullSelectOptions.length > 0
+ ) {
+ setSelectValue(
+ labelInValue
+ ? ([...ensureIsArray(value), selectAllOption] as AntdLabeledValue[])
+ : ([
+ ...ensureIsArray(value),
+ SELECT_ALL_VALUE,
+ ] as AntdLabeledValue[]),
+ );
+ }
+ }, [value, isSingleMode, labelInValue, fullSelectOptions.length]);
+
+ useEffect(() => {
+ const checkSelectAll = ensureIsArray(selectValue).some(
+ v => getValue(v) === SELECT_ALL_VALUE,
+ );
+ if (checkSelectAll && !selectAllMode) {
+ setSelectValue(
+ labelInValue
+ ? ([...fullSelectOptions, selectAllOption] as AntdLabeledValue[])
+ : ([...fullSelectOptions, SELECT_ALL_VALUE] as AntdLabeledValue[]),
+ );
+ }
+ }, [selectValue, selectAllMode, labelInValue, fullSelectOptions]);
+
+ const selectAllLabel = useMemo(
+ () => () =>
+ `${SELECT_ALL_VALUE} (${formatNumber(
+ NumberFormats.INTEGER,
+ fullSelectOptions.length,
+ )})`,
+ [fullSelectOptions.length],
+ );
+
+ const handleOnChange = (values: any, options: any) => {
+ // intercept onChange call to handle the select all case
+ // if the "select all" option is selected, we want to send all options to the onChange,
+ // otherwise we want to remove
+ let newValues = values;
+ let newOptions = options;
+ if (!isSingleMode) {
+ if (
+ ensureIsArray(newValues).some(
+ val => getValue(val) === SELECT_ALL_VALUE,
+ )
+ ) {
+ // send all options to onchange if all are not currently there
+ if (!selectAllMode) {
+ newValues = labelInValue
+ ? fullSelectOptions.map(opt => ({
+ key: opt.value,
+ value: opt.value,
+ label: opt.label,
+ }))
+ : fullSelectOptions.map(opt => opt.value);
+ newOptions = fullSelectOptions.map(opt => ({
+ children: opt.label,
+ key: opt.value,
+ value: opt.value,
+ label: opt.label,
+ }));
+ } else {
+ newValues = ensureIsArray(values).filter(
+ (val: any) => getValue(val) !== SELECT_ALL_VALUE,
+ );
+ }
+ } else if (
+ ensureIsArray(values).length === fullSelectOptions.length &&
+ selectAllMode
+ ) {
+ newValues = [];
+ newValues = [];
+ }
+ }
+ onChange?.(newValues, newOptions);
+ };
+
+ const customMaxTagPlaceholder = () => {
+ const num_selected = ensureIsArray(selectValue).length;
+ const num_shown = maxTagCount as number;
+ return selectAllMode
+ ? `+ ${num_selected - num_shown - 1} ...`
+ : `+ ${num_selected - num_shown} ...`;
+ };
+
return (
<StyledContainer headerPosition={headerPosition}>
{header && (
@@ -294,6 +432,7 @@ const Select = forwardRef(
headerPosition={headerPosition}
labelInValue={labelInValue}
maxTagCount={maxTagCount}
+ maxTagPlaceholder={customMaxTagPlaceholder}
mode={mappedMode}
notFoundContent={isLoading ? t('Loading...') : notFoundContent}
onDeselect={handleOnDeselect}
@@ -302,8 +441,7 @@ const Select = forwardRef(
onSearch={shouldShowSearch ? handleOnSearch : undefined}
onSelect={handleOnSelect}
onClear={handleClear}
- onChange={onChange}
- options={hasCustomLabels(options) ? undefined : fullSelectOptions}
+ onChange={handleOnChange}
placeholder={placeholder}
showSearch={shouldShowSearch}
showArrow
@@ -322,11 +460,20 @@ const Select = forwardRef(
)
}
oneLine={oneLine}
- tagRender={oneLine ? oneLineTagRender : undefined}
+ tagRender={customTagRender}
{...props}
ref={ref}
>
- {hasCustomLabels(options) && renderSelectOptions(fullSelectOptions)}
+ {selectAllEnabled && (
+ <Option
+ id="select-all"
+ key={SELECT_ALL_VALUE}
+ value={SELECT_ALL_VALUE}
+ >
+ {selectAllLabel()}
+ </Option>
+ )}
+ {renderSelectOptions(fullSelectOptions)}
</StyledSelect>
</StyledContainer>
);
diff --git a/superset-frontend/src/components/Select/styles.tsx b/superset-frontend/src/components/Select/styles.tsx
index 7064ae55a3..b954a04fc7 100644
--- a/superset-frontend/src/components/Select/styles.tsx
+++ b/superset-frontend/src/components/Select/styles.tsx
@@ -18,7 +18,7 @@
*/
import { styled } from '@superset-ui/core';
import Icons from 'src/components/Icons';
-import { Spin } from 'antd';
+import { Spin, Tag } from 'antd';
import AntdSelect from 'antd/lib/select';
export const StyledHeader = styled.span<{ headerPosition: string }>`
@@ -74,6 +74,18 @@ export const StyledSelect = styled(AntdSelect, {
`}
`;
+export const NoElement = styled.span`
+ display: none;
+`;
+
+export const StyledTag = styled(Tag)`
+ ${({ theme }) => `
+ background: ${theme.colors.grayscale.light3};
+ font-size: ${theme.typography.sizes.m}px;
+ border: none;
+ `}
+`;
+
export const StyledStopOutlined = styled(Icons.StopOutlined)`
vertical-align: 0;
`;
diff --git a/superset-frontend/src/components/Select/types.ts b/superset-frontend/src/components/Select/types.ts
index 76f7acd0a2..6e4c7f072d 100644
--- a/superset-frontend/src/components/Select/types.ts
+++ b/superset-frontend/src/components/Select/types.ts
@@ -158,8 +158,6 @@ 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;
}
@@ -215,4 +213,5 @@ export interface AsyncSelectProps extends BaseSelectProps {
export type CustomTagProps = HTMLSpanElement &
TagProps & {
label: ReactNode;
+ value: string;
};
diff --git a/superset-frontend/src/components/Select/utils.tsx b/superset-frontend/src/components/Select/utils.tsx
index 0d499b4f1d..e819935822 100644
--- a/superset-frontend/src/components/Select/utils.tsx
+++ b/superset-frontend/src/components/Select/utils.tsx
@@ -25,6 +25,12 @@ import { LabeledValue, RawValue, SelectOptionsType, V } from './types';
const { Option } = AntdSelect;
+export const SELECT_ALL_VALUE: RawValue = 'Select All';
+export const selectAllOption = {
+ value: SELECT_ALL_VALUE,
+ label: String(SELECT_ALL_VALUE),
+};
+
export function isObject(value: unknown): value is Record<string, unknown> {
return (
value !== null &&
diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
index c19754b518..fb8e20093c 100644
--- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
+++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
@@ -333,9 +333,6 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
// @ts-ignore
options={options}
sortComparator={sortComparator}
- maxTagPlaceholder={(val: AntdLabeledValue[]) => (
- <span>+{val.length}</span>
- )}
onDropdownVisibleChange={setFilterActive}
/>
</StyledFormItem>