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(); @@ -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 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(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); + 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(LOADING)).not.toBeInTheDocument(); +}); + +test('static - shows "Loading..." when allowNewOptions is true and a new option is entered', 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(screen.queryByText(NO_DATA)).not.toBeInTheDocument(); +}); + test('async - sets a initial value in single mode', async () => { render(