Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
cdab55b
Created AsyncSelect Component
cccs-RyanK Jun 17, 2022
df65c03
Merge branch 'master' of github.com:CybercentreCanada/superset into C…
cccs-RyanK Jun 22, 2022
3d6319a
modified import of AsyncSelect, removed async tests and prefixes from…
cccs-RyanK Jun 23, 2022
967f1b5
fixed various import and lint warnings
cccs-RyanK Jun 27, 2022
0caf31b
Merge branch 'master' of github.com:CybercentreCanada/superset into C…
cccs-RyanK Jun 29, 2022
cc549a5
fixing lint errors
cccs-RyanK Jun 29, 2022
7a165b4
fixed frontend test errors
cccs-RyanK Jul 6, 2022
684852a
fixed alertreportmodel tests
cccs-RyanK Jul 6, 2022
56c20f7
Merge branch 'master' into CLDN-1484-Split-Select-Component-into-Asyn…
cccs-RyanK Jul 7, 2022
405e8e4
removed accidental import
cccs-RyanK Jul 7, 2022
593c521
fixed lint errors
cccs-RyanK Jul 7, 2022
0483fe7
updated async select
cccs-RyanK Jul 7, 2022
e43cdd3
Merge branch 'master' of github.com:CybercentreCanada/superset into C…
cccs-RyanK Jul 7, 2022
251b0ea
removed code from select component
cccs-RyanK Jul 11, 2022
38ea4b6
Merge branches 'Remove-Unecessary-Code-from-Async-and-Sync-Select-Com…
cccs-RyanK Jul 11, 2022
519ad38
fixed select test
cccs-RyanK Jul 11, 2022
6ee9849
fixed async label value and select initial values
cccs-RyanK Jul 12, 2022
f1fbcfc
Merge branch 'master' of github.com:CybercentreCanada/superset into R…
cccs-RyanK Jul 12, 2022
a97fca5
cleaned up async test
cccs-RyanK Jul 12, 2022
a8bc98f
fixed lint errors
cccs-RyanK Jul 12, 2022
0dc06dd
minor fixes to sync select component
cccs-RyanK Jul 14, 2022
d7bc113
Merge branch 'master' of github.com:CybercentreCanada/superset into R…
cccs-RyanK Jul 19, 2022
92a4594
removed unecessary variables and fixed linting
cccs-RyanK Jul 19, 2022
0dcd36c
fixed npm test errors
cccs-RyanK Jul 20, 2022
bf5176a
fixed linting issues
cccs-RyanK Jul 20, 2022
7a6cf25
Merge branch 'master' of github.com:CybercentreCanada/superset into R…
cccs-RyanK Jul 21, 2022
78500f7
fixed showSearch and storybook
cccs-RyanK Jul 27, 2022
eb04cbb
fixed linting
cccs-RyanK Jul 27, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,6 @@ export default function DatabaseSelector({
disabled={!currentDb || readOnly}
header={<FormLabel>{t('Schema')}</FormLabel>}
labelInValue
lazyLoading={false}
loading={loadingSchemas}
name="select-schema"
placeholder={t('Select schema or type schema name')}
Expand Down
42 changes: 29 additions & 13 deletions superset-frontend/src/components/ListView/Filters/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { t } from '@superset-ui/core';
import { Select } from 'src/components';
import { Filter, SelectOption } from 'src/components/ListView/types';
import { FormLabel } from 'src/components/Form';
import AsyncSelect from 'src/components/Select/AsyncSelect';
import { FilterContainer, BaseFilter, FilterHandler } from './Base';

interface SelectFilterProps extends BaseFilter {
Expand Down Expand Up @@ -86,19 +87,34 @@ function SelectFilter(

return (
<FilterContainer>
<Select
allowClear
ariaLabel={typeof Header === 'string' ? Header : name || t('Filter')}
labelInValue
data-test="filters-select"
header={<FormLabel>{Header}</FormLabel>}
onChange={onChange}
onClear={onClear}
options={fetchSelects ? fetchAndFormatSelects : selects}
placeholder={t('Select or type a value')}
showSearch
value={selectedOption}
/>
{fetchSelects ? (
<AsyncSelect
allowClear
ariaLabel={typeof Header === 'string' ? Header : name || t('Filter')}
data-test="filters-select"
header={<FormLabel>{Header}</FormLabel>}
onChange={onChange}
onClear={onClear}
options={fetchAndFormatSelects}
placeholder={t('Select or type a value')}
showSearch
value={selectedOption}
/>
) : (
<Select
allowClear
ariaLabel={typeof Header === 'string' ? Header : name || t('Filter')}
data-test="filters-select"
header={<FormLabel>{Header}</FormLabel>}
labelInValue
onChange={onChange}
onClear={onClear}
options={selects}
placeholder={t('Select or type a value')}
showSearch
value={selectedOption}
/>
)}
</FilterContainer>
);
}
Expand Down
150 changes: 102 additions & 48 deletions superset-frontend/src/components/Select/AsyncSelect.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,16 @@ const loadOptions = async (search: string, page: number, pageSize: number) => {
const start = page * pageSize;
const deleteCount =
start + pageSize < totalCount ? pageSize : totalCount - start;
const data = OPTIONS.filter(option => option.label.match(search)).splice(
start,
deleteCount,
);
const searchValue = search.trim().toLowerCase();
const optionFilterProps = ['label', 'value', 'gender'];
const data = OPTIONS.filter(option =>
optionFilterProps.some(prop => {
const optionProp = option?.[prop]
? String(option[prop]).trim().toLowerCase()
: '';
return optionProp.includes(searchValue);
}),
).splice(start, deleteCount);
return {
data,
totalCount: OPTIONS.length,
Expand All @@ -74,7 +80,7 @@ const defaultProps = {
allowClear: true,
ariaLabel: ARIA_LABEL,
labelInValue: true,
options: OPTIONS,
options: loadOptions,
pageSize: 10,
showSearch: true,
};
Expand Down Expand Up @@ -129,24 +135,52 @@ test('displays a header', async () => {
expect(screen.getByText(headerText)).toBeInTheDocument();
});

test('adds a new option if the value is not in the options', async () => {
const { rerender } = render(
<AsyncSelect {...defaultProps} options={[]} value={OPTIONS[0]} />,
test('adds a new option if the value is not in the options, when options are empty', async () => {
const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 }));
render(
<AsyncSelect {...defaultProps} options={loadOptions} value={OPTIONS[0]} />,
);
await open();
expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument();
const options = await findAllSelectOptions();
expect(options).toHaveLength(1);
options.forEach((option, i) =>
expect(option).toHaveTextContent(OPTIONS[i].label),
);
});

rerender(
<AsyncSelect {...defaultProps} options={[OPTIONS[1]]} value={OPTIONS[0]} />,
test('adds a new option if the value is not in the options, when options have values', async () => {
const loadOptions = jest.fn(async () => ({
data: [OPTIONS[1]],
totalCount: 1,
}));
render(
<AsyncSelect {...defaultProps} options={loadOptions} value={OPTIONS[0]} />,
);
await open();
expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument();
expect(await findSelectOption(OPTIONS[1].label)).toBeInTheDocument();
const options = await findAllSelectOptions();
expect(options).toHaveLength(2);
options.forEach((option, i) =>
expect(option).toHaveTextContent(OPTIONS[i].label),
);
});

test('does not add a new option if the value is already in the options', async () => {
const loadOptions = jest.fn(async () => ({
data: [OPTIONS[0]],
totalCount: 1,
}));
render(
<AsyncSelect {...defaultProps} options={loadOptions} value={OPTIONS[0]} />,
);
await open();
expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument();
const options = await findAllSelectOptions();
expect(options).toHaveLength(1);
});

test('inverts the selection', async () => {
render(<AsyncSelect {...defaultProps} invertSelection />);
await open();
Expand All @@ -155,8 +189,11 @@ test('inverts the selection', async () => {
});

test('sort the options by label if no sort comparator is provided', async () => {
const unsortedOptions = [...OPTIONS].sort(() => Math.random());
render(<AsyncSelect {...defaultProps} options={unsortedOptions} />);
const loadUnsortedOptions = jest.fn(async () => ({
data: [...OPTIONS].sort(() => Math.random()),
totalCount: 2,
}));
render(<AsyncSelect {...defaultProps} options={loadUnsortedOptions} />);
await open();
const options = await findAllSelectOptions();
options.forEach((option, key) =>
Expand Down Expand Up @@ -250,20 +287,23 @@ test('searches for label or value', async () => {
render(<AsyncSelect {...defaultProps} />);
const search = option.value;
await type(search.toString());
expect(await findSelectOption(option.label)).toBeInTheDocument();
const options = await findAllSelectOptions();
expect(options.length).toBe(1);
expect(options[0]).toHaveTextContent(option.label);
});

test('search order exact and startWith match first', async () => {
render(<AsyncSelect {...defaultProps} />);
render(<AsyncSelect {...defaultProps} options={loadOptions} />);
await open();
await type('Her');
expect(await findSelectOption('Guilherme')).toBeInTheDocument();
const options = await findAllSelectOptions();
expect(options.length).toBe(4);
expect(options[0]?.textContent).toEqual('Her');
expect(options[1]?.textContent).toEqual('Herme');
expect(options[2]?.textContent).toEqual('Cher');
expect(options[3]?.textContent).toEqual('Guilherme');
expect(options[0]).toHaveTextContent('Her');
expect(options[1]).toHaveTextContent('Herme');
expect(options[2]).toHaveTextContent('Cher');
expect(options[3]).toHaveTextContent('Guilherme');
});

test('ignores case when searching', async () => {
Expand All @@ -273,17 +313,16 @@ test('ignores case when searching', async () => {
});

test('same case should be ranked to the top', async () => {
render(
<AsyncSelect
{...defaultProps}
options={[
{ value: 'Cac' },
{ value: 'abac' },
{ value: 'acbc' },
{ value: 'CAc' },
]}
/>,
);
const loadOptions = jest.fn(async () => ({
data: [
{ value: 'Cac' },
{ value: 'abac' },
{ value: 'acbc' },
{ value: 'CAc' },
],
totalCount: 4,
}));
render(<AsyncSelect {...defaultProps} options={loadOptions} />);
await type('Ac');
const options = await findAllSelectOptions();
expect(options.length).toBe(4);
Expand All @@ -294,7 +333,7 @@ test('same case should be ranked to the top', async () => {
});

test('ignores special keys when searching', async () => {
render(<AsyncSelect {...defaultProps} />);
render(<AsyncSelect {...defaultProps} options={loadOptions} />);
await type('{shift}');
expect(screen.queryByText(LOADING)).not.toBeInTheDocument();
});
Expand All @@ -303,11 +342,16 @@ test('searches for custom fields', async () => {
render(
<AsyncSelect {...defaultProps} optionFilterProps={['label', 'gender']} />,
);
await open();
await type('Liam');
// Liam is on the second page. need to wait to fetch options
expect(await findSelectOption('Liam')).toBeInTheDocument();
let options = await findAllSelectOptions();
expect(options.length).toBe(1);
expect(options[0]).toHaveTextContent('Liam');
await type('Female');
// Olivia is on the second page. need to wait to fetch options
expect(await findSelectOption('Olivia')).toBeInTheDocument();
options = await findAllSelectOptions();
expect(options.length).toBe(6);
expect(options[0]).toHaveTextContent('Ava');
Expand All @@ -317,7 +361,7 @@ test('searches for custom fields', async () => {
expect(options[4]).toHaveTextContent('Nikole');
expect(options[5]).toHaveTextContent('Olivia');
await type('1');
expect(screen.getByText(NO_DATA)).toBeInTheDocument();
expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
});

test('removes duplicated values', async () => {
Expand All @@ -332,25 +376,31 @@ test('removes duplicated values', async () => {
});

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(<AsyncSelect {...defaultProps} options={options} />);
const loadOptions = jest.fn(async () => ({
data: [
{ label: 'John', value: 1, customLabel: <h1>John</h1> },
{ label: 'Liam', value: 2, customLabel: <h1>Liam</h1> },
{ label: 'Olivia', value: 3, customLabel: <h1>Olivia</h1> },
],
totalCount: 3,
}));
render(<AsyncSelect {...defaultProps} options={loadOptions} />);
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(<AsyncSelect {...defaultProps} options={options} />);
const loadOptions = jest.fn(async () => ({
data: [
{ label: 'John', value: 1, customLabel: <h1>John</h1> },
{ label: 'Liam', value: 2, customLabel: <h1>Liam</h1> },
{ label: 'Olivia', value: 3, customLabel: <h1>Olivia</h1> },
],
totalCount: 3,
}));
render(<AsyncSelect {...defaultProps} options={loadOptions} />);
await type('Liam');
const selectOptions = await findAllSelectOptions();
expect(selectOptions.length).toBe(1);
Expand Down Expand Up @@ -391,20 +441,24 @@ test('does not add a new option if allowNewOptions is false', async () => {
});

test('adds the null option when selected in single mode', async () => {
render(<AsyncSelect {...defaultProps} options={[OPTIONS[0], NULL_OPTION]} />);
const loadOptions = jest.fn(async () => ({
data: [OPTIONS[0], NULL_OPTION],
totalCount: 2,
}));
render(<AsyncSelect {...defaultProps} options={loadOptions} />);
await open();
userEvent.click(await findSelectOption(NULL_OPTION.label));
const values = await findAllSelectValues();
expect(values[0]).toHaveTextContent(NULL_OPTION.label);
});

test('adds the null option when selected in multiple mode', async () => {
const loadOptions = jest.fn(async () => ({
data: [OPTIONS[0], NULL_OPTION],
totalCount: 2,
}));
render(
<AsyncSelect
{...defaultProps}
options={[OPTIONS[0], NULL_OPTION, OPTIONS[2]]}
mode="multiple"
/>,
<AsyncSelect {...defaultProps} options={loadOptions} mode="multiple" />,
);
await open();
userEvent.click(await findSelectOption(OPTIONS[0].label));
Expand Down
Loading