mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
fix: Select refactoring known issues (#16666)
* Clean up and reorganize effects * Enhance optionFilterProps * Render custom label * Remove prop filtering * Create options * Create option from value in single mode * Change to customLabel * Show search by default * Update superset-frontend/src/components/Select/Select.tsx Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> * Update superset-frontend/src/components/Select/Select.tsx Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> * Update superset-frontend/src/components/Select/Select.tsx Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> * Apply minor changes * Fixes a bug that was failing CI * Adds more tests to the component * Apply customLabel in ColorSchemeControl * Remove customLabel from rendered Option * Hide No data when allowNewOptions * Remove unnecessary prop from tests * Adjust loading height * Show no data with fetchOnlyOnSearch Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> Co-authored-by: Michael S. Molina <michael.s.molina@gmail.com>
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import rison from 'rison';
|
||||
import { styled, t, SupersetClient, JsonResponse } from '@superset-ui/core';
|
||||
import { Steps } from 'src/common/components';
|
||||
@@ -193,7 +193,6 @@ export default class AddSliceContainer extends React.PureComponent<
|
||||
this.gotoSlice = this.gotoSlice.bind(this);
|
||||
this.newLabel = this.newLabel.bind(this);
|
||||
this.loadDatasources = this.loadDatasources.bind(this);
|
||||
this.handleFilterOption = this.handleFilterOption.bind(this);
|
||||
}
|
||||
|
||||
exploreUrl() {
|
||||
@@ -254,16 +253,17 @@ export default class AddSliceContainer extends React.PureComponent<
|
||||
endpoint: `/api/v1/dataset/?q=${query}`,
|
||||
}).then((response: JsonResponse) => {
|
||||
const list: {
|
||||
customLabel: ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
}[] = response.json.result
|
||||
.map((item: Dataset) => ({
|
||||
value: `${item.id}__${item.datasource_type}`,
|
||||
label: this.newLabel(item),
|
||||
labelText: item.table_name,
|
||||
customLabel: this.newLabel(item),
|
||||
label: item.table_name,
|
||||
}))
|
||||
.sort((a: { labelText: string }, b: { labelText: string }) =>
|
||||
a.labelText.localeCompare(b.labelText),
|
||||
.sort((a: { label: string }, b: { label: string }) =>
|
||||
a.label.localeCompare(b.label),
|
||||
);
|
||||
return {
|
||||
data: list,
|
||||
@@ -272,15 +272,6 @@ export default class AddSliceContainer extends React.PureComponent<
|
||||
});
|
||||
}
|
||||
|
||||
handleFilterOption(
|
||||
search: string,
|
||||
option: { label: string; value: number; labelText: string },
|
||||
) {
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
const { labelText } = option;
|
||||
return labelText.toLowerCase().includes(searchValue);
|
||||
}
|
||||
|
||||
render() {
|
||||
const isButtonDisabled = this.isBtnDisabled();
|
||||
return (
|
||||
@@ -296,7 +287,6 @@ export default class AddSliceContainer extends React.PureComponent<
|
||||
autoFocus
|
||||
ariaLabel={t('Dataset')}
|
||||
name="select-datasource"
|
||||
filterOption={this.handleFilterOption}
|
||||
onChange={this.changeDatasource}
|
||||
options={this.loadDatasources}
|
||||
placeholder={t('Choose a dataset')}
|
||||
|
||||
@@ -31,12 +31,17 @@ const options = [
|
||||
{
|
||||
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: 'Just a label', value: 'Just a 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' },
|
||||
@@ -137,6 +142,7 @@ InteractiveSelect.args = {
|
||||
disabled: false,
|
||||
invertSelection: false,
|
||||
placeholder: 'Select ...',
|
||||
optionFilterProps: ['value', 'label', 'custom'],
|
||||
};
|
||||
|
||||
InteractiveSelect.argTypes = {
|
||||
|
||||
@@ -32,26 +32,26 @@ const NEW_OPTION = 'Kyle';
|
||||
const NO_DATA = 'No Data';
|
||||
const LOADING = 'Loading...';
|
||||
const OPTIONS = [
|
||||
{ label: 'John', value: 1 },
|
||||
{ label: 'Liam', value: 2 },
|
||||
{ label: 'Olivia', value: 3 },
|
||||
{ label: 'Emma', value: 4 },
|
||||
{ label: 'Noah', value: 5 },
|
||||
{ label: 'Ava', value: 6 },
|
||||
{ label: 'Oliver', value: 7 },
|
||||
{ label: 'ElijahH', value: 8 },
|
||||
{ label: 'Charlotte', value: 9 },
|
||||
{ label: 'Giovanni', value: 10 },
|
||||
{ label: 'Franco', value: 11 },
|
||||
{ label: 'Sandro', value: 12 },
|
||||
{ label: 'Alehandro', value: 13 },
|
||||
{ label: 'Johnny', value: 14 },
|
||||
{ label: 'Nikole', value: 15 },
|
||||
{ label: 'Igor', value: 16 },
|
||||
{ label: 'Guilherme', value: 17 },
|
||||
{ label: 'Irfan', value: 18 },
|
||||
{ label: 'George', value: 19 },
|
||||
{ label: 'Ashfaq', value: 20 },
|
||||
{ label: 'John', value: 1, gender: 'Male' },
|
||||
{ label: 'Liam', value: 2, gender: 'Male' },
|
||||
{ label: 'Olivia', value: 3, gender: 'Female' },
|
||||
{ label: 'Emma', value: 4, gender: 'Female' },
|
||||
{ label: 'Noah', value: 5, gender: 'Male' },
|
||||
{ label: 'Ava', value: 6, gender: 'Female' },
|
||||
{ label: 'Oliver', value: 7, gender: 'Male' },
|
||||
{ label: 'ElijahH', value: 8, gender: 'Male' },
|
||||
{ label: 'Charlotte', value: 9, gender: 'Female' },
|
||||
{ label: 'Giovanni', value: 10, gender: 'Male' },
|
||||
{ label: 'Franco', value: 11, gender: 'Male' },
|
||||
{ label: 'Sandro', value: 12, gender: 'Male' },
|
||||
{ label: 'Alehandro', value: 13, gender: 'Male' },
|
||||
{ label: 'Johnny', value: 14, gender: 'Male' },
|
||||
{ label: 'Nikole', value: 15, gender: 'Female' },
|
||||
{ label: 'Igor', value: 16, gender: 'Male' },
|
||||
{ label: 'Guilherme', value: 17, gender: 'Male' },
|
||||
{ label: 'Irfan', value: 18, gender: 'Male' },
|
||||
{ label: 'George', value: 19, gender: 'Male' },
|
||||
{ label: 'Ashfaq', value: 20, gender: 'Male' },
|
||||
];
|
||||
|
||||
const loadOptions = async (search: string, page: number, pageSize: number) => {
|
||||
@@ -100,7 +100,11 @@ const findSelectValue = () =>
|
||||
const findAllSelectValues = () =>
|
||||
waitFor(() => getElementsByClassName('.ant-select-selection-item'));
|
||||
|
||||
const type = (text: string) => userEvent.type(getSelect(), text, { delay: 10 });
|
||||
const type = (text: string) => {
|
||||
const select = getSelect();
|
||||
userEvent.clear(select);
|
||||
return userEvent.type(select, text, { delay: 10 });
|
||||
};
|
||||
|
||||
const open = () => waitFor(() => userEvent.click(getSelect()));
|
||||
|
||||
@@ -110,6 +114,12 @@ test('displays a header', async () => {
|
||||
expect(screen.getByText(headerText)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('adds a new option if the value is not in the options', async () => {
|
||||
render(<Select {...defaultProps} options={[]} value={OPTIONS[0]} />);
|
||||
await open();
|
||||
expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('inverts the selection', async () => {
|
||||
render(<Select {...defaultProps} invertSelection />);
|
||||
await open();
|
||||
@@ -141,6 +151,60 @@ test('searches for label or value', async () => {
|
||||
expect(options[0]).toHaveTextContent(option.label);
|
||||
});
|
||||
|
||||
test('searches for custom fields', async () => {
|
||||
render(<Select {...defaultProps} optionFilterProps={['label', 'gender']} />);
|
||||
await type('Liam');
|
||||
let options = await findAllSelectOptions();
|
||||
expect(options.length).toBe(1);
|
||||
expect(options[0]).toHaveTextContent('Liam');
|
||||
await type('Female');
|
||||
options = await findAllSelectOptions();
|
||||
expect(options.length).toBe(5);
|
||||
expect(options[0]).toHaveTextContent('Olivia');
|
||||
expect(options[1]).toHaveTextContent('Emma');
|
||||
expect(options[2]).toHaveTextContent('Ava');
|
||||
expect(options[3]).toHaveTextContent('Charlotte');
|
||||
expect(options[4]).toHaveTextContent('Nikole');
|
||||
await type('1');
|
||||
expect(screen.getByText(NO_DATA)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders a custom label', async () => {
|
||||
const options = [
|
||||
{ label: 'John', value: 1, customLabel: <h1>John</h1> },
|
||||
{ label: 'Liam', value: 2, customLabel: <h1>Liam</h1> },
|
||||
{ label: 'Olivia', value: 3, customLabel: <h1>Olivia</h1> },
|
||||
];
|
||||
render(<Select {...defaultProps} options={options} />);
|
||||
await open();
|
||||
expect(screen.getByRole('heading', { name: 'John' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Liam' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Olivia' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('searches for a word with a custom label', async () => {
|
||||
const options = [
|
||||
{ label: 'John', value: 1, customLabel: <h1>John</h1> },
|
||||
{ label: 'Liam', value: 2, customLabel: <h1>Liam</h1> },
|
||||
{ label: 'Olivia', value: 3, customLabel: <h1>Olivia</h1> },
|
||||
];
|
||||
render(<Select {...defaultProps} options={options} />);
|
||||
await type('Liam');
|
||||
const selectOptions = await findAllSelectOptions();
|
||||
expect(selectOptions.length).toBe(1);
|
||||
expect(selectOptions[0]).toHaveTextContent('Liam');
|
||||
});
|
||||
|
||||
test('removes a new option if the user does not select it', async () => {
|
||||
render(<Select {...defaultProps} allowNewOptions />);
|
||||
await type(NEW_OPTION);
|
||||
expect(await findSelectOption(NEW_OPTION)).toBeInTheDocument();
|
||||
await type('k');
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText(NEW_OPTION)).not.toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
test('clear all the values', async () => {
|
||||
const onClear = jest.fn();
|
||||
render(
|
||||
@@ -157,7 +221,7 @@ test('clear all the values', async () => {
|
||||
expect(values.length).toBe(0);
|
||||
});
|
||||
|
||||
test('does not add a new option if allowNewValue is false', async () => {
|
||||
test('does not add a new option if allowNewOptions is false', async () => {
|
||||
render(<Select {...defaultProps} options={loadOptions} />);
|
||||
await open();
|
||||
await type(NEW_OPTION);
|
||||
@@ -202,12 +266,18 @@ test('static - changes the selected item in single mode', async () => {
|
||||
userEvent.click(await findSelectOption(firstOption.label));
|
||||
expect(await findSelectValue()).toHaveTextContent(firstOption.label);
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining(firstOption),
|
||||
expect.objectContaining({
|
||||
label: firstOption.label,
|
||||
value: firstOption.value,
|
||||
}),
|
||||
firstOption,
|
||||
);
|
||||
userEvent.click(await findSelectOption(secondOption.label));
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining(secondOption),
|
||||
expect.objectContaining({
|
||||
label: secondOption.label,
|
||||
value: secondOption.value,
|
||||
}),
|
||||
secondOption,
|
||||
);
|
||||
expect(await findSelectValue()).toHaveTextContent(secondOption.label);
|
||||
@@ -236,6 +306,34 @@ test('static - adds a new option if none is available and allowNewOptions is tru
|
||||
expect(await findSelectOption(NEW_OPTION)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('static - shows "No data" when allowNewOptions is false and a new option is entered', async () => {
|
||||
render(<Select {...defaultProps} allowNewOptions={false} />);
|
||||
await open();
|
||||
await type(NEW_OPTION);
|
||||
expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('static - does not show "No data" when allowNewOptions is true and a new option is entered', async () => {
|
||||
render(<Select {...defaultProps} allowNewOptions />);
|
||||
await open();
|
||||
await type(NEW_OPTION);
|
||||
expect(screen.queryByText(NO_DATA)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('static - does not show "Loading..." when allowNewOptions is false and a new option is entered', async () => {
|
||||
render(<Select {...defaultProps} allowNewOptions={false} />);
|
||||
await open();
|
||||
await type(NEW_OPTION);
|
||||
expect(screen.queryByText(LOADING)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('static - shows "Loading..." when allowNewOptions is true and a new option is entered', async () => {
|
||||
render(<Select {...defaultProps} allowNewOptions />);
|
||||
await open();
|
||||
await type(NEW_OPTION);
|
||||
expect(await screen.findByText(LOADING)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('static - does not add a new option if the option already exists', async () => {
|
||||
render(<Select {...defaultProps} allowNewOptions />);
|
||||
const option = OPTIONS[0].label;
|
||||
@@ -334,13 +432,19 @@ test('async - changes the selected item in single mode', async () => {
|
||||
const [firstOption, secondOption] = OPTIONS;
|
||||
userEvent.click(await findSelectOption(firstOption.label));
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining(firstOption),
|
||||
expect.objectContaining({
|
||||
label: firstOption.label,
|
||||
value: firstOption.value,
|
||||
}),
|
||||
firstOption,
|
||||
);
|
||||
expect(await findSelectValue()).toHaveTextContent(firstOption.label);
|
||||
userEvent.click(await findSelectOption(secondOption.label));
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining(secondOption),
|
||||
expect.objectContaining({
|
||||
label: secondOption.label,
|
||||
value: secondOption.value,
|
||||
}),
|
||||
secondOption,
|
||||
);
|
||||
expect(await findSelectValue()).toHaveTextContent(secondOption.label);
|
||||
@@ -382,6 +486,27 @@ test('async - does not add a new option if the option already exists', async ()
|
||||
});
|
||||
});
|
||||
|
||||
test('async - shows "No data" when allowNewOptions is false and a new option is entered', async () => {
|
||||
render(
|
||||
<Select
|
||||
{...defaultProps}
|
||||
options={loadOptions}
|
||||
allowNewOptions={false}
|
||||
showSearch
|
||||
/>,
|
||||
);
|
||||
await open();
|
||||
await type(NEW_OPTION);
|
||||
expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('async - does not show "No data" when allowNewOptions is true and a new option is entered', async () => {
|
||||
render(<Select {...defaultProps} options={loadOptions} allowNewOptions />);
|
||||
await open();
|
||||
await type(NEW_OPTION);
|
||||
expect(screen.queryByText(NO_DATA)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('async - sets a initial value in single mode', async () => {
|
||||
render(<Select {...defaultProps} options={loadOptions} value={OPTIONS[0]} />);
|
||||
expect(await findSelectValue()).toHaveTextContent(OPTIONS[0].label);
|
||||
@@ -469,9 +594,9 @@ test('async - does not fire a new request for the same search input', async () =
|
||||
});
|
||||
|
||||
/*
|
||||
TODO: Add tests that require scroll interaction. Needs further investigation.
|
||||
- Fetches more data when scrolling and more data is available
|
||||
- Doesn't fetch more data when no more data is available
|
||||
- Requests the correct page and page size
|
||||
- Sets the page to zero when a new search is made
|
||||
*/
|
||||
TODO: Add tests that require scroll interaction. Needs further investigation.
|
||||
- Fetches more data when scrolling and more data is available
|
||||
- Doesn't fetch more data when no more data is available
|
||||
- Requests the correct page and page size
|
||||
- Sets the page to zero when a new search is made
|
||||
*/
|
||||
|
||||
@@ -41,6 +41,8 @@ import Icons from 'src/components/Icons';
|
||||
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||
import { hasOption } from './utils';
|
||||
|
||||
const { Option } = AntdSelect;
|
||||
|
||||
type AntdSelectAllProps = AntdSelectProps<AntdSelectValue>;
|
||||
|
||||
type PickedSelectProps = Pick<
|
||||
@@ -60,7 +62,12 @@ type PickedSelectProps = Pick<
|
||||
| 'value'
|
||||
>;
|
||||
|
||||
export type OptionsType = Exclude<AntdSelectAllProps['options'], undefined>;
|
||||
type OptionsProps = Exclude<AntdSelectAllProps['options'], undefined>;
|
||||
|
||||
export interface OptionsType extends Omit<OptionsProps, 'label'> {
|
||||
label?: string;
|
||||
customLabel?: ReactNode;
|
||||
}
|
||||
|
||||
export type OptionsTypePage = {
|
||||
data: OptionsType;
|
||||
@@ -80,6 +87,7 @@ export interface SelectProps extends PickedSelectProps {
|
||||
lazyLoading?: boolean;
|
||||
mode?: 'single' | 'multiple';
|
||||
name?: string; // discourage usage
|
||||
optionFilterProps?: string[];
|
||||
options: OptionsType | OptionsPagePromise;
|
||||
pageSize?: number;
|
||||
invertSelection?: boolean;
|
||||
@@ -138,9 +146,10 @@ const StyledSpin = styled(Spin)`
|
||||
margin-top: ${({ theme }) => -theme.gridUnit}px;
|
||||
`;
|
||||
|
||||
const StyledLoadingText = styled.span`
|
||||
const StyledLoadingText = styled.div`
|
||||
${({ theme }) => `
|
||||
margin-left: ${theme.gridUnit * 3}px;
|
||||
line-height: ${theme.gridUnit * 8}px;
|
||||
color: ${theme.colors.grayscale.light1};
|
||||
`}
|
||||
`;
|
||||
@@ -169,12 +178,14 @@ const Select = ({
|
||||
loading,
|
||||
mode = 'single',
|
||||
name,
|
||||
notFoundContent,
|
||||
onChange,
|
||||
onClear,
|
||||
optionFilterProps = ['label', 'value'],
|
||||
options,
|
||||
pageSize = DEFAULT_PAGE_SIZE,
|
||||
placeholder = t('Select ...'),
|
||||
showSearch,
|
||||
showSearch = true,
|
||||
value,
|
||||
...props
|
||||
}: SelectProps) => {
|
||||
@@ -186,6 +197,9 @@ const Select = ({
|
||||
const [selectOptions, setSelectOptions] = useState<OptionsType>(
|
||||
initialOptions,
|
||||
);
|
||||
const shouldUseChildrenOptions = !!selectOptions.find(
|
||||
opt => opt?.customLabel,
|
||||
);
|
||||
const [selectValue, setSelectValue] = useState(value);
|
||||
const [searchedValue, setSearchedValue] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(loading);
|
||||
@@ -202,39 +216,7 @@ const Select = ({
|
||||
? 'tags'
|
||||
: 'multiple';
|
||||
|
||||
useEffect(() => {
|
||||
fetchedQueries.current.clear();
|
||||
setSelectOptions(
|
||||
options && Array.isArray(options) ? options : EMPTY_OPTIONS,
|
||||
);
|
||||
}, [options]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectValue(value);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAsync && selectValue) {
|
||||
const array: AntdLabeledValue[] = Array.isArray(selectValue)
|
||||
? (selectValue as AntdLabeledValue[])
|
||||
: [selectValue as AntdLabeledValue];
|
||||
const options: AntdLabeledValue[] = [];
|
||||
array.forEach(element => {
|
||||
const found = selectOptions.find(
|
||||
option => option.value === element.value,
|
||||
);
|
||||
if (!found) {
|
||||
options.push(element);
|
||||
}
|
||||
});
|
||||
if (options.length > 0) {
|
||||
setSelectOptions([...selectOptions, ...options]);
|
||||
}
|
||||
}
|
||||
}, [isAsync, selectOptions, selectValue]);
|
||||
|
||||
// TODO: Simplify the code. We're only accepting label, value options.
|
||||
// TODO: Remove labelInValue prop.
|
||||
// TODO: Don't assume that isAsync is always labelInValue
|
||||
const handleTopOptions = useCallback(
|
||||
(selectedValue: AntdSelectValue | undefined) => {
|
||||
// bringing selected options to the top of the list
|
||||
@@ -393,53 +375,30 @@ const Select = ({
|
||||
() =>
|
||||
debounce((search: string) => {
|
||||
const searchValue = search.trim();
|
||||
// enables option creation
|
||||
if (allowNewOptions && isSingleMode) {
|
||||
const firstOption =
|
||||
selectOptions.length > 0 && selectOptions[0].value;
|
||||
// replaces the last search value entered with the new one
|
||||
// only when the value wasn't part of the original options
|
||||
if (
|
||||
searchValue &&
|
||||
firstOption === searchedValue &&
|
||||
!initialOptions.find(o => o.value === searchedValue)
|
||||
) {
|
||||
selectOptions.shift();
|
||||
setSelectOptions(selectOptions);
|
||||
}
|
||||
if (searchValue && !hasOption(searchValue, selectOptions)) {
|
||||
const newOption = {
|
||||
const newOption = searchValue &&
|
||||
!hasOption(searchValue, selectOptions) && {
|
||||
label: searchValue,
|
||||
value: searchValue,
|
||||
};
|
||||
// adds a custom option
|
||||
const newOptions = [...selectOptions, newOption];
|
||||
setSelectOptions(newOptions);
|
||||
setSelectValue(newOption);
|
||||
const newOptions = newOption
|
||||
? [
|
||||
newOption,
|
||||
...selectOptions.filter(opt => opt.value !== searchedValue),
|
||||
]
|
||||
: [...selectOptions.filter(opt => opt.value !== searchedValue)];
|
||||
|
||||
if (onChange) {
|
||||
onChange(searchValue, newOptions);
|
||||
}
|
||||
}
|
||||
setSelectOptions(newOptions);
|
||||
}
|
||||
|
||||
if (!searchValue || searchValue === searchedValue) {
|
||||
setIsTyping(false);
|
||||
}
|
||||
setSearchedValue(searchValue);
|
||||
}, DEBOUNCE_TIMEOUT),
|
||||
[
|
||||
allowNewOptions,
|
||||
initialOptions,
|
||||
isSingleMode,
|
||||
onChange,
|
||||
searchedValue,
|
||||
selectOptions,
|
||||
],
|
||||
[allowNewOptions, isSingleMode, searchedValue, selectOptions],
|
||||
);
|
||||
|
||||
// Stop the invocation of the debounced function after unmounting
|
||||
useEffect(() => () => handleOnSearch.cancel(), [handleOnSearch]);
|
||||
|
||||
const handlePagination = (e: UIEvent<HTMLElement>) => {
|
||||
const vScroll = e.currentTarget;
|
||||
const thresholdReached =
|
||||
@@ -460,13 +419,15 @@ const Select = ({
|
||||
|
||||
if (filterOption) {
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
const { value, label } = option;
|
||||
const valueText = String(value);
|
||||
const labelText = String(label);
|
||||
return (
|
||||
valueText.toLowerCase().includes(searchValue) ||
|
||||
labelText.toLowerCase().includes(searchValue)
|
||||
);
|
||||
|
||||
if (optionFilterProps && optionFilterProps.length) {
|
||||
return optionFilterProps.some(prop => {
|
||||
const optionProp = option?.[prop]
|
||||
? String(option[prop]).trim().toLowerCase()
|
||||
: '';
|
||||
return optionProp.includes(searchValue);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -486,34 +447,6 @@ const Select = ({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const allowFetch = !fetchOnlyOnSearch || searchedValue;
|
||||
if (isAsync && loadingEnabled && allowFetch) {
|
||||
const page = 0;
|
||||
handlePaginatedFetch(searchedValue, page, pageSize);
|
||||
setPage(page);
|
||||
}
|
||||
}, [
|
||||
isAsync,
|
||||
searchedValue,
|
||||
pageSize,
|
||||
handlePaginatedFetch,
|
||||
loadingEnabled,
|
||||
fetchOnlyOnSearch,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSingleMode) {
|
||||
handleTopOptions(selectValue);
|
||||
}
|
||||
}, [handleTopOptions, isSingleMode, selectValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading !== undefined && loading !== isLoading) {
|
||||
setIsLoading(loading);
|
||||
}
|
||||
}, [isLoading, loading]);
|
||||
|
||||
const dropdownRender = (
|
||||
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
|
||||
) => {
|
||||
@@ -549,6 +482,75 @@ const Select = ({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchedQueries.current.clear();
|
||||
setSelectOptions(
|
||||
options && Array.isArray(options) ? options : EMPTY_OPTIONS,
|
||||
);
|
||||
}, [options]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectValue(value);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectValue) {
|
||||
const array = Array.isArray(selectValue)
|
||||
? (selectValue as AntdLabeledValue[])
|
||||
: [selectValue as AntdLabeledValue | string | number];
|
||||
const options: AntdLabeledValue[] = [];
|
||||
const isLabeledValue = isAsync || labelInValue;
|
||||
array.forEach(element => {
|
||||
const found = selectOptions.find((option: { value: string | number }) =>
|
||||
isLabeledValue
|
||||
? option.value === (element as AntdLabeledValue).value
|
||||
: option.value === element,
|
||||
);
|
||||
if (!found) {
|
||||
options.push(
|
||||
isLabeledValue
|
||||
? (element as AntdLabeledValue)
|
||||
: ({ value: element, label: element } as AntdLabeledValue),
|
||||
);
|
||||
}
|
||||
});
|
||||
if (options.length > 0) {
|
||||
setSelectOptions([...options, ...selectOptions]);
|
||||
}
|
||||
}
|
||||
}, [labelInValue, isAsync, selectOptions, selectValue]);
|
||||
|
||||
// Stop the invocation of the debounced function after unmounting
|
||||
useEffect(() => () => handleOnSearch.cancel(), [handleOnSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
const allowFetch = !fetchOnlyOnSearch || searchedValue;
|
||||
if (isAsync && loadingEnabled && allowFetch) {
|
||||
const page = 0;
|
||||
handlePaginatedFetch(searchedValue, page, pageSize);
|
||||
setPage(page);
|
||||
}
|
||||
}, [
|
||||
isAsync,
|
||||
searchedValue,
|
||||
pageSize,
|
||||
handlePaginatedFetch,
|
||||
loadingEnabled,
|
||||
fetchOnlyOnSearch,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSingleMode) {
|
||||
handleTopOptions(selectValue);
|
||||
}
|
||||
}, [handleTopOptions, isSingleMode, selectValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading !== undefined && loading !== isLoading) {
|
||||
setIsLoading(loading);
|
||||
}
|
||||
}, [isLoading, loading]);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{header}
|
||||
@@ -560,6 +562,13 @@ const Select = ({
|
||||
labelInValue={isAsync || labelInValue}
|
||||
maxTagCount={MAX_TAG_COUNT}
|
||||
mode={mappedMode}
|
||||
notFoundContent={
|
||||
allowNewOptions && !fetchOnlyOnSearch ? (
|
||||
<StyledLoadingText>{t('Loading...')}</StyledLoadingText>
|
||||
) : (
|
||||
notFoundContent
|
||||
)
|
||||
}
|
||||
onDeselect={handleOnDeselect}
|
||||
onDropdownVisibleChange={handleOnDropdownVisibleChange}
|
||||
onInputKeyDown={onInputKeyDown}
|
||||
@@ -568,7 +577,7 @@ const Select = ({
|
||||
onSelect={handleOnSelect}
|
||||
onClear={handleClear}
|
||||
onChange={onChange}
|
||||
options={selectOptions}
|
||||
options={shouldUseChildrenOptions ? undefined : selectOptions}
|
||||
placeholder={placeholder}
|
||||
showSearch={shouldShowSearch}
|
||||
showArrow
|
||||
@@ -583,7 +592,21 @@ const Select = ({
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{shouldUseChildrenOptions &&
|
||||
selectOptions.map(opt => {
|
||||
const isOptObject = typeof opt === 'object';
|
||||
const label = isOptObject ? opt?.label || opt.value : opt;
|
||||
const value = (isOptObject && opt.value) || opt;
|
||||
const { customLabel, ...optProps } = opt;
|
||||
|
||||
return (
|
||||
<Option {...optProps} key={value} label={label} value={value}>
|
||||
{isOptObject && customLabel ? customLabel : label}
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</StyledSelect>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -67,8 +67,8 @@ export function hasOption(search: string, options: AntdOptionsType) {
|
||||
const labelText = String(label);
|
||||
const valueText = String(value);
|
||||
return (
|
||||
valueText.toLowerCase().includes(searchOption) ||
|
||||
labelText.toLowerCase().includes(searchOption)
|
||||
valueText.toLowerCase() === searchOption ||
|
||||
labelText.toLowerCase() === searchOption
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -722,15 +722,14 @@ const FiltersConfigForm = (
|
||||
!doLoadedDatasetsHaveTemporalColumns;
|
||||
return {
|
||||
value: filterType,
|
||||
label: isDisabled ? (
|
||||
label: mappedName || name,
|
||||
customLabel: isDisabled ? (
|
||||
<Tooltip
|
||||
title={t('Datasets do not contain a temporal column')}
|
||||
>
|
||||
{mappedName || name}
|
||||
</Tooltip>
|
||||
) : (
|
||||
mappedName || name
|
||||
),
|
||||
) : undefined,
|
||||
disabled: isDisabled,
|
||||
};
|
||||
})}
|
||||
|
||||
@@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
|
||||
import { isFunction } from 'lodash';
|
||||
import { Select } from 'src/components';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import { t } from '@superset-ui/core';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
|
||||
const propTypes = {
|
||||
@@ -112,10 +113,12 @@ export default class ColorSchemeControl extends React.PureComponent {
|
||||
const options = (isFunction(choices) ? choices() : choices).map(
|
||||
([value]) => ({
|
||||
value,
|
||||
label: this.renderOption(value),
|
||||
label: this.schemes?.[value]?.label || value,
|
||||
customLabel: this.renderOption(value),
|
||||
}),
|
||||
);
|
||||
const selectProps = {
|
||||
ariaLabel: t('Select color scheme'),
|
||||
allowClear: this.props.clearable,
|
||||
defaultValue: this.props.default,
|
||||
name: `select-${this.props.name}`,
|
||||
|
||||
Reference in New Issue
Block a user