mirror of https://github.com/apache/superset.git
feat(select): keep options order when in single mode (#19085)
This commit is contained in:
parent
124cb0dc66
commit
ae13d8313b
|
@ -106,8 +106,6 @@ const groupByControl: SharedControlConfig<'SelectControl', ColumnMeta> = {
|
|||
'One or many columns to group by. High cardinality groupings should include a sort by metric ' +
|
||||
'and series limit to limit the number of fetched and rendered series.',
|
||||
),
|
||||
sortComparator: (a: { label: string }, b: { label: string }) =>
|
||||
a.label.localeCompare(b.label),
|
||||
optionRenderer: c => <ColumnOption showType column={c} />,
|
||||
valueRenderer: c => <ColumnOption column={c} />,
|
||||
valueKey: 'column_name',
|
||||
|
|
|
@ -21,6 +21,7 @@ export { default as ensureIsArray } from './ensureIsArray';
|
|||
export { default as ensureIsInt } from './ensureIsInt';
|
||||
export { default as isDefined } from './isDefined';
|
||||
export { default as isRequired } from './isRequired';
|
||||
export { default as isEqualArray } from './isEqualArray';
|
||||
export { default as makeSingleton } from './makeSingleton';
|
||||
export { default as promiseTimeout } from './promiseTimeout';
|
||||
export { default as logging } from './logging';
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import isEqualArray from './isEqualArray';
|
||||
|
||||
test('isEqualArray', () => {
|
||||
expect(isEqualArray([], [])).toBe(true);
|
||||
expect(isEqualArray([1, 2], [1, 2])).toBe(true);
|
||||
const item1 = { a: 1 };
|
||||
expect(isEqualArray([item1], [item1])).toBe(true);
|
||||
expect(isEqualArray(null, undefined)).toBe(true);
|
||||
// compare is shallow
|
||||
expect(isEqualArray([{ a: 1 }], [{ a: 1 }])).toBe(false);
|
||||
expect(isEqualArray(null, [])).toBe(false);
|
||||
expect(isEqualArray([1, 2], [])).toBe(false);
|
||||
});
|
|
@ -23,9 +23,11 @@ export default function isEqualArray<T extends unknown[] | undefined | null>(
|
|||
return (
|
||||
arrA === arrB ||
|
||||
(!arrA && !arrB) ||
|
||||
(arrA &&
|
||||
!!(
|
||||
arrA &&
|
||||
arrB &&
|
||||
arrA.length === arrB.length &&
|
||||
arrA.every((x, i) => x === arrB[i]))
|
||||
arrA.every((x, i) => x === arrB[i])
|
||||
)
|
||||
);
|
||||
}
|
|
@ -6,11 +6,7 @@
|
|||
"rootDir": "."
|
||||
},
|
||||
"extends": "../../../tsconfig.json",
|
||||
"include": [
|
||||
"**/*",
|
||||
"../types/**/*",
|
||||
"../../../types/**/*"
|
||||
],
|
||||
"include": ["**/*", "../types/**/*", "../../../types/**/*"],
|
||||
"references": [
|
||||
{
|
||||
"path": ".."
|
||||
|
|
|
@ -116,8 +116,6 @@ const all_columns: typeof sharedControls.groupby = {
|
|||
? [t('must have a value')]
|
||||
: [],
|
||||
}),
|
||||
sortComparator: (a: { label: string }, b: { label: string }) =>
|
||||
a.label.localeCompare(b.label),
|
||||
visibility: isRawMode,
|
||||
};
|
||||
|
||||
|
@ -279,8 +277,6 @@ const config: ControlPanelConfig = {
|
|||
choices: datasource?.order_by_choices || [],
|
||||
}),
|
||||
visibility: isRawMode,
|
||||
sortComparator: (a: { label: string }, b: { label: string }) =>
|
||||
a.label.localeCompare(b.label),
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import isEqualArray from './isEqualArray';
|
||||
import { isEqualArray } from '@superset-ui/core';
|
||||
import { TableChartProps } from '../types';
|
||||
|
||||
export default function isEqualColumns(
|
||||
|
|
|
@ -4,16 +4,9 @@
|
|||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"exclude": [
|
||||
"lib",
|
||||
"test"
|
||||
],
|
||||
"exclude": ["lib", "test"],
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"types/**/*",
|
||||
"../../types/**/*",
|
||||
],
|
||||
"include": ["src/**/*", "types/**/*", "../../types/**/*"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/superset-ui-chart-controls"
|
||||
|
|
|
@ -187,10 +187,10 @@ export const InteractiveSelect = ({
|
|||
);
|
||||
|
||||
InteractiveSelect.args = {
|
||||
autoFocus: false,
|
||||
autoFocus: true,
|
||||
allowNewOptions: false,
|
||||
allowClear: false,
|
||||
showSearch: false,
|
||||
showSearch: true,
|
||||
disabled: false,
|
||||
invertSelection: false,
|
||||
placeholder: 'Select ...',
|
||||
|
|
|
@ -99,6 +99,18 @@ const findAllSelectValues = () =>
|
|||
|
||||
const clearAll = () => userEvent.click(screen.getByLabelText('close-circle'));
|
||||
|
||||
const matchOrder = async (expectedLabels: string[]) => {
|
||||
const actualLabels: string[] = [];
|
||||
(await findAllSelectOptions()).forEach(option => {
|
||||
actualLabels.push(option.textContent || '');
|
||||
});
|
||||
// menu is a virtual list, which means it may not render all options
|
||||
expect(actualLabels.slice(0, expectedLabels.length)).toEqual(
|
||||
expectedLabels.slice(0, actualLabels.length),
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
const type = (text: string) => {
|
||||
const select = getSelect();
|
||||
userEvent.clear(select);
|
||||
|
@ -169,34 +181,64 @@ test('sort the options using a custom sort comparator', async () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('displays the selected values first', async () => {
|
||||
render(<Select {...defaultProps} mode="multiple" />);
|
||||
const option3 = OPTIONS[2].label;
|
||||
const option8 = OPTIONS[7].label;
|
||||
test('should sort selected to top when in single mode', async () => {
|
||||
render(<Select {...defaultProps} mode="single" />);
|
||||
const originalLabels = OPTIONS.map(option => option.label);
|
||||
await open();
|
||||
userEvent.click(await findSelectOption(option3));
|
||||
userEvent.click(await findSelectOption(option8));
|
||||
userEvent.click(await findSelectOption(originalLabels[1]));
|
||||
// after selection, keep the original order
|
||||
expect(await matchOrder(originalLabels)).toBe(true);
|
||||
|
||||
// order selected to top when reopen
|
||||
await type('{esc}');
|
||||
await open();
|
||||
const sortedOptions = await findAllSelectOptions();
|
||||
expect(sortedOptions[0]).toHaveTextContent(option3);
|
||||
expect(sortedOptions[1]).toHaveTextContent(option8);
|
||||
let labels = originalLabels.slice();
|
||||
labels = labels.splice(1, 1).concat(labels);
|
||||
expect(await matchOrder(labels)).toBe(true);
|
||||
|
||||
// keep clicking other items, the updated order should still based on
|
||||
// original order
|
||||
userEvent.click(await findSelectOption(originalLabels[5]));
|
||||
await matchOrder(labels);
|
||||
await type('{esc}');
|
||||
await open();
|
||||
labels = originalLabels.slice();
|
||||
labels = labels.splice(5, 1).concat(labels);
|
||||
expect(await matchOrder(labels)).toBe(true);
|
||||
|
||||
// should revert to original order
|
||||
clearAll();
|
||||
await type('{esc}');
|
||||
await open();
|
||||
expect(await matchOrder(originalLabels)).toBe(true);
|
||||
});
|
||||
|
||||
test('displays the original order when unselecting', async () => {
|
||||
test('should sort selected to the top when in multi mode', async () => {
|
||||
render(<Select {...defaultProps} mode="multiple" />);
|
||||
const option3 = OPTIONS[2].label;
|
||||
const option8 = OPTIONS[7].label;
|
||||
const originalLabels = OPTIONS.map(option => option.label);
|
||||
let labels = originalLabels.slice();
|
||||
|
||||
await open();
|
||||
userEvent.click(await findSelectOption(option3));
|
||||
userEvent.click(await findSelectOption(option8));
|
||||
userEvent.click(await findSelectOption(labels[1]));
|
||||
expect(await matchOrder(labels)).toBe(true);
|
||||
|
||||
await type('{esc}');
|
||||
clearAll();
|
||||
await open();
|
||||
const options = await findAllSelectOptions();
|
||||
options.forEach((option, key) =>
|
||||
expect(option).toHaveTextContent(OPTIONS[key].label),
|
||||
);
|
||||
labels = labels.splice(1, 1).concat(labels);
|
||||
expect(await matchOrder(labels)).toBe(true);
|
||||
|
||||
await open();
|
||||
userEvent.click(await findSelectOption(labels[5]));
|
||||
await type('{esc}');
|
||||
await open();
|
||||
labels = [labels.splice(0, 1)[0], labels.splice(4, 1)[0]].concat(labels);
|
||||
expect(await matchOrder(labels)).toBe(true);
|
||||
|
||||
// should revert to original order
|
||||
clearAll();
|
||||
await type('{esc}');
|
||||
await open();
|
||||
expect(await matchOrder(originalLabels)).toBe(true);
|
||||
});
|
||||
|
||||
test('searches for label or value', async () => {
|
||||
|
@ -540,17 +582,35 @@ test('async - changes the selected item in single mode', async () => {
|
|||
test('async - deselects an item in multiple mode', async () => {
|
||||
render(<Select {...defaultProps} options={loadOptions} mode="multiple" />);
|
||||
await open();
|
||||
const [firstOption, secondOption] = OPTIONS;
|
||||
userEvent.click(await findSelectOption(firstOption.label));
|
||||
userEvent.click(await findSelectOption(secondOption.label));
|
||||
const option3 = OPTIONS[2];
|
||||
const option8 = OPTIONS[7];
|
||||
userEvent.click(await findSelectOption(option8.label));
|
||||
userEvent.click(await findSelectOption(option3.label));
|
||||
|
||||
let options = await findAllSelectOptions();
|
||||
expect(options).toHaveLength(Math.min(defaultProps.pageSize, OPTIONS.length));
|
||||
expect(options[0]).toHaveTextContent(OPTIONS[0].label);
|
||||
expect(options[1]).toHaveTextContent(OPTIONS[1].label);
|
||||
|
||||
await type('{esc}');
|
||||
await open();
|
||||
|
||||
// should rank selected options to the top after menu closes
|
||||
options = await findAllSelectOptions();
|
||||
expect(options).toHaveLength(Math.min(defaultProps.pageSize, OPTIONS.length));
|
||||
expect(options[0]).toHaveTextContent(option3.label);
|
||||
expect(options[1]).toHaveTextContent(option8.label);
|
||||
|
||||
let values = await findAllSelectValues();
|
||||
expect(values.length).toBe(2);
|
||||
expect(values[0]).toHaveTextContent(firstOption.label);
|
||||
expect(values[1]).toHaveTextContent(secondOption.label);
|
||||
userEvent.click(await findSelectOption(firstOption.label));
|
||||
expect(values).toHaveLength(2);
|
||||
// should keep the order by which the options were selected
|
||||
expect(values[0]).toHaveTextContent(option8.label);
|
||||
expect(values[1]).toHaveTextContent(option3.label);
|
||||
|
||||
userEvent.click(await findSelectOption(option3.label));
|
||||
values = await findAllSelectValues();
|
||||
expect(values.length).toBe(1);
|
||||
expect(values[0]).toHaveTextContent(secondOption.label);
|
||||
expect(values[0]).toHaveTextContent(option8.label);
|
||||
});
|
||||
|
||||
test('async - adds a new option if none is available and allowNewOptions is true', async () => {
|
||||
|
|
|
@ -156,6 +156,10 @@ export interface SelectProps extends PickedSelectProps {
|
|||
* Works in async mode only (See the options property).
|
||||
*/
|
||||
onError?: (error: string) => void;
|
||||
/**
|
||||
* Customize how filtered options are sorted while users search.
|
||||
* Will not apply to predefined `options` array when users are not searching.
|
||||
*/
|
||||
sortComparator?: typeof DEFAULT_SORT_COMPARATOR;
|
||||
}
|
||||
|
||||
|
@ -314,8 +318,6 @@ const Select = (
|
|||
const isAsync = typeof options === 'function';
|
||||
const isSingleMode = mode === 'single';
|
||||
const shouldShowSearch = isAsync || allowNewOptions ? true : showSearch;
|
||||
const initialOptions =
|
||||
options && Array.isArray(options) ? options.slice() : EMPTY_OPTIONS;
|
||||
const [selectValue, setSelectValue] = useState(value);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(loading);
|
||||
|
@ -346,13 +348,27 @@ const Select = (
|
|||
sortSelectedFirst(a, b) || sortComparator(a, b, inputValue),
|
||||
[inputValue, sortComparator, sortSelectedFirst],
|
||||
);
|
||||
const sortComparatorWithoutSearch = useCallback(
|
||||
const sortComparatorForNoSearch = useCallback(
|
||||
(a: AntdLabeledValue, b: AntdLabeledValue) =>
|
||||
sortSelectedFirst(a, b) || sortComparator(a, b, ''),
|
||||
[sortComparator, sortSelectedFirst],
|
||||
sortSelectedFirst(a, b) ||
|
||||
// Only apply the custom sorter in async mode because we should
|
||||
// preserve the options order as much as possible.
|
||||
(isAsync ? sortComparator(a, b, '') : 0),
|
||||
[isAsync, sortComparator, sortSelectedFirst],
|
||||
);
|
||||
|
||||
const initialOptions = useMemo(
|
||||
() => (options && Array.isArray(options) ? options.slice() : EMPTY_OPTIONS),
|
||||
[options],
|
||||
);
|
||||
const initialOptionsSorted = useMemo(
|
||||
() => initialOptions.slice().sort(sortComparatorForNoSearch),
|
||||
[initialOptions, sortComparatorForNoSearch],
|
||||
);
|
||||
|
||||
const [selectOptions, setSelectOptions] =
|
||||
useState<OptionsType>(initialOptions);
|
||||
useState<OptionsType>(initialOptionsSorted);
|
||||
|
||||
// add selected values to options list if they are not in it
|
||||
const fullSelectOptions = useMemo(() => {
|
||||
const missingValues: OptionsType = ensureIsArray(selectValue)
|
||||
|
@ -433,13 +449,13 @@ const Select = (
|
|||
mergedData = prevOptions
|
||||
.filter(previousOption => !dataValues.has(previousOption.value))
|
||||
.concat(data)
|
||||
.sort(sortComparatorWithoutSearch);
|
||||
.sort(sortComparatorForNoSearch);
|
||||
return mergedData;
|
||||
});
|
||||
}
|
||||
return mergedData;
|
||||
},
|
||||
[sortComparatorWithoutSearch],
|
||||
[sortComparatorForNoSearch],
|
||||
);
|
||||
|
||||
const fetchPage = useMemo(
|
||||
|
@ -575,11 +591,13 @@ const Select = (
|
|||
}
|
||||
// if no search input value, force sort options because it won't be sorted by
|
||||
// `filterSort`.
|
||||
if (isDropdownVisible && !inputValue && fullSelectOptions.length > 0) {
|
||||
const sortedOptions = [...fullSelectOptions].sort(
|
||||
sortComparatorWithSearch,
|
||||
);
|
||||
if (!isEqual(sortedOptions, fullSelectOptions)) {
|
||||
if (isDropdownVisible && !inputValue && selectOptions.length > 1) {
|
||||
const sortedOptions = isAsync
|
||||
? selectOptions.slice().sort(sortComparatorForNoSearch)
|
||||
: // if not in async mode, revert to the original select options
|
||||
// (with selected options still sorted to the top)
|
||||
initialOptionsSorted;
|
||||
if (!isEqual(sortedOptions, selectOptions)) {
|
||||
setSelectOptions(sortedOptions);
|
||||
}
|
||||
}
|
||||
|
@ -624,10 +642,8 @@ const Select = (
|
|||
// when `options` list is updated from component prop, reset states
|
||||
fetchedQueries.current.clear();
|
||||
setAllValuesLoaded(false);
|
||||
setSelectOptions(
|
||||
options && Array.isArray(options) ? options : EMPTY_OPTIONS,
|
||||
);
|
||||
}, [options]);
|
||||
setSelectOptions(initialOptions);
|
||||
}, [initialOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectValue(value);
|
||||
|
|
|
@ -99,6 +99,7 @@ const TIMEZONE_OPTIONS_SORT_COMPARATOR = (
|
|||
moment.tz(currentDate, a.timezoneName).utcOffset() -
|
||||
moment.tz(currentDate, b.timezoneName).utcOffset();
|
||||
|
||||
// pre-sort timezone options by time offset
|
||||
TIMEZONE_OPTIONS.sort(TIMEZONE_OPTIONS_SORT_COMPARATOR);
|
||||
|
||||
const matchTimezoneToOptions = (timezone: string) =>
|
||||
|
|
|
@ -510,7 +510,8 @@ export default function getNativeFilterConfig(
|
|||
childComponent.filterType as FILTER_COMPONENT_FILTER_TYPES,
|
||||
)
|
||||
) {
|
||||
childComponent.cascadeParentIds ||= [];
|
||||
childComponent.cascadeParentIds =
|
||||
childComponent.cascadeParentIds || [];
|
||||
childComponent.cascadeParentIds.push(parentComponentId);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import React from 'react';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import { Form, FormItem, FormProps } from 'src/components/Form';
|
||||
import Select, { propertyComparator } from 'src/components/Select/Select';
|
||||
import Select from 'src/components/Select/Select';
|
||||
import { Col, Row } from 'src/components';
|
||||
import { InputNumber } from 'src/components/Input';
|
||||
import Button from 'src/components/Button';
|
||||
|
@ -45,17 +45,17 @@ const colorSchemeOptions = [
|
|||
];
|
||||
|
||||
const operatorOptions = [
|
||||
{ value: COMPARATOR.NONE, label: 'None', order: 0 },
|
||||
{ value: COMPARATOR.GREATER_THAN, label: '>', order: 1 },
|
||||
{ value: COMPARATOR.LESS_THAN, label: '<', order: 2 },
|
||||
{ value: COMPARATOR.GREATER_OR_EQUAL, label: '≥', order: 3 },
|
||||
{ value: COMPARATOR.LESS_OR_EQUAL, label: '≤', order: 4 },
|
||||
{ value: COMPARATOR.EQUAL, label: '=', order: 5 },
|
||||
{ value: COMPARATOR.NOT_EQUAL, label: '≠', order: 6 },
|
||||
{ value: COMPARATOR.BETWEEN, label: '< x <', order: 7 },
|
||||
{ value: COMPARATOR.BETWEEN_OR_EQUAL, label: '≤ x ≤', order: 8 },
|
||||
{ value: COMPARATOR.BETWEEN_OR_LEFT_EQUAL, label: '≤ x <', order: 9 },
|
||||
{ value: COMPARATOR.BETWEEN_OR_RIGHT_EQUAL, label: '< x ≤', order: 10 },
|
||||
{ value: COMPARATOR.NONE, label: 'None' },
|
||||
{ value: COMPARATOR.GREATER_THAN, label: '>' },
|
||||
{ value: COMPARATOR.LESS_THAN, label: '<' },
|
||||
{ value: COMPARATOR.GREATER_OR_EQUAL, label: '≥' },
|
||||
{ value: COMPARATOR.LESS_OR_EQUAL, label: '≤' },
|
||||
{ value: COMPARATOR.EQUAL, label: '=' },
|
||||
{ value: COMPARATOR.NOT_EQUAL, label: '≠' },
|
||||
{ value: COMPARATOR.BETWEEN, label: '< x <' },
|
||||
{ value: COMPARATOR.BETWEEN_OR_EQUAL, label: '≤ x ≤' },
|
||||
{ value: COMPARATOR.BETWEEN_OR_LEFT_EQUAL, label: '≤ x <' },
|
||||
{ value: COMPARATOR.BETWEEN_OR_RIGHT_EQUAL, label: '< x ≤' },
|
||||
];
|
||||
|
||||
const targetValueValidator =
|
||||
|
@ -127,11 +127,7 @@ const operatorField = (
|
|||
rules={rulesRequired}
|
||||
initialValue={operatorOptions[0].value}
|
||||
>
|
||||
<Select
|
||||
ariaLabel={t('Operator')}
|
||||
options={operatorOptions}
|
||||
sortComparator={propertyComparator('order')}
|
||||
/>
|
||||
<Select ariaLabel={t('Operator')} options={operatorOptions} />
|
||||
</FormItem>
|
||||
);
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ import Label, { Type } from 'src/components/Label';
|
|||
import Popover from 'src/components/Popover';
|
||||
import { Divider } from 'src/components';
|
||||
import Icons from 'src/components/Icons';
|
||||
import Select, { propertyComparator } from 'src/components/Select/Select';
|
||||
import Select from 'src/components/Select/Select';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import { DEFAULT_TIME_RANGE } from 'src/explore/constants';
|
||||
import { useDebouncedEffect } from 'src/explore/exploreUtils';
|
||||
|
@ -294,7 +294,6 @@ export default function DateFilterLabel(props: DateFilterControlProps) {
|
|||
options={FRAME_OPTIONS}
|
||||
value={frame}
|
||||
onChange={onChangeFrame}
|
||||
sortComparator={propertyComparator('order')}
|
||||
/>
|
||||
{frame !== 'No filter' && <Divider />}
|
||||
{frame === 'Common' && (
|
||||
|
|
|
@ -24,7 +24,7 @@ import { Col, Row } from 'src/components';
|
|||
import { InputNumber } from 'src/components/Input';
|
||||
import { DatePicker } from 'src/components/DatePicker';
|
||||
import { Radio } from 'src/components/Radio';
|
||||
import Select, { propertyComparator } from 'src/components/Select/Select';
|
||||
import Select from 'src/components/Select/Select';
|
||||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||
import {
|
||||
SINCE_GRAIN_OPTIONS,
|
||||
|
@ -42,8 +42,6 @@ import {
|
|||
FrameComponentProps,
|
||||
} from 'src/explore/components/controls/DateFilterControl/types';
|
||||
|
||||
const sortComparator = propertyComparator('order');
|
||||
|
||||
export function CustomFrame(props: FrameComponentProps) {
|
||||
const { customRange, matchedFlag } = customTimeRangeDecode(props.value);
|
||||
if (!matchedFlag) {
|
||||
|
@ -124,7 +122,6 @@ export function CustomFrame(props: FrameComponentProps) {
|
|||
options={SINCE_MODE_OPTIONS}
|
||||
value={sinceMode}
|
||||
onChange={(value: string) => onChange('sinceMode', value)}
|
||||
sortComparator={sortComparator}
|
||||
/>
|
||||
{sinceMode === 'specific' && (
|
||||
<Row>
|
||||
|
@ -159,7 +156,6 @@ export function CustomFrame(props: FrameComponentProps) {
|
|||
options={SINCE_GRAIN_OPTIONS}
|
||||
value={sinceGrain}
|
||||
onChange={(value: string) => onChange('sinceGrain', value)}
|
||||
sortComparator={sortComparator}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -178,7 +174,6 @@ export function CustomFrame(props: FrameComponentProps) {
|
|||
options={UNTIL_MODE_OPTIONS}
|
||||
value={untilMode}
|
||||
onChange={(value: string) => onChange('untilMode', value)}
|
||||
sortComparator={sortComparator}
|
||||
/>
|
||||
{untilMode === 'specific' && (
|
||||
<Row>
|
||||
|
@ -212,7 +207,6 @@ export function CustomFrame(props: FrameComponentProps) {
|
|||
options={UNTIL_GRAIN_OPTIONS}
|
||||
value={untilGrain}
|
||||
onChange={(value: string) => onChange('untilGrain', value)}
|
||||
sortComparator={sortComparator}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
export type SelectOptionType = {
|
||||
value: string;
|
||||
label: string;
|
||||
order: number;
|
||||
};
|
||||
|
||||
export type FrameType =
|
||||
|
|
|
@ -28,32 +28,31 @@ import {
|
|||
} from 'src/explore/components/controls/DateFilterControl/types';
|
||||
|
||||
export const FRAME_OPTIONS: SelectOptionType[] = [
|
||||
{ value: 'Common', label: t('Last'), order: 0 },
|
||||
{ value: 'Calendar', label: t('Previous'), order: 1 },
|
||||
{ value: 'Custom', label: t('Custom'), order: 2 },
|
||||
{ value: 'Advanced', label: t('Advanced'), order: 3 },
|
||||
{ value: 'No filter', label: t('No filter'), order: 4 },
|
||||
{ value: 'Common', label: t('Last') },
|
||||
{ value: 'Calendar', label: t('Previous') },
|
||||
{ value: 'Custom', label: t('Custom') },
|
||||
{ value: 'Advanced', label: t('Advanced') },
|
||||
{ value: 'No filter', label: t('No filter') },
|
||||
];
|
||||
|
||||
export const COMMON_RANGE_OPTIONS: SelectOptionType[] = [
|
||||
{ value: 'Last day', label: t('last day'), order: 0 },
|
||||
{ value: 'Last week', label: t('last week'), order: 1 },
|
||||
{ value: 'Last month', label: t('last month'), order: 2 },
|
||||
{ value: 'Last quarter', label: t('last quarter'), order: 3 },
|
||||
{ value: 'Last year', label: t('last year'), order: 4 },
|
||||
{ value: 'Last day', label: t('last day') },
|
||||
{ value: 'Last week', label: t('last week') },
|
||||
{ value: 'Last month', label: t('last month') },
|
||||
{ value: 'Last quarter', label: t('last quarter') },
|
||||
{ value: 'Last year', label: t('last year') },
|
||||
];
|
||||
export const COMMON_RANGE_VALUES_SET = new Set(
|
||||
COMMON_RANGE_OPTIONS.map(({ value }) => value),
|
||||
);
|
||||
|
||||
export const CALENDAR_RANGE_OPTIONS: SelectOptionType[] = [
|
||||
{ value: PreviousCalendarWeek, label: t('previous calendar week'), order: 0 },
|
||||
{ value: PreviousCalendarWeek, label: t('previous calendar week') },
|
||||
{
|
||||
value: PreviousCalendarMonth,
|
||||
label: t('previous calendar month'),
|
||||
order: 1,
|
||||
},
|
||||
{ value: PreviousCalendarYear, label: t('previous calendar year'), order: 2 },
|
||||
{ value: PreviousCalendarYear, label: t('previous calendar year') },
|
||||
];
|
||||
export const CALENDAR_RANGE_VALUES_SET = new Set(
|
||||
CALENDAR_RANGE_OPTIONS.map(({ value }) => value),
|
||||
|
@ -71,26 +70,24 @@ const GRAIN_OPTIONS = [
|
|||
];
|
||||
|
||||
export const SINCE_GRAIN_OPTIONS: SelectOptionType[] = GRAIN_OPTIONS.map(
|
||||
(item, index) => ({
|
||||
item => ({
|
||||
value: item.value,
|
||||
label: item.label(t('Before')),
|
||||
order: index,
|
||||
}),
|
||||
);
|
||||
|
||||
export const UNTIL_GRAIN_OPTIONS: SelectOptionType[] = GRAIN_OPTIONS.map(
|
||||
(item, index) => ({
|
||||
item => ({
|
||||
value: item.value,
|
||||
label: item.label(t('After')),
|
||||
order: index,
|
||||
}),
|
||||
);
|
||||
|
||||
export const SINCE_MODE_OPTIONS: SelectOptionType[] = [
|
||||
{ value: 'specific', label: t('Specific Date/Time'), order: 0 },
|
||||
{ value: 'relative', label: t('Relative Date/Time'), order: 1 },
|
||||
{ value: 'now', label: t('Now'), order: 2 },
|
||||
{ value: 'today', label: t('Midnight'), order: 3 },
|
||||
{ value: 'specific', label: t('Specific Date/Time') },
|
||||
{ value: 'relative', label: t('Relative Date/Time') },
|
||||
{ value: 'now', label: t('Now') },
|
||||
{ value: 'today', label: t('Midnight') },
|
||||
];
|
||||
|
||||
export const UNTIL_MODE_OPTIONS: SelectOptionType[] =
|
||||
|
|
|
@ -37,7 +37,6 @@ import AdhocFilter, {
|
|||
CLAUSES,
|
||||
} from 'src/explore/components/controls/FilterControl/AdhocFilter';
|
||||
import { Input } from 'src/components/Input';
|
||||
import { propertyComparator } from 'src/components/Select/Select';
|
||||
import { optionLabel } from 'src/utils/common';
|
||||
|
||||
const StyledInput = styled(Input)`
|
||||
|
@ -405,16 +404,12 @@ const AdhocFilterEditPopoverSimpleTabContent: React.FC<Props> = props => {
|
|||
order: index,
|
||||
}))}
|
||||
{...operatorSelectProps}
|
||||
sortComparator={propertyComparator('order')}
|
||||
/>
|
||||
{MULTI_OPERATORS.has(operatorId) || suggestions.length > 0 ? (
|
||||
<SelectWithLabel
|
||||
labelText={labelText}
|
||||
options={suggestions}
|
||||
{...comparatorSelectProps}
|
||||
sortComparator={propertyComparator(
|
||||
typeof suggestions[0] === 'number' ? 'value' : 'label',
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<StyledInput
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { css, t } from '@superset-ui/core';
|
||||
import Select, { propertyComparator } from 'src/components/Select/Select';
|
||||
import { css, isEqualArray, t } from '@superset-ui/core';
|
||||
import Select from 'src/components/Select/Select';
|
||||
import ControlHeader from 'src/explore/components/ControlHeader';
|
||||
|
||||
const propTypes = {
|
||||
|
@ -94,8 +94,8 @@ export default class SelectControl extends React.PureComponent {
|
|||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (
|
||||
nextProps.choices !== this.props.choices ||
|
||||
nextProps.options !== this.props.options
|
||||
!isEqualArray(nextProps.choices, this.props.choices) ||
|
||||
!isEqualArray(nextProps.options, this.props.options)
|
||||
) {
|
||||
const options = this.getOptions(nextProps);
|
||||
this.setState({ options });
|
||||
|
@ -133,10 +133,9 @@ export default class SelectControl extends React.PureComponent {
|
|||
}));
|
||||
} else if (choices) {
|
||||
// Accepts different formats of input
|
||||
options = choices.map((c, i) => {
|
||||
options = choices.map(c => {
|
||||
if (Array.isArray(c)) {
|
||||
const [value, label] = c.length > 1 ? c : [c[0], c[0]];
|
||||
if (!this.props.sortComparator) return { value, label, order: i };
|
||||
return {
|
||||
value,
|
||||
label,
|
||||
|
@ -241,7 +240,7 @@ export default class SelectControl extends React.PureComponent {
|
|||
optionRenderer,
|
||||
options: this.state.options,
|
||||
placeholder,
|
||||
sortComparator: this.props.sortComparator || propertyComparator('order'),
|
||||
sortComparator: this.props.sortComparator,
|
||||
value: getValue(),
|
||||
};
|
||||
|
||||
|
|
|
@ -37,9 +37,9 @@ const defaultProps = {
|
|||
};
|
||||
|
||||
const options = [
|
||||
{ value: '1 year ago', label: '1 year ago', order: 0 },
|
||||
{ value: '1 week ago', label: '1 week ago', order: 1 },
|
||||
{ value: 'today', label: 'today', order: 2 },
|
||||
{ value: '1 year ago', label: '1 year ago' },
|
||||
{ value: '1 week ago', label: '1 week ago' },
|
||||
{ value: 'today', label: 'today' },
|
||||
];
|
||||
|
||||
describe('SelectControl', () => {
|
||||
|
@ -151,25 +151,6 @@ describe('SelectControl', () => {
|
|||
expect(wrapper.html()).not.toContain('add something');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when select has a sortComparator prop', () => {
|
||||
it('does not add add order key', () => {
|
||||
const sortComparator = (a, b) => a.label.localeCompare(b.label);
|
||||
const optionsSortedByLabel = options.map(opt => ({
|
||||
label: opt.label,
|
||||
value: opt.value,
|
||||
}));
|
||||
wrapper = mount(
|
||||
<SelectControl
|
||||
{...defaultProps}
|
||||
sortComparator={sortComparator}
|
||||
value={50}
|
||||
placeholder="add something"
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.state().options).toEqual(optionsSortedByLabel);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOptions', () => {
|
||||
|
@ -178,23 +159,4 @@ describe('SelectControl', () => {
|
|||
expect(wrapper.instance().getOptions(defaultProps)).toEqual(options);
|
||||
});
|
||||
});
|
||||
describe('UNSAFE_componentWillReceiveProps', () => {
|
||||
it('sets state.options if props.choices has changed', () => {
|
||||
const updatedOptions = [
|
||||
{ value: 'three', label: 'three', order: 0 },
|
||||
{ value: 'four', label: 'four', order: 1 },
|
||||
];
|
||||
const newProps = {
|
||||
choices: [
|
||||
['three', 'three'],
|
||||
['four', 'four'],
|
||||
],
|
||||
name: 'name',
|
||||
freeForm: false,
|
||||
value: null,
|
||||
};
|
||||
wrapper.setProps(newProps);
|
||||
expect(wrapper.state().options).toEqual(updatedOptions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -87,37 +87,30 @@ const CONDITIONS = [
|
|||
{
|
||||
label: t('< (Smaller than)'),
|
||||
value: '<',
|
||||
order: 0,
|
||||
},
|
||||
{
|
||||
label: t('> (Larger than)'),
|
||||
value: '>',
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
label: t('<= (Smaller or equal)'),
|
||||
value: '<=',
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
label: t('>= (Larger or equal)'),
|
||||
value: '>=',
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
label: t('== (Is equal)'),
|
||||
value: '==',
|
||||
order: 4,
|
||||
},
|
||||
{
|
||||
label: t('!= (Is not equal)'),
|
||||
value: '!=',
|
||||
order: 5,
|
||||
},
|
||||
{
|
||||
label: t('Not null'),
|
||||
value: 'not null',
|
||||
order: 6,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -1194,7 +1187,6 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
currentAlert?.validator_config_json?.op || undefined
|
||||
}
|
||||
options={CONDITIONS}
|
||||
sortComparator={propertyComparator('order')}
|
||||
/>
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
|
|
Loading…
Reference in New Issue