feat: Select all for synchronous select (#22084)
Co-authored-by: GITHUB_USERNAME <EMAIL>
This commit is contained in:
parent
ad758c0802
commit
02c9242d68
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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 />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue