mirror of
https://github.com/apache/superset.git
synced 2026-05-12 19:35:17 +00:00
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:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user