| /** |
| * 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 React from 'react'; |
| import PropTypes from 'prop-types'; |
| import { t } from '@superset-ui/core'; |
| 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, |
| disabled: PropTypes.bool, |
| freeForm: PropTypes.bool, |
| isLoading: PropTypes.bool, |
| label: PropTypes.string, |
| multi: PropTypes.bool, |
| isMulti: PropTypes.bool, |
| allowAll: PropTypes.bool, |
| name: PropTypes.string.isRequired, |
| onChange: PropTypes.func, |
| onFocus: PropTypes.func, |
| value: PropTypes.oneOfType([ |
| PropTypes.string, |
| PropTypes.number, |
| PropTypes.array, |
| ]), |
| showHeader: PropTypes.bool, |
| optionRenderer: PropTypes.func, |
| valueRenderer: PropTypes.func, |
| valueKey: PropTypes.string, |
| options: PropTypes.array, |
| placeholder: PropTypes.string, |
| noResultsText: PropTypes.string, |
| selectRef: PropTypes.func, |
| filterOption: PropTypes.func, |
| promptTextCreator: PropTypes.func, |
| commaChoosesOption: PropTypes.bool, |
| menuPortalTarget: PropTypes.element, |
| menuPosition: PropTypes.string, |
| menuPlacement: PropTypes.string, |
| forceOverflow: PropTypes.bool, |
| }; |
| |
| const defaultProps = { |
| autoFocus: false, |
| choices: [], |
| clearable: true, |
| description: null, |
| disabled: false, |
| freeForm: false, |
| isLoading: false, |
| label: null, |
| multi: false, |
| onChange: () => {}, |
| onFocus: () => {}, |
| showHeader: true, |
| valueKey: 'value', |
| noResultsText: t('No results found'), |
| promptTextCreator: label => `Create Option ${label}`, |
| commaChoosesOption: true, |
| allowAll: false, |
| }; |
| |
| export default class SelectControl extends React.PureComponent { |
| constructor(props) { |
| super(props); |
| this.state = { |
| options: this.getOptions(props), |
| value: this.props.value, |
| }; |
| 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) { |
| if ( |
| nextProps.choices !== this.props.choices || |
| nextProps.options !== this.props.options |
| ) { |
| const options = this.getOptions(nextProps); |
| this.setState({ options }); |
| } |
| } |
| |
| // Beware: This is acting like an on-click instead of an on-change |
| // (firing every time user chooses vs firing only if a new option is chosen). |
| onChange(opt) { |
| let optionValue = this.props.multi ? [] : null; |
| if (opt) { |
| if (this.props.multi) { |
| opt.forEach(o => { |
| // select all options |
| if (o.meta === true) { |
| optionValue = 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; |
| } else { |
| 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 if (props.choices) { |
| // Accepts different formats of input |
| options = props.choices.map(c => { |
| if (Array.isArray(c)) { |
| const [value, label] = c.length > 1 ? c : [c[0], c[0]]; |
| return { label, [props.valueKey]: value }; |
| } |
| if (Object.is(c)) { |
| return c; |
| } |
| return { label: c, [props.valueKey]: c }; |
| }); |
| } |
| // 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 }); |
| } |
| }); |
| } |
| if (props.allowAll === true && props.multi === true) { |
| if (!this.optionsIncludesSelectAll(options)) { |
| options.unshift(this.createMetaSelectAllOption()); |
| } |
| } else { |
| options = options.filter(o => !this.isMetaSelectAllOption(o)); |
| } |
| return options; |
| } |
| |
| handleKeyDownForCreate(event) { |
| const { key } = event; |
| 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'; |
| } |
| |
| optionsIncludesSelectAll(o) { |
| return o.findIndex(o => this.isMetaSelectAllOption(o)) >= 0; |
| } |
| |
| optionsRemaining() { |
| const { options } = this.state; |
| const { value } = this.props; |
| // if select is multi/value is array, we show the options not selected |
| let remainingOptions = Array.isArray(value) |
| ? options.length - value.length |
| : options.length; |
| if (this.optionsIncludesSelectAll(options)) { |
| remainingOptions -= 1; |
| } |
| return remainingOptions; |
| } |
| |
| createMetaSelectAllOption() { |
| const option = { label: 'Select all', meta: true }; |
| option[this.props.valueKey] = 'Select all'; |
| return option; |
| } |
| |
| render() { |
| // Tab, comma or Enter will trigger a new option created for FreeFormSelect |
| const { |
| autoFocus, |
| clearable, |
| disabled, |
| filterOption, |
| isLoading, |
| menuPlacement, |
| name, |
| noResultsText, |
| onFocus, |
| optionRenderer, |
| promptTextCreator, |
| value, |
| valueKey, |
| valueRenderer, |
| forceOverflow, |
| menuPortalTarget, |
| menuPosition, |
| } = this.props; |
| |
| const optionsRemaining = this.optionsRemaining(); |
| const optionRemaingText = optionsRemaining |
| ? t('%s option(s)', optionsRemaining) |
| : ''; |
| const placeholder = this.props.placeholder || optionRemaingText; |
| const isMulti = this.props.isMulti || this.props.multi; |
| |
| let assistiveText; |
| if ( |
| isMulti && |
| optionsRemaining && |
| Array.isArray(this.state.value) && |
| !!value.length |
| ) { |
| assistiveText = optionRemaingText; |
| } |
| |
| const selectProps = { |
| autoFocus, |
| clearable, |
| disabled, |
| filterOption, |
| ignoreAccents: false, |
| isLoading, |
| isMulti, |
| labelKey: 'label', |
| menuPlacement, |
| forceOverflow, |
| menuPortalTarget, |
| menuPosition, |
| name: `select-${name}`, |
| noResultsText, |
| onChange: this.onChange, |
| onFocus, |
| optionRenderer, |
| value, |
| options: this.state.options, |
| placeholder, |
| assistiveText, |
| promptTextCreator, |
| selectRef: this.getSelectRef, |
| valueKey, |
| valueRenderer, |
| }; |
| |
| let SelectComponent; |
| if (this.props.freeForm) { |
| 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 { |
| SelectComponent = Select; |
| } |
| |
| return ( |
| <div> |
| {this.props.showHeader && <ControlHeader {...this.props} />} |
| {isMulti ? ( |
| <OnPasteSelect {...selectProps} selectWrap={SelectComponent} /> |
| ) : ( |
| <SelectComponent {...selectProps} /> |
| )} |
| </div> |
| ); |
| } |
| } |
| |
| SelectControl.propTypes = propTypes; |
| SelectControl.defaultProps = defaultProps; |