feat: upgrade react-select and make multi-select sortable (#9628)

* feat: upgrade react-select v1.3.0 to v3.1.0

Upgrade `react-select`, replace `react-virtualized-select` with a custom
solution implemented with `react-window`.

Future plans include deprecate `react-virtualized` used in other places, too.

Migrate all react-select related components to `src/Components/Select`.

* Fix new list view

* Fix tests

* Address PR comments

* Fix a flacky Cypress test

* Adjust styles for Select in CRUD ListView

* Fix loadOptions for owners select in chart PropertiesModal

TODO: add typing support for AsyncSelect props.

* Address PR comments; allow isMulti in SelectControl, too

* Clean up NaN in table filter values

* Fix flacky test
This commit is contained in:
Jesse Yang
2020-05-19 16:59:49 -07:00
committed by GitHub
parent 68832d2fa5
commit 81ab8dd8b4
97 changed files with 2027 additions and 1234 deletions

View File

@@ -18,13 +18,12 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import VirtualizedSelect from 'react-virtualized-select';
import { t } from '@superset-ui/translation';
import { isEqual } from 'lodash';
import OnPasteSelect from 'src/components/Select/OnPasteSelect';
import ControlHeader from '../ControlHeader';
import VirtualizedRendererWrap from '../../../components/VirtualizedRendererWrap';
import OnPasteSelect from '../../../components/OnPasteSelect';
import MetricDefinitionOption from '../MetricDefinitionOption';
import MetricDefinitionValue from '../MetricDefinitionValue';
import AdhocMetric from '../../AdhocMetric';
@@ -33,6 +32,7 @@ import savedMetricType from '../../propTypes/savedMetricType';
import adhocMetricType from '../../propTypes/adhocMetricType';
import {
AGGREGATES,
AGGREGATES_OPTIONS,
sqlaAutoGeneratedMetricNameRegex,
druidAutoGeneratedMetricRegex,
} from '../../constants';
@@ -141,10 +141,7 @@ export default class MetricsControl extends React.PureComponent {
this.optionsForSelect = this.optionsForSelect.bind(this);
this.selectFilterOption = this.selectFilterOption.bind(this);
this.isAutoGeneratedMetric = this.isAutoGeneratedMetric.bind(this);
this.optionRenderer = VirtualizedRendererWrap(
option => <MetricDefinitionOption option={option} />,
{ ignoreAutogeneratedMetrics: true },
);
this.optionRenderer = option => <MetricDefinitionOption option={option} />;
this.valueRenderer = option => (
<MetricDefinitionValue
option={option}
@@ -154,10 +151,12 @@ export default class MetricsControl extends React.PureComponent {
datasourceType={this.props.datasourceType}
/>
);
this.refFunc = ref => {
this.select = null;
this.selectRef = ref => {
if (ref) {
// eslint-disable-next-line no-underscore-dangle
this.select = ref._selectRef;
this.select = ref.select;
} else {
this.select = null;
}
};
this.state = {
@@ -207,60 +206,66 @@ export default class MetricsControl extends React.PureComponent {
return;
}
let transformedOpts = opts;
if (!this.props.multi) {
transformedOpts = [opts].filter(option => option);
let transformedOpts;
if (Array.isArray(opts)) {
transformedOpts = opts;
} else {
transformedOpts = opts ? [opts] : [];
}
let optionValues = transformedOpts
const optionValues = transformedOpts
.map(option => {
// pre-defined metric
if (option.metric_name) {
return option.metric_name;
} else if (option.column_name) {
}
// adding a new adhoc metric
if (option.column_name) {
const clearedAggregate = this.clearedAggregateInInput;
this.clearedAggregateInInput = null;
return new AdhocMetric({
isNew: true,
column: option,
aggregate: clearedAggregate || getDefaultAggregateForColumn(option),
});
} else if (option instanceof AdhocMetric) {
}
// existing adhoc metric or custom SQL metric
if (option instanceof AdhocMetric) {
return option;
} else if (option.aggregate_name) {
}
// start with selecting an aggregate function
if (option.aggregate_name && this.select) {
const newValue = `${option.aggregate_name}()`;
this.select.setInputValue(newValue);
this.select.handleInputChange({ target: { value: newValue } });
this.select.inputRef.value = newValue;
this.select.handleInputChange({ currentTarget: { value: newValue } });
// we need to set a timeout here or the selectionWill be overwritten
// by some browsers (e.g. Chrome)
setTimeout(() => {
this.select.input.input.selectionStart = newValue.length - 1;
this.select.input.input.selectionEnd = newValue.length - 1;
}, 0);
return null;
this.select.focusInput();
this.select.inputRef.selectionStart = newValue.length - 1;
this.select.inputRef.selectionEnd = newValue.length - 1;
});
}
return null;
})
.filter(option => option);
if (!this.props.multi) {
optionValues = optionValues[0];
}
this.props.onChange(optionValues);
this.props.onChange(this.props.multi ? optionValues : optionValues[0]);
}
checkIfAggregateInInput(input) {
let nextState = { aggregateInInput: null };
Object.keys(AGGREGATES).forEach(aggregate => {
if (input.toLowerCase().startsWith(aggregate.toLowerCase() + '(')) {
nextState = { aggregateInInput: aggregate };
}
});
const lowercaseInput = input.toLowerCase();
const aggregateInInput =
AGGREGATES_OPTIONS.find(x =>
lowercaseInput.startsWith(`${x.toLowerCase()}(`),
) || null;
this.clearedAggregateInInput = this.state.aggregateInInput;
this.setState(nextState);
this.setState({ aggregateInInput });
}
optionsForSelect(props) {
const { columns, savedMetrics } = props;
const aggregates =
columns && columns.length
? Object.keys(AGGREGATES).map(aggregate => ({
? AGGREGATES_OPTIONS.map(aggregate => ({
aggregate_name: aggregate,
}))
: [];
@@ -292,7 +297,7 @@ export default class MetricsControl extends React.PureComponent {
return sqlaAutoGeneratedMetricNameRegex.test(savedMetric.metric_name);
}
selectFilterOption(option, filterValue) {
selectFilterOption({ data: option }, filterValue) {
if (this.state.aggregateInInput) {
let endIndex = filterValue.length;
if (filterValue.endsWith(')')) {
@@ -324,11 +329,11 @@ export default class MetricsControl extends React.PureComponent {
<div className="metrics-select">
<ControlHeader {...this.props} />
<OnPasteSelect
multi={this.props.multi}
isMulti={this.props.multi}
name={`select-${this.props.name}`}
placeholder={t('choose a column or aggregate function')}
options={this.state.options}
value={this.props.multi ? this.state.value : this.state.value[0]}
value={this.state.value}
labelKey="label"
valueKey="optionName"
clearable={this.props.clearable}
@@ -336,10 +341,10 @@ export default class MetricsControl extends React.PureComponent {
onChange={this.onChange}
optionRenderer={this.optionRenderer}
valueRenderer={this.valueRenderer}
valueRenderedAsLabel
onInputChange={this.checkIfAggregateInInput}
filterOption={this.selectFilterOption}
refFunc={this.refFunc}
selectWrap={VirtualizedSelect}
selectRef={this.selectRef}
/>
</div>
);