diff --git a/superset-frontend/src/addSlice/AddSliceContainer.tsx b/superset-frontend/src/addSlice/AddSliceContainer.tsx
index 8440cecf5a3..8dc1cab8f4f 100644
--- a/superset-frontend/src/addSlice/AddSliceContainer.tsx
+++ b/superset-frontend/src/addSlice/AddSliceContainer.tsx
@@ -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')}
diff --git a/superset-frontend/src/components/Select/Select.stories.tsx b/superset-frontend/src/components/Select/Select.stories.tsx
index b2582326722..938cc040cad 100644
--- a/superset-frontend/src/components/Select/Select.stories.tsx
+++ b/superset-frontend/src/components/Select/Select.stories.tsx
@@ -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:
JSX Label
,
+ 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 = {
diff --git a/superset-frontend/src/components/Select/Select.test.tsx b/superset-frontend/src/components/Select/Select.test.tsx
index 80f4c2add6a..cf26babc4dc 100644
--- a/superset-frontend/src/components/Select/Select.test.tsx
+++ b/superset-frontend/src/components/Select/Select.test.tsx
@@ -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();
+ await open();
+ expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument();
+});
+
test('inverts the selection', async () => {
render();
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();
+ 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: John
},
+ { label: 'Liam', value: 2, customLabel: Liam
},
+ { label: 'Olivia', value: 3, customLabel: Olivia
},
+ ];
+ render();
+ 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: John
},
+ { label: 'Liam', value: 2, customLabel: Liam
},
+ { label: 'Olivia', value: 3, customLabel: Olivia
},
+ ];
+ render();
+ 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();
+ 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();
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();
+ 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();
+ 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();
+ 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();
+ 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();
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(
+ ,
+ );
+ 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();
+ await open();
+ await type(NEW_OPTION);
+ expect(screen.queryByText(NO_DATA)).not.toBeInTheDocument();
+});
+
test('async - sets a initial value in single mode', async () => {
render();
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
+ */
diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx
index 2ff9e6b368d..cde4d950594 100644
--- a/superset-frontend/src/components/Select/Select.tsx
+++ b/superset-frontend/src/components/Select/Select.tsx
@@ -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;
type PickedSelectProps = Pick<
@@ -60,7 +62,12 @@ type PickedSelectProps = Pick<
| 'value'
>;
-export type OptionsType = Exclude;
+type OptionsProps = Exclude;
+
+export interface OptionsType extends Omit {
+ 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(
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) => {
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 },
) => {
@@ -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 (
{header}
@@ -560,6 +562,13 @@ const Select = ({
labelInValue={isAsync || labelInValue}
maxTagCount={MAX_TAG_COUNT}
mode={mappedMode}
+ notFoundContent={
+ allowNewOptions && !fetchOnlyOnSearch ? (
+ {t('Loading...')}
+ ) : (
+ 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 (
+
+ );
+ })}
+
);
};
diff --git a/superset-frontend/src/components/Select/utils.ts b/superset-frontend/src/components/Select/utils.ts
index 54a7caf9b18..71a90452059 100644
--- a/superset-frontend/src/components/Select/utils.ts
+++ b/superset-frontend/src/components/Select/utils.ts
@@ -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
);
});
}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
index 210739b0a0e..dba9cc66b90 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
@@ -722,15 +722,14 @@ const FiltersConfigForm = (
!doLoadedDatasetsHaveTemporalColumns;
return {
value: filterType,
- label: isDisabled ? (
+ label: mappedName || name,
+ customLabel: isDisabled ? (
{mappedName || name}
- ) : (
- mappedName || name
- ),
+ ) : undefined,
disabled: isDisabled,
};
})}
diff --git a/superset-frontend/src/explore/components/controls/ColorSchemeControl.jsx b/superset-frontend/src/explore/components/controls/ColorSchemeControl.jsx
index cf1d4255208..da363aa34f1 100644
--- a/superset-frontend/src/explore/components/controls/ColorSchemeControl.jsx
+++ b/superset-frontend/src/explore/components/controls/ColorSchemeControl.jsx
@@ -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}`,