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:
Michael S. Molina 2021-10-25 08:47:01 -03:00 committed by GitHub
parent ef3afbde82
commit 55be249870
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 216 additions and 112 deletions

View File

@ -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,

View File

@ -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'));
}); });

View File

@ -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,

View File

@ -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);

View File

@ -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);
} }
}; };

View File

@ -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);
}) })

View File

@ -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()
}
/> />
); );
}; };

View File

@ -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>

View File

@ -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],
); );

View File

@ -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,

View File

@ -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>
); );

View File

@ -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' && (

View File

@ -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>

View File

@ -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 =

View File

@ -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();

View File

@ -44,6 +44,7 @@ jest.mock('src/components/Select/Select', () => ({
</button> </button>
</div> </div>
), ),
propertyComparator: jest.fn(),
})); }));
fetchMock.get(datasetsOwnersEndpoint, { fetchMock.get(datasetsOwnersEndpoint, {

View File

@ -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>