mirror of
https://github.com/apache/superset.git
synced 2024-09-12 08:39:45 -04:00
fix: Order of Select items when unselecting (#17169)
* fix: Order of Select items when unselecting * Adds a property comparator that supports strings and numbers * Uses a named export for propertyComparator
This commit is contained in:
parent
ef3afbde82
commit
55be249870
@ -256,15 +256,11 @@ export default class AddSliceContainer extends React.PureComponent<
|
|||||||
customLabel: ReactNode;
|
customLabel: ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
}[] = response.json.result
|
}[] = response.json.result.map((item: Dataset) => ({
|
||||||
.map((item: Dataset) => ({
|
value: `${item.id}__${item.datasource_type}`,
|
||||||
value: `${item.id}__${item.datasource_type}`,
|
customLabel: this.newLabel(item),
|
||||||
customLabel: this.newLabel(item),
|
label: item.table_name,
|
||||||
label: item.table_name,
|
}));
|
||||||
}))
|
|
||||||
.sort((a: { label: string }, b: { label: string }) =>
|
|
||||||
a.label.localeCompare(b.label),
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
data: list,
|
data: list,
|
||||||
totalCount: response.json.count,
|
totalCount: response.json.count,
|
||||||
|
@ -201,22 +201,18 @@ export default function DatabaseSelector({
|
|||||||
// TODO: Would be nice to add pagination in a follow-up. Needs endpoint changes.
|
// TODO: Would be nice to add pagination in a follow-up. Needs endpoint changes.
|
||||||
SupersetClient.get({ endpoint })
|
SupersetClient.get({ endpoint })
|
||||||
.then(({ json }) => {
|
.then(({ json }) => {
|
||||||
const options = json.result
|
const options = json.result.map((s: string) => ({
|
||||||
.map((s: string) => ({
|
value: s,
|
||||||
value: s,
|
label: s,
|
||||||
label: s,
|
title: s,
|
||||||
title: s,
|
}));
|
||||||
}))
|
|
||||||
.sort((a: { label: string }, b: { label: string }) =>
|
|
||||||
a.label.localeCompare(b.label),
|
|
||||||
);
|
|
||||||
if (onSchemasLoad) {
|
if (onSchemasLoad) {
|
||||||
onSchemasLoad(options);
|
onSchemasLoad(options);
|
||||||
}
|
}
|
||||||
setSchemaOptions(options);
|
setSchemaOptions(options);
|
||||||
setLoadingSchemas(false);
|
setLoadingSchemas(false);
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(() => {
|
||||||
setLoadingSchemas(false);
|
setLoadingSchemas(false);
|
||||||
handleError(t('There was an error loading the schemas'));
|
handleError(t('There was an error loading the schemas'));
|
||||||
});
|
});
|
||||||
|
@ -313,7 +313,7 @@ const USERS = [
|
|||||||
'Claire',
|
'Claire',
|
||||||
'Benedetta',
|
'Benedetta',
|
||||||
'Ilenia',
|
'Ilenia',
|
||||||
];
|
].sort();
|
||||||
|
|
||||||
export const AsyncSelect = ({
|
export const AsyncSelect = ({
|
||||||
fetchOnlyOnSearch,
|
fetchOnlyOnSearch,
|
||||||
@ -429,6 +429,7 @@ export const AsyncSelect = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
AsyncSelect.args = {
|
AsyncSelect.args = {
|
||||||
|
allowClear: false,
|
||||||
allowNewOptions: false,
|
allowNewOptions: false,
|
||||||
fetchOnlyOnSearch: false,
|
fetchOnlyOnSearch: false,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
|
@ -52,7 +52,7 @@ const OPTIONS = [
|
|||||||
{ label: 'Irfan', value: 18, gender: 'Male' },
|
{ label: 'Irfan', value: 18, gender: 'Male' },
|
||||||
{ label: 'George', value: 19, gender: 'Male' },
|
{ label: 'George', value: 19, gender: 'Male' },
|
||||||
{ label: 'Ashfaq', value: 20, gender: 'Male' },
|
{ label: 'Ashfaq', value: 20, gender: 'Male' },
|
||||||
];
|
].sort((option1, option2) => option1.label.localeCompare(option2.label));
|
||||||
|
|
||||||
const loadOptions = async (search: string, page: number, pageSize: number) => {
|
const loadOptions = async (search: string, page: number, pageSize: number) => {
|
||||||
const totalCount = OPTIONS.length;
|
const totalCount = OPTIONS.length;
|
||||||
@ -100,6 +100,8 @@ const findSelectValue = () =>
|
|||||||
const findAllSelectValues = () =>
|
const findAllSelectValues = () =>
|
||||||
waitFor(() => getElementsByClassName('.ant-select-selection-item'));
|
waitFor(() => getElementsByClassName('.ant-select-selection-item'));
|
||||||
|
|
||||||
|
const clearAll = () => userEvent.click(screen.getByLabelText('close-circle'));
|
||||||
|
|
||||||
const type = (text: string) => {
|
const type = (text: string) => {
|
||||||
const select = getSelect();
|
const select = getSelect();
|
||||||
userEvent.clear(select);
|
userEvent.clear(select);
|
||||||
@ -127,6 +129,37 @@ test('inverts the selection', async () => {
|
|||||||
expect(await screen.findByLabelText('stop')).toBeInTheDocument();
|
expect(await screen.findByLabelText('stop')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('sort the options by label if no sort comparator is provided', async () => {
|
||||||
|
const unsortedOptions = [...OPTIONS].sort(() => Math.random());
|
||||||
|
render(<Select {...defaultProps} options={unsortedOptions} />);
|
||||||
|
await open();
|
||||||
|
const options = await findAllSelectOptions();
|
||||||
|
options.forEach((option, key) =>
|
||||||
|
expect(option).toHaveTextContent(OPTIONS[key].label),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sort the options using a custom sort comparator', async () => {
|
||||||
|
const sortComparator = (
|
||||||
|
option1: typeof OPTIONS[0],
|
||||||
|
option2: typeof OPTIONS[0],
|
||||||
|
) => option1.gender.localeCompare(option2.gender);
|
||||||
|
render(
|
||||||
|
<Select
|
||||||
|
{...defaultProps}
|
||||||
|
options={loadOptions}
|
||||||
|
sortComparator={sortComparator}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
await open();
|
||||||
|
const options = await findAllSelectOptions();
|
||||||
|
const optionsPage = OPTIONS.slice(0, defaultProps.pageSize);
|
||||||
|
const sortedOptions = optionsPage.sort(sortComparator);
|
||||||
|
options.forEach((option, key) =>
|
||||||
|
expect(option).toHaveTextContent(sortedOptions[key].label),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('displays the selected values first', async () => {
|
test('displays the selected values first', async () => {
|
||||||
render(<Select {...defaultProps} mode="multiple" />);
|
render(<Select {...defaultProps} mode="multiple" />);
|
||||||
const option3 = OPTIONS[2].label;
|
const option3 = OPTIONS[2].label;
|
||||||
@ -141,6 +174,22 @@ test('displays the selected values first', async () => {
|
|||||||
expect(sortedOptions[1]).toHaveTextContent(option8);
|
expect(sortedOptions[1]).toHaveTextContent(option8);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('displays the original order when unselecting', async () => {
|
||||||
|
render(<Select {...defaultProps} mode="multiple" />);
|
||||||
|
const option3 = OPTIONS[2].label;
|
||||||
|
const option8 = OPTIONS[7].label;
|
||||||
|
await open();
|
||||||
|
userEvent.click(await findSelectOption(option3));
|
||||||
|
userEvent.click(await findSelectOption(option8));
|
||||||
|
await type('{esc}');
|
||||||
|
clearAll();
|
||||||
|
await open();
|
||||||
|
const options = await findAllSelectOptions();
|
||||||
|
options.forEach((option, key) =>
|
||||||
|
expect(option).toHaveTextContent(OPTIONS[key].label),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('searches for label or value', async () => {
|
test('searches for label or value', async () => {
|
||||||
const option = OPTIONS[11];
|
const option = OPTIONS[11];
|
||||||
render(<Select {...defaultProps} />);
|
render(<Select {...defaultProps} />);
|
||||||
@ -172,11 +221,11 @@ test('searches for custom fields', async () => {
|
|||||||
await type('Female');
|
await type('Female');
|
||||||
options = await findAllSelectOptions();
|
options = await findAllSelectOptions();
|
||||||
expect(options.length).toBe(5);
|
expect(options.length).toBe(5);
|
||||||
expect(options[0]).toHaveTextContent('Olivia');
|
expect(options[0]).toHaveTextContent('Ava');
|
||||||
expect(options[1]).toHaveTextContent('Emma');
|
expect(options[1]).toHaveTextContent('Charlotte');
|
||||||
expect(options[2]).toHaveTextContent('Ava');
|
expect(options[2]).toHaveTextContent('Emma');
|
||||||
expect(options[3]).toHaveTextContent('Charlotte');
|
expect(options[3]).toHaveTextContent('Nikole');
|
||||||
expect(options[4]).toHaveTextContent('Nikole');
|
expect(options[4]).toHaveTextContent('Olivia');
|
||||||
await type('1');
|
await type('1');
|
||||||
expect(screen.getByText(NO_DATA)).toBeInTheDocument();
|
expect(screen.getByText(NO_DATA)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@ -227,7 +276,7 @@ test('clear all the values', async () => {
|
|||||||
onClear={onClear}
|
onClear={onClear}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
userEvent.click(screen.getByLabelText('close-circle'));
|
clearAll();
|
||||||
expect(onClear).toHaveBeenCalled();
|
expect(onClear).toHaveBeenCalled();
|
||||||
const values = await findAllSelectValues();
|
const values = await findAllSelectValues();
|
||||||
expect(values.length).toBe(0);
|
expect(values.length).toBe(0);
|
||||||
@ -378,8 +427,8 @@ test('static - searches for an item', async () => {
|
|||||||
await type(search);
|
await type(search);
|
||||||
const options = await findAllSelectOptions();
|
const options = await findAllSelectOptions();
|
||||||
expect(options.length).toBe(2);
|
expect(options.length).toBe(2);
|
||||||
expect(options[0]).toHaveTextContent('Olivia');
|
expect(options[0]).toHaveTextContent('Oliver');
|
||||||
expect(options[1]).toHaveTextContent('Oliver');
|
expect(options[1]).toHaveTextContent('Olivia');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('async - renders the select with default props', () => {
|
test('async - renders the select with default props', () => {
|
||||||
@ -546,8 +595,8 @@ test('async - searches for an item already loaded', async () => {
|
|||||||
await waitForElementToBeRemoved(screen.getByText(LOADING));
|
await waitForElementToBeRemoved(screen.getByText(LOADING));
|
||||||
const options = await findAllSelectOptions();
|
const options = await findAllSelectOptions();
|
||||||
expect(options.length).toBe(2);
|
expect(options.length).toBe(2);
|
||||||
expect(options[0]).toHaveTextContent('Olivia');
|
expect(options[0]).toHaveTextContent('Oliver');
|
||||||
expect(options[1]).toHaveTextContent('Oliver');
|
expect(options[1]).toHaveTextContent('Olivia');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('async - searches for an item in a page not loaded', async () => {
|
test('async - searches for an item in a page not loaded', async () => {
|
||||||
@ -599,7 +648,7 @@ test('async - does not fire a new request for the same search input', async () =
|
|||||||
await type('search');
|
await type('search');
|
||||||
expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
|
expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
|
||||||
expect(loadOptions).toHaveBeenCalledTimes(1);
|
expect(loadOptions).toHaveBeenCalledTimes(1);
|
||||||
userEvent.click(screen.getByLabelText('close-circle'));
|
clearAll();
|
||||||
await type('search');
|
await type('search');
|
||||||
expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
|
expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
|
||||||
expect(loadOptions).toHaveBeenCalledTimes(1);
|
expect(loadOptions).toHaveBeenCalledTimes(1);
|
||||||
|
@ -94,6 +94,7 @@ export interface SelectProps extends PickedSelectProps {
|
|||||||
invertSelection?: boolean;
|
invertSelection?: boolean;
|
||||||
fetchOnlyOnSearch?: boolean;
|
fetchOnlyOnSearch?: boolean;
|
||||||
onError?: (error: string) => void;
|
onError?: (error: string) => void;
|
||||||
|
sortComparator?: (a: AntdLabeledValue, b: AntdLabeledValue) => number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -168,6 +169,30 @@ const Error = ({ error }: { error: string }) => (
|
|||||||
</StyledError>
|
</StyledError>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const defaultSortComparator = (a: AntdLabeledValue, b: AntdLabeledValue) => {
|
||||||
|
if (typeof a.label === 'string' && typeof b.label === 'string') {
|
||||||
|
return a.label.localeCompare(b.label);
|
||||||
|
}
|
||||||
|
if (typeof a.value === 'string' && typeof b.value === 'string') {
|
||||||
|
return a.value.localeCompare(b.value);
|
||||||
|
}
|
||||||
|
return (a.value as number) - (b.value as number);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It creates a comparator to check for a specific property.
|
||||||
|
* Can be used with string and number property values.
|
||||||
|
* */
|
||||||
|
export const propertyComparator = (property: string) => (
|
||||||
|
a: AntdLabeledValue,
|
||||||
|
b: AntdLabeledValue,
|
||||||
|
) => {
|
||||||
|
if (typeof a[property] === 'string' && typeof b[property] === 'string') {
|
||||||
|
return a[property].localeCompare(b[property]);
|
||||||
|
}
|
||||||
|
return (a[property] as number) - (b[property] as number);
|
||||||
|
};
|
||||||
|
|
||||||
const Select = ({
|
const Select = ({
|
||||||
allowNewOptions = false,
|
allowNewOptions = false,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
@ -189,6 +214,7 @@ const Select = ({
|
|||||||
pageSize = DEFAULT_PAGE_SIZE,
|
pageSize = DEFAULT_PAGE_SIZE,
|
||||||
placeholder = t('Select ...'),
|
placeholder = t('Select ...'),
|
||||||
showSearch = true,
|
showSearch = true,
|
||||||
|
sortComparator = defaultSortComparator,
|
||||||
value,
|
value,
|
||||||
...props
|
...props
|
||||||
}: SelectProps) => {
|
}: SelectProps) => {
|
||||||
@ -277,14 +303,21 @@ const Select = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const sortedOptions = [
|
||||||
const sortedOptions = [...topOptions, ...otherOptions];
|
...topOptions.sort(sortComparator),
|
||||||
|
...otherOptions.sort(sortComparator),
|
||||||
|
];
|
||||||
|
if (!isEqual(sortedOptions, selectOptions)) {
|
||||||
|
setSelectOptions(sortedOptions);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const sortedOptions = [...selectOptions].sort(sortComparator);
|
||||||
if (!isEqual(sortedOptions, selectOptions)) {
|
if (!isEqual(sortedOptions, selectOptions)) {
|
||||||
setSelectOptions(sortedOptions);
|
setSelectOptions(sortedOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isAsync, isSingleMode, labelInValue, selectOptions],
|
[isAsync, isSingleMode, labelInValue, selectOptions, sortComparator],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOnSelect = (
|
const handleOnSelect = (
|
||||||
@ -342,28 +375,34 @@ const Select = ({
|
|||||||
[onError],
|
[onError],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleData = (data: OptionsType) => {
|
const handleData = useCallback(
|
||||||
let mergedData: OptionsType = [];
|
(data: OptionsType) => {
|
||||||
if (data && Array.isArray(data) && data.length) {
|
let mergedData: OptionsType = [];
|
||||||
const dataValues = new Set();
|
if (data && Array.isArray(data) && data.length) {
|
||||||
data.forEach(option =>
|
const dataValues = new Set();
|
||||||
dataValues.add(String(option.value).toLocaleLowerCase()),
|
data.forEach(option =>
|
||||||
);
|
dataValues.add(String(option.value).toLocaleLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
// merges with existing and creates unique options
|
// merges with existing and creates unique options
|
||||||
setSelectOptions(prevOptions => {
|
setSelectOptions(prevOptions => {
|
||||||
mergedData = [
|
mergedData = [
|
||||||
...prevOptions.filter(
|
...prevOptions.filter(
|
||||||
previousOption =>
|
previousOption =>
|
||||||
!dataValues.has(String(previousOption.value).toLocaleLowerCase()),
|
!dataValues.has(
|
||||||
),
|
String(previousOption.value).toLocaleLowerCase(),
|
||||||
...data,
|
),
|
||||||
];
|
),
|
||||||
return mergedData;
|
...data,
|
||||||
});
|
];
|
||||||
}
|
mergedData.sort(sortComparator);
|
||||||
return mergedData;
|
return mergedData;
|
||||||
};
|
});
|
||||||
|
}
|
||||||
|
return mergedData;
|
||||||
|
},
|
||||||
|
[sortComparator],
|
||||||
|
);
|
||||||
|
|
||||||
const handlePaginatedFetch = useMemo(
|
const handlePaginatedFetch = useMemo(
|
||||||
() => (value: string, page: number, pageSize: number) => {
|
() => (value: string, page: number, pageSize: number) => {
|
||||||
@ -401,7 +440,7 @@ const Select = ({
|
|||||||
setIsTyping(false);
|
setIsTyping(false);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[allValuesLoaded, fetchOnlyOnSearch, internalOnError, options],
|
[allValuesLoaded, fetchOnlyOnSearch, handleData, internalOnError, options],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOnSearch = useMemo(
|
const handleOnSearch = useMemo(
|
||||||
@ -474,8 +513,8 @@ const Select = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// multiple or tags mode keep the dropdown visible while selecting options
|
// multiple or tags mode keep the dropdown visible while selecting options
|
||||||
// this waits for the dropdown to be closed before sorting the top options
|
// this waits for the dropdown to be opened before sorting the top options
|
||||||
if (!isSingleMode && !isDropdownVisible) {
|
if (!isSingleMode && isDropdownVisible) {
|
||||||
handleTopOptions(selectValue);
|
handleTopOptions(selectValue);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -209,11 +209,7 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
|
|||||||
if (onTablesLoad) {
|
if (onTablesLoad) {
|
||||||
onTablesLoad(json.options);
|
onTablesLoad(json.options);
|
||||||
}
|
}
|
||||||
setTableOptions(
|
setTableOptions(options);
|
||||||
options.sort((a: { text: string }, b: { text: string }) =>
|
|
||||||
a.text.localeCompare(b.text),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setCurrentTable(currentTable);
|
setCurrentTable(currentTable);
|
||||||
setLoadingTables(false);
|
setLoadingTables(false);
|
||||||
})
|
})
|
||||||
|
@ -83,17 +83,13 @@ ALL_ZONES.forEach(zone => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const TIMEZONE_OPTIONS = TIMEZONES.sort(
|
const TIMEZONE_OPTIONS = TIMEZONES.map(zone => ({
|
||||||
// sort by offset
|
|
||||||
(a, b) =>
|
|
||||||
moment.tz(currentDate, a.name).utcOffset() -
|
|
||||||
moment.tz(currentDate, b.name).utcOffset(),
|
|
||||||
).map(zone => ({
|
|
||||||
label: `GMT ${moment
|
label: `GMT ${moment
|
||||||
.tz(currentDate, zone.name)
|
.tz(currentDate, zone.name)
|
||||||
.format('Z')} (${getTimezoneName(zone.name)})`,
|
.format('Z')} (${getTimezoneName(zone.name)})`,
|
||||||
value: zone.name,
|
value: zone.name,
|
||||||
offsets: getOffsetKey(zone.name),
|
offsets: getOffsetKey(zone.name),
|
||||||
|
timezoneName: zone.name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const TimezoneSelector = ({ onTimezoneChange, timezone }: TimezoneProps) => {
|
const TimezoneSelector = ({ onTimezoneChange, timezone }: TimezoneProps) => {
|
||||||
@ -126,6 +122,13 @@ const TimezoneSelector = ({ onTimezoneChange, timezone }: TimezoneProps) => {
|
|||||||
onChange={onTimezoneChange}
|
onChange={onTimezoneChange}
|
||||||
value={timezone || DEFAULT_TIMEZONE.value}
|
value={timezone || DEFAULT_TIMEZONE.value}
|
||||||
options={TIMEZONE_OPTIONS}
|
options={TIMEZONE_OPTIONS}
|
||||||
|
sortComparator={(
|
||||||
|
a: typeof TIMEZONE_OPTIONS[number],
|
||||||
|
b: typeof TIMEZONE_OPTIONS[number],
|
||||||
|
) =>
|
||||||
|
moment.tz(currentDate, a.timezoneName).utcOffset() -
|
||||||
|
moment.tz(currentDate, b.timezoneName).utcOffset()
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { RefObject } from 'react';
|
import React, { RefObject } from 'react';
|
||||||
import { Select } from 'src/components';
|
import Select, { propertyComparator } from 'src/components/Select/Select';
|
||||||
import { t, styled } from '@superset-ui/core';
|
import { t, styled } from '@superset-ui/core';
|
||||||
import Alert from 'src/components/Alert';
|
import Alert from 'src/components/Alert';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
@ -120,6 +120,7 @@ class RefreshIntervalModal extends React.PureComponent<
|
|||||||
options={options}
|
options={options}
|
||||||
value={refreshFrequency}
|
value={refreshFrequency}
|
||||||
onChange={this.handleFrequencyChange}
|
onChange={this.handleFrequencyChange}
|
||||||
|
sortComparator={propertyComparator('value')}
|
||||||
/>
|
/>
|
||||||
{showRefreshWarning && (
|
{showRefreshWarning && (
|
||||||
<RefreshWarningContainer>
|
<RefreshWarningContainer>
|
||||||
|
@ -73,7 +73,6 @@ export function ColumnSelect({
|
|||||||
ensureIsArray(columns)
|
ensureIsArray(columns)
|
||||||
.filter(filterValues)
|
.filter(filterValues)
|
||||||
.map((col: Column) => col.column_name)
|
.map((col: Column) => col.column_name)
|
||||||
.sort((a: string, b: string) => a.localeCompare(b))
|
|
||||||
.map((column: string) => ({ label: column, value: column })),
|
.map((column: string) => ({ label: column, value: column })),
|
||||||
[columns, filterValues],
|
[columns, filterValues],
|
||||||
);
|
);
|
||||||
|
@ -72,11 +72,7 @@ const DatasetSelect = ({ onChange, value }: DatasetSelectProps) => {
|
|||||||
const data: {
|
const data: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
}[] = response.json.result
|
}[] = response.json.result.map(datasetToSelectOption);
|
||||||
.map(datasetToSelectOption)
|
|
||||||
.sort((a: { label: string }, b: { label: string }) =>
|
|
||||||
a.label.localeCompare(b.label),
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
totalCount: response.json.count,
|
totalCount: response.json.count,
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { styled, t } from '@superset-ui/core';
|
import { styled, t } from '@superset-ui/core';
|
||||||
import { Form, FormItem, FormProps } from 'src/components/Form';
|
import { Form, FormItem, FormProps } from 'src/components/Form';
|
||||||
import { Select } from 'src/components';
|
import Select, { propertyComparator } from 'src/components/Select/Select';
|
||||||
import { Col, InputNumber, Row } from 'src/common/components';
|
import { Col, InputNumber, Row } from 'src/common/components';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
import {
|
import {
|
||||||
@ -44,17 +44,17 @@ const colorSchemeOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const operatorOptions = [
|
const operatorOptions = [
|
||||||
{ value: COMPARATOR.NONE, label: 'None' },
|
{ value: COMPARATOR.NONE, label: 'None', order: 0 },
|
||||||
{ value: COMPARATOR.GREATER_THAN, label: '>' },
|
{ value: COMPARATOR.GREATER_THAN, label: '>', order: 1 },
|
||||||
{ value: COMPARATOR.LESS_THAN, label: '<' },
|
{ value: COMPARATOR.LESS_THAN, label: '<', order: 2 },
|
||||||
{ value: COMPARATOR.GREATER_OR_EQUAL, label: '≥' },
|
{ value: COMPARATOR.GREATER_OR_EQUAL, label: '≥', order: 3 },
|
||||||
{ value: COMPARATOR.LESS_OR_EQUAL, label: '≤' },
|
{ value: COMPARATOR.LESS_OR_EQUAL, label: '≤', order: 4 },
|
||||||
{ value: COMPARATOR.EQUAL, label: '=' },
|
{ value: COMPARATOR.EQUAL, label: '=', order: 5 },
|
||||||
{ value: COMPARATOR.NOT_EQUAL, label: '≠' },
|
{ value: COMPARATOR.NOT_EQUAL, label: '≠', order: 6 },
|
||||||
{ value: COMPARATOR.BETWEEN, label: '< x <' },
|
{ value: COMPARATOR.BETWEEN, label: '< x <', order: 7 },
|
||||||
{ value: COMPARATOR.BETWEEN_OR_EQUAL, label: '≤ x ≤' },
|
{ value: COMPARATOR.BETWEEN_OR_EQUAL, label: '≤ x ≤', order: 8 },
|
||||||
{ value: COMPARATOR.BETWEEN_OR_LEFT_EQUAL, label: '≤ x <' },
|
{ value: COMPARATOR.BETWEEN_OR_LEFT_EQUAL, label: '≤ x <', order: 9 },
|
||||||
{ value: COMPARATOR.BETWEEN_OR_RIGHT_EQUAL, label: '< x ≤' },
|
{ value: COMPARATOR.BETWEEN_OR_RIGHT_EQUAL, label: '< x ≤', order: 10 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const targetValueValidator = (
|
const targetValueValidator = (
|
||||||
@ -126,7 +126,11 @@ const operatorField = (
|
|||||||
rules={rulesRequired}
|
rules={rulesRequired}
|
||||||
initialValue={operatorOptions[0].value}
|
initialValue={operatorOptions[0].value}
|
||||||
>
|
>
|
||||||
<Select ariaLabel={t('Operator')} options={operatorOptions} />
|
<Select
|
||||||
|
ariaLabel={t('Operator')}
|
||||||
|
options={operatorOptions}
|
||||||
|
sortComparator={propertyComparator('order')}
|
||||||
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ import Label, { Type } from 'src/components/Label';
|
|||||||
import Popover from 'src/components/Popover';
|
import Popover from 'src/components/Popover';
|
||||||
import { Divider } from 'src/common/components';
|
import { Divider } from 'src/common/components';
|
||||||
import Icons from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
import { Select } from 'src/components';
|
import Select, { propertyComparator } from 'src/components/Select/Select';
|
||||||
import { Tooltip } from 'src/components/Tooltip';
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
import { DEFAULT_TIME_RANGE } from 'src/explore/constants';
|
import { DEFAULT_TIME_RANGE } from 'src/explore/constants';
|
||||||
import { useDebouncedEffect } from 'src/explore/exploreUtils';
|
import { useDebouncedEffect } from 'src/explore/exploreUtils';
|
||||||
@ -295,6 +295,7 @@ export default function DateFilterLabel(props: DateFilterControlProps) {
|
|||||||
options={FRAME_OPTIONS}
|
options={FRAME_OPTIONS}
|
||||||
value={frame}
|
value={frame}
|
||||||
onChange={onChangeFrame}
|
onChange={onChangeFrame}
|
||||||
|
sortComparator={propertyComparator('order')}
|
||||||
/>
|
/>
|
||||||
{frame !== 'No filter' && <Divider />}
|
{frame !== 'No filter' && <Divider />}
|
||||||
{frame === 'Common' && (
|
{frame === 'Common' && (
|
||||||
|
@ -23,7 +23,7 @@ import { isInteger } from 'lodash';
|
|||||||
import { Col, InputNumber, Row } from 'src/common/components';
|
import { Col, InputNumber, Row } from 'src/common/components';
|
||||||
import { DatePicker } from 'src/components/DatePicker';
|
import { DatePicker } from 'src/components/DatePicker';
|
||||||
import { Radio } from 'src/components/Radio';
|
import { Radio } from 'src/components/Radio';
|
||||||
import { Select } from 'src/components';
|
import Select, { propertyComparator } from 'src/components/Select/Select';
|
||||||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||||
import {
|
import {
|
||||||
SINCE_GRAIN_OPTIONS,
|
SINCE_GRAIN_OPTIONS,
|
||||||
@ -41,6 +41,8 @@ import {
|
|||||||
FrameComponentProps,
|
FrameComponentProps,
|
||||||
} from 'src/explore/components/controls/DateFilterControl/types';
|
} from 'src/explore/components/controls/DateFilterControl/types';
|
||||||
|
|
||||||
|
const sortComparator = propertyComparator('order');
|
||||||
|
|
||||||
export function CustomFrame(props: FrameComponentProps) {
|
export function CustomFrame(props: FrameComponentProps) {
|
||||||
const { customRange, matchedFlag } = customTimeRangeDecode(props.value);
|
const { customRange, matchedFlag } = customTimeRangeDecode(props.value);
|
||||||
if (!matchedFlag) {
|
if (!matchedFlag) {
|
||||||
@ -121,6 +123,7 @@ export function CustomFrame(props: FrameComponentProps) {
|
|||||||
options={SINCE_MODE_OPTIONS}
|
options={SINCE_MODE_OPTIONS}
|
||||||
value={sinceMode}
|
value={sinceMode}
|
||||||
onChange={(value: string) => onChange('sinceMode', value)}
|
onChange={(value: string) => onChange('sinceMode', value)}
|
||||||
|
sortComparator={sortComparator}
|
||||||
/>
|
/>
|
||||||
{sinceMode === 'specific' && (
|
{sinceMode === 'specific' && (
|
||||||
<Row>
|
<Row>
|
||||||
@ -155,6 +158,7 @@ export function CustomFrame(props: FrameComponentProps) {
|
|||||||
options={SINCE_GRAIN_OPTIONS}
|
options={SINCE_GRAIN_OPTIONS}
|
||||||
value={sinceGrain}
|
value={sinceGrain}
|
||||||
onChange={(value: string) => onChange('sinceGrain', value)}
|
onChange={(value: string) => onChange('sinceGrain', value)}
|
||||||
|
sortComparator={sortComparator}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@ -173,6 +177,7 @@ export function CustomFrame(props: FrameComponentProps) {
|
|||||||
options={UNTIL_MODE_OPTIONS}
|
options={UNTIL_MODE_OPTIONS}
|
||||||
value={untilMode}
|
value={untilMode}
|
||||||
onChange={(value: string) => onChange('untilMode', value)}
|
onChange={(value: string) => onChange('untilMode', value)}
|
||||||
|
sortComparator={sortComparator}
|
||||||
/>
|
/>
|
||||||
{untilMode === 'specific' && (
|
{untilMode === 'specific' && (
|
||||||
<Row>
|
<Row>
|
||||||
@ -206,6 +211,7 @@ export function CustomFrame(props: FrameComponentProps) {
|
|||||||
options={UNTIL_GRAIN_OPTIONS}
|
options={UNTIL_GRAIN_OPTIONS}
|
||||||
value={untilGrain}
|
value={untilGrain}
|
||||||
onChange={(value: string) => onChange('untilGrain', value)}
|
onChange={(value: string) => onChange('untilGrain', value)}
|
||||||
|
sortComparator={sortComparator}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
export type SelectOptionType = {
|
export type SelectOptionType = {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
order: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FrameType =
|
export type FrameType =
|
||||||
|
@ -28,28 +28,32 @@ import {
|
|||||||
} from 'src/explore/components/controls/DateFilterControl/types';
|
} from 'src/explore/components/controls/DateFilterControl/types';
|
||||||
|
|
||||||
export const FRAME_OPTIONS: SelectOptionType[] = [
|
export const FRAME_OPTIONS: SelectOptionType[] = [
|
||||||
{ value: 'Common', label: t('Last') },
|
{ value: 'Common', label: t('Last'), order: 0 },
|
||||||
{ value: 'Calendar', label: t('Previous') },
|
{ value: 'Calendar', label: t('Previous'), order: 1 },
|
||||||
{ value: 'Custom', label: t('Custom') },
|
{ value: 'Custom', label: t('Custom'), order: 2 },
|
||||||
{ value: 'Advanced', label: t('Advanced') },
|
{ value: 'Advanced', label: t('Advanced'), order: 3 },
|
||||||
{ value: 'No filter', label: t('No filter') },
|
{ value: 'No filter', label: t('No filter'), order: 4 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const COMMON_RANGE_OPTIONS: SelectOptionType[] = [
|
export const COMMON_RANGE_OPTIONS: SelectOptionType[] = [
|
||||||
{ value: 'Last day', label: t('last day') },
|
{ value: 'Last day', label: t('last day'), order: 0 },
|
||||||
{ value: 'Last week', label: t('last week') },
|
{ value: 'Last week', label: t('last week'), order: 1 },
|
||||||
{ value: 'Last month', label: t('last month') },
|
{ value: 'Last month', label: t('last month'), order: 2 },
|
||||||
{ value: 'Last quarter', label: t('last quarter') },
|
{ value: 'Last quarter', label: t('last quarter'), order: 3 },
|
||||||
{ value: 'Last year', label: t('last year') },
|
{ value: 'Last year', label: t('last year'), order: 4 },
|
||||||
];
|
];
|
||||||
export const COMMON_RANGE_VALUES_SET = new Set(
|
export const COMMON_RANGE_VALUES_SET = new Set(
|
||||||
COMMON_RANGE_OPTIONS.map(({ value }) => value),
|
COMMON_RANGE_OPTIONS.map(({ value }) => value),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CALENDAR_RANGE_OPTIONS: SelectOptionType[] = [
|
export const CALENDAR_RANGE_OPTIONS: SelectOptionType[] = [
|
||||||
{ value: PreviousCalendarWeek, label: t('previous calendar week') },
|
{ value: PreviousCalendarWeek, label: t('previous calendar week'), order: 0 },
|
||||||
{ value: PreviousCalendarMonth, label: t('previous calendar month') },
|
{
|
||||||
{ value: PreviousCalendarYear, label: t('previous calendar year') },
|
value: PreviousCalendarMonth,
|
||||||
|
label: t('previous calendar month'),
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
{ value: PreviousCalendarYear, label: t('previous calendar year'), order: 2 },
|
||||||
];
|
];
|
||||||
export const CALENDAR_RANGE_VALUES_SET = new Set(
|
export const CALENDAR_RANGE_VALUES_SET = new Set(
|
||||||
CALENDAR_RANGE_OPTIONS.map(({ value }) => value),
|
CALENDAR_RANGE_OPTIONS.map(({ value }) => value),
|
||||||
@ -67,24 +71,26 @@ const GRAIN_OPTIONS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const SINCE_GRAIN_OPTIONS: SelectOptionType[] = GRAIN_OPTIONS.map(
|
export const SINCE_GRAIN_OPTIONS: SelectOptionType[] = GRAIN_OPTIONS.map(
|
||||||
item => ({
|
(item, index) => ({
|
||||||
value: item.value,
|
value: item.value,
|
||||||
label: item.label(t('Before')),
|
label: item.label(t('Before')),
|
||||||
|
order: index,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UNTIL_GRAIN_OPTIONS: SelectOptionType[] = GRAIN_OPTIONS.map(
|
export const UNTIL_GRAIN_OPTIONS: SelectOptionType[] = GRAIN_OPTIONS.map(
|
||||||
item => ({
|
(item, index) => ({
|
||||||
value: item.value,
|
value: item.value,
|
||||||
label: item.label(t('After')),
|
label: item.label(t('After')),
|
||||||
|
order: index,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SINCE_MODE_OPTIONS: SelectOptionType[] = [
|
export const SINCE_MODE_OPTIONS: SelectOptionType[] = [
|
||||||
{ value: 'specific', label: t('Specific Date/Time') },
|
{ value: 'specific', label: t('Specific Date/Time'), order: 0 },
|
||||||
{ value: 'relative', label: t('Relative Date/Time') },
|
{ value: 'relative', label: t('Relative Date/Time'), order: 1 },
|
||||||
{ value: 'now', label: t('Now') },
|
{ value: 'now', label: t('Now'), order: 2 },
|
||||||
{ value: 'today', label: t('Midnight') },
|
{ value: 'today', label: t('Midnight'), order: 3 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const UNTIL_MODE_OPTIONS: SelectOptionType[] = SINCE_MODE_OPTIONS.slice();
|
export const UNTIL_MODE_OPTIONS: SelectOptionType[] = SINCE_MODE_OPTIONS.slice();
|
||||||
|
@ -44,6 +44,7 @@ jest.mock('src/components/Select/Select', () => ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
propertyComparator: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
fetchMock.get(datasetsOwnersEndpoint, {
|
fetchMock.get(datasetsOwnersEndpoint, {
|
||||||
|
@ -38,7 +38,7 @@ import { Switch } from 'src/components/Switch';
|
|||||||
import Modal from 'src/components/Modal';
|
import Modal from 'src/components/Modal';
|
||||||
import TimezoneSelector from 'src/components/TimezoneSelector';
|
import TimezoneSelector from 'src/components/TimezoneSelector';
|
||||||
import { Radio } from 'src/components/Radio';
|
import { Radio } from 'src/components/Radio';
|
||||||
import { Select } from 'src/components';
|
import Select, { propertyComparator } from 'src/components/Select/Select';
|
||||||
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
|
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
|
||||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||||
import Owner from 'src/types/Owner';
|
import Owner from 'src/types/Owner';
|
||||||
@ -85,30 +85,37 @@ const CONDITIONS = [
|
|||||||
{
|
{
|
||||||
label: t('< (Smaller than)'),
|
label: t('< (Smaller than)'),
|
||||||
value: '<',
|
value: '<',
|
||||||
|
order: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('> (Larger than)'),
|
label: t('> (Larger than)'),
|
||||||
value: '>',
|
value: '>',
|
||||||
|
order: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('<= (Smaller or equal)'),
|
label: t('<= (Smaller or equal)'),
|
||||||
value: '<=',
|
value: '<=',
|
||||||
|
order: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('>= (Larger or equal)'),
|
label: t('>= (Larger or equal)'),
|
||||||
value: '>=',
|
value: '>=',
|
||||||
|
order: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('== (Is equal)'),
|
label: t('== (Is equal)'),
|
||||||
value: '==',
|
value: '==',
|
||||||
|
order: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('!= (Is not equal)'),
|
label: t('!= (Is not equal)'),
|
||||||
value: '!=',
|
value: '!=',
|
||||||
|
order: 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('Not null'),
|
label: t('Not null'),
|
||||||
value: 'not null',
|
value: 'not null',
|
||||||
|
order: 6,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -1147,6 +1154,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||||||
currentAlert?.validator_config_json?.op || undefined
|
currentAlert?.validator_config_json?.op || undefined
|
||||||
}
|
}
|
||||||
options={CONDITIONS}
|
options={CONDITIONS}
|
||||||
|
sortComparator={propertyComparator('order')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</StyledInputContainer>
|
</StyledInputContainer>
|
||||||
@ -1214,6 +1222,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||||||
onChange={onLogRetentionChange}
|
onChange={onLogRetentionChange}
|
||||||
value={currentAlert?.log_retention || DEFAULT_RETENTION}
|
value={currentAlert?.log_retention || DEFAULT_RETENTION}
|
||||||
options={RETENTION_OPTIONS}
|
options={RETENTION_OPTIONS}
|
||||||
|
sortComparator={propertyComparator('value')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</StyledInputContainer>
|
</StyledInputContainer>
|
||||||
|
Loading…
Reference in New Issue
Block a user