mirror of
https://github.com/apache/superset.git
synced 2026-04-19 16:14:52 +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,15 +18,12 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import Select, { Creatable } from 'react-select';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import VirtualizedRendererWrap from '../../../components/VirtualizedRendererWrap';
|
||||
import OnPasteSelect from '../../../components/OnPasteSelect';
|
||||
import { Select, CreatableSelect, OnPasteSelect } from 'src/components/Select';
|
||||
import ControlHeader from 'src/explore/components/ControlHeader';
|
||||
|
||||
const propTypes = {
|
||||
autoFocus: PropTypes.bool,
|
||||
choices: PropTypes.array,
|
||||
clearable: PropTypes.bool,
|
||||
description: PropTypes.string,
|
||||
@@ -35,6 +32,7 @@ const propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
multi: PropTypes.bool,
|
||||
isMulti: PropTypes.bool,
|
||||
allowAll: PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
@@ -51,13 +49,14 @@ const propTypes = {
|
||||
options: PropTypes.array,
|
||||
placeholder: PropTypes.string,
|
||||
noResultsText: PropTypes.string,
|
||||
refFunc: PropTypes.func,
|
||||
selectRef: PropTypes.func,
|
||||
filterOption: PropTypes.func,
|
||||
promptTextCreator: PropTypes.func,
|
||||
commaChoosesOption: PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
autoFocus: false,
|
||||
choices: [],
|
||||
clearable: true,
|
||||
description: null,
|
||||
@@ -69,8 +68,6 @@ const defaultProps = {
|
||||
onChange: () => {},
|
||||
onFocus: () => {},
|
||||
showHeader: true,
|
||||
optionRenderer: opt => opt.label,
|
||||
valueRenderer: opt => opt.label,
|
||||
valueKey: 'value',
|
||||
noResultsText: t('No results found'),
|
||||
promptTextCreator: label => `Create Option ${label}`,
|
||||
@@ -84,6 +81,9 @@ export default class SelectControl extends React.PureComponent {
|
||||
this.state = { options: this.getOptions(props) };
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.createMetaSelectAllOption = this.createMetaSelectAllOption.bind(this);
|
||||
this.select = null; // pointer to the react-select instance
|
||||
this.getSelectRef = this.getSelectRef.bind(this);
|
||||
this.handleKeyDownForCreate = this.handleKeyDownForCreate.bind(this);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
@@ -102,14 +102,16 @@ export default class SelectControl extends React.PureComponent {
|
||||
if (this.props.multi) {
|
||||
optionValue = [];
|
||||
for (const o of opt) {
|
||||
// select all options
|
||||
if (o.meta === true) {
|
||||
optionValue = this.getOptions(this.props)
|
||||
.filter(x => !x.meta)
|
||||
.map(x => x[this.props.valueKey]);
|
||||
break;
|
||||
} else {
|
||||
optionValue.push(o[this.props.valueKey]);
|
||||
this.props.onChange(
|
||||
this.getOptions(this.props)
|
||||
.filter(x => !x.meta)
|
||||
.map(x => x[this.props.valueKey]),
|
||||
);
|
||||
return;
|
||||
}
|
||||
optionValue.push(o[this.props.valueKey] || o);
|
||||
}
|
||||
} else if (opt.meta === true) {
|
||||
return;
|
||||
@@ -117,46 +119,47 @@ export default class SelectControl extends React.PureComponent {
|
||||
optionValue = opt[this.props.valueKey];
|
||||
}
|
||||
}
|
||||
// will eventually call `exploreReducer`: SET_FIELD_VALUE
|
||||
this.props.onChange(optionValue);
|
||||
}
|
||||
|
||||
getSelectRef(instance) {
|
||||
this.select = instance;
|
||||
if (this.props.selectRef) {
|
||||
this.props.selectRef(instance);
|
||||
}
|
||||
}
|
||||
|
||||
getOptions(props) {
|
||||
let options = [];
|
||||
if (props.options) {
|
||||
options = props.options.map(x => x);
|
||||
} else {
|
||||
} else if (props.choices) {
|
||||
// Accepts different formats of input
|
||||
options = props.choices.map(c => {
|
||||
let option;
|
||||
if (Array.isArray(c)) {
|
||||
const label = c.length > 1 ? c[1] : c[0];
|
||||
option = { label };
|
||||
option[props.valueKey] = c[0];
|
||||
} else if (Object.is(c)) {
|
||||
option = c;
|
||||
} else {
|
||||
option = { label: c };
|
||||
option[props.valueKey] = c;
|
||||
const [value, label] = c.length > 1 ? c : [c[0], c[0]];
|
||||
return { label, [props.valueKey]: value };
|
||||
}
|
||||
return option;
|
||||
if (Object.is(c)) {
|
||||
return c;
|
||||
}
|
||||
return { label: c, [props.valueKey]: c };
|
||||
});
|
||||
}
|
||||
if (props.freeForm) {
|
||||
// For FreeFormSelect, insert value into options if not exist
|
||||
const values = options.map(c => c[props.valueKey]);
|
||||
if (props.value) {
|
||||
let valuesToAdd = props.value;
|
||||
if (!Array.isArray(valuesToAdd)) {
|
||||
valuesToAdd = [valuesToAdd];
|
||||
// For FreeFormSelect, insert newly created values into options
|
||||
if (props.freeForm && props.value) {
|
||||
const existingOptionValues = new Set(options.map(c => c[props.valueKey]));
|
||||
const selectedValues = Array.isArray(props.value)
|
||||
? props.value
|
||||
: [props.value];
|
||||
selectedValues.forEach(v => {
|
||||
if (!existingOptionValues.has(v)) {
|
||||
// place the newly created options at the top
|
||||
options.unshift({ label: v, [props.valueKey]: v });
|
||||
}
|
||||
valuesToAdd.forEach(v => {
|
||||
if (values.indexOf(v) < 0) {
|
||||
const toAdd = { label: v };
|
||||
toAdd[props.valueKey] = v;
|
||||
options.push(toAdd);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (props.allowAll === true && props.multi === true) {
|
||||
if (options.findIndex(o => this.isMetaSelectAllOption(o)) < 0) {
|
||||
@@ -168,6 +171,16 @@ export default class SelectControl extends React.PureComponent {
|
||||
return options;
|
||||
}
|
||||
|
||||
handleKeyDownForCreate(event) {
|
||||
const key = event.key;
|
||||
if (key === 'Tab' || (this.props.commaChoosesOption && key === ',')) {
|
||||
// simulate an Enter event
|
||||
if (this.select) {
|
||||
this.select.onKeyDown({ ...event, key: 'Enter' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isMetaSelectAllOption(o) {
|
||||
return o.meta && o.meta === true && o.label === 'Select All';
|
||||
}
|
||||
@@ -182,44 +195,49 @@ export default class SelectControl extends React.PureComponent {
|
||||
// Tab, comma or Enter will trigger a new option created for FreeFormSelect
|
||||
const placeholder =
|
||||
this.props.placeholder || t('%s option(s)', this.state.options.length);
|
||||
const isMulti = this.props.isMulti || this.props.multi;
|
||||
|
||||
const selectProps = {
|
||||
multi: this.props.multi,
|
||||
autoFocus: this.props.autoFocus,
|
||||
isMulti,
|
||||
selectRef: this.getSelectRef,
|
||||
name: `select-${this.props.name}`,
|
||||
placeholder,
|
||||
options: this.state.options,
|
||||
value: this.props.value,
|
||||
labelKey: 'label',
|
||||
valueKey: this.props.valueKey,
|
||||
autosize: false,
|
||||
clearable: this.props.clearable,
|
||||
isLoading: this.props.isLoading,
|
||||
onChange: this.onChange,
|
||||
onFocus: this.props.onFocus,
|
||||
optionRenderer: VirtualizedRendererWrap(this.props.optionRenderer),
|
||||
optionRenderer: this.props.optionRenderer,
|
||||
valueRenderer: this.props.valueRenderer,
|
||||
noResultsText: this.props.noResultsText,
|
||||
disabled: this.props.disabled,
|
||||
refFunc: this.props.refFunc,
|
||||
filterOption: this.props.filterOption,
|
||||
promptTextCreator: this.props.promptTextCreator,
|
||||
ignoreAccents: false,
|
||||
};
|
||||
|
||||
let SelectComponent;
|
||||
if (this.props.freeForm) {
|
||||
selectProps.selectComponent = Creatable;
|
||||
selectProps.shouldKeyDownEventCreateNewOption = key => {
|
||||
const keyCode = key.keyCode;
|
||||
if (this.props.commaChoosesOption && keyCode === 188) {
|
||||
return true;
|
||||
}
|
||||
return keyCode === 9 || keyCode === 13;
|
||||
};
|
||||
SelectComponent = CreatableSelect;
|
||||
// Don't create functions in `render` because React relies on shallow
|
||||
// compare to decide weathere to rerender child components.
|
||||
selectProps.onKeyDown = this.handleKeyDownForCreate;
|
||||
} else {
|
||||
selectProps.selectComponent = Select;
|
||||
SelectComponent = Select;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.props.showHeader && <ControlHeader {...this.props} />}
|
||||
<OnPasteSelect {...selectProps} selectWrap={VirtualizedSelect} />
|
||||
{isMulti ? (
|
||||
<OnPasteSelect {...selectProps} selectWrap={SelectComponent} />
|
||||
) : (
|
||||
<SelectComponent {...selectProps} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user