| /** |
| * 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, { SyntheticEvent, MutableRefObject, ComponentType } from 'react'; |
| import { merge } from 'lodash'; |
| import BasicSelect, { |
| OptionTypeBase, |
| MultiValueProps, |
| FormatOptionLabelMeta, |
| ValueType, |
| SelectComponentsConfig, |
| components as defaultComponents, |
| createFilter, |
| } from 'react-select'; |
| import Async from 'react-select/async'; |
| import Creatable from 'react-select/creatable'; |
| import AsyncCreatable from 'react-select/async-creatable'; |
| import { withAsyncPaginate } from 'react-select-async-paginate'; |
| |
| import { SelectComponents } from 'react-select/src/components'; |
| import { |
| SortableContainer, |
| SortableElement, |
| SortableContainerProps, |
| } from 'react-sortable-hoc'; |
| import arrayMove from 'array-move'; |
| import { Props as SelectProps } from 'react-select/src/Select'; |
| import { useTheme } from '@superset-ui/core'; |
| import { |
| WindowedSelectComponentType, |
| WindowedSelectProps, |
| WindowedSelect, |
| WindowedAsyncSelect, |
| WindowedCreatableSelect, |
| WindowedAsyncCreatableSelect, |
| } from './WindowedSelect'; |
| import { |
| DEFAULT_CLASS_NAME, |
| DEFAULT_CLASS_NAME_PREFIX, |
| DEFAULT_STYLES, |
| DEFAULT_COMPONENTS, |
| VALUE_LABELED_STYLES, |
| PartialThemeConfig, |
| PartialStylesConfig, |
| SelectComponentsType, |
| InputProps, |
| defaultTheme, |
| } from './styles'; |
| import { findValue } from './utils'; |
| |
| type AnyReactSelect<OptionType extends OptionTypeBase> = |
| | BasicSelect<OptionType> |
| | Async<OptionType> |
| | Creatable<OptionType> |
| | AsyncCreatable<OptionType>; |
| |
| export type SupersetStyledSelectProps< |
| OptionType extends OptionTypeBase, |
| T extends WindowedSelectProps<OptionType> = WindowedSelectProps<OptionType> |
| > = T & { |
| // additional props for easier usage or backward compatibility |
| labelKey?: string; |
| valueKey?: string; |
| assistiveText?: string; |
| multi?: boolean; |
| clearable?: boolean; |
| sortable?: boolean; |
| ignoreAccents?: boolean; |
| creatable?: boolean; |
| selectRef?: |
| | React.RefCallback<AnyReactSelect<OptionType>> |
| | MutableRefObject<AnyReactSelect<OptionType>>; |
| getInputValue?: (selectBalue: ValueType<OptionType>) => string | undefined; |
| optionRenderer?: (option: OptionType) => React.ReactNode; |
| valueRenderer?: (option: OptionType) => React.ReactNode; |
| valueRenderedAsLabel?: boolean; |
| // callback for paste event |
| onPaste?: (e: SyntheticEvent) => void; |
| forceOverflow?: boolean; |
| // for simplier theme overrides |
| themeConfig?: PartialThemeConfig; |
| stylesConfig?: PartialStylesConfig; |
| }; |
| |
| function styled< |
| OptionType extends OptionTypeBase, |
| SelectComponentType extends |
| | WindowedSelectComponentType<OptionType> |
| | ComponentType< |
| SelectProps<OptionType> |
| > = WindowedSelectComponentType<OptionType> |
| >(SelectComponent: SelectComponentType) { |
| type SelectProps = SupersetStyledSelectProps<OptionType>; |
| type Components = SelectComponents<OptionType>; |
| |
| const SortableSelectComponent = SortableContainer(SelectComponent, { |
| withRef: true, |
| }); |
| |
| // default components for the given OptionType |
| const supersetDefaultComponents: SelectComponentsConfig<OptionType> = DEFAULT_COMPONENTS; |
| |
| const getSortableMultiValue = (MultiValue: Components['MultiValue']) => |
| SortableElement((props: MultiValueProps<OptionType>) => { |
| const onMouseDown = (e: SyntheticEvent) => { |
| e.preventDefault(); |
| e.stopPropagation(); |
| }; |
| const innerProps = { onMouseDown }; |
| return <MultiValue {...props} innerProps={innerProps} />; |
| }); |
| |
| /** |
| * Superset styled `Select` component. Apply Superset themed stylesheets and |
| * consolidate props API for backward compatibility with react-select v1. |
| */ |
| function StyledSelect(selectProps: SelectProps) { |
| let stateManager: AnyReactSelect<OptionType>; // reference to react-select StateManager |
| const { |
| // additional props for Superset Select |
| selectRef, |
| labelKey = 'label', |
| valueKey = 'value', |
| themeConfig, |
| stylesConfig = {}, |
| optionRenderer, |
| valueRenderer, |
| // whether value is rendered as `option-label` in input, |
| // useful for AdhocMetric and AdhocFilter |
| valueRenderedAsLabel: valueRenderedAsLabel_, |
| onPaste, |
| multi = false, // same as `isMulti`, used for backward compatibility |
| clearable, // same as `isClearable` |
| sortable = true, // whether to enable drag & drop sorting |
| forceOverflow, // whether the dropdown should be forcefully overflowing |
| |
| // react-select props |
| className = DEFAULT_CLASS_NAME, |
| classNamePrefix = DEFAULT_CLASS_NAME_PREFIX, |
| options, |
| value: value_, |
| components: components_, |
| isMulti: isMulti_, |
| isClearable: isClearable_, |
| minMenuHeight = 100, // apply different defaults |
| maxMenuHeight = 220, |
| filterOption, |
| ignoreAccents = false, // default is `true`, but it is slow |
| |
| getOptionValue = option => |
| typeof option === 'string' ? option : option[valueKey], |
| |
| getOptionLabel = option => |
| typeof option === 'string' |
| ? option |
| : option[labelKey] || option[valueKey], |
| |
| formatOptionLabel = ( |
| option: OptionType, |
| { context }: FormatOptionLabelMeta<OptionType>, |
| ) => { |
| if (context === 'value') { |
| return valueRenderer ? valueRenderer(option) : getOptionLabel(option); |
| } |
| return optionRenderer ? optionRenderer(option) : getOptionLabel(option); |
| }, |
| |
| ...restProps |
| } = selectProps; |
| |
| // `value` may be rendered values (strings), we want option objects |
| const value: OptionType[] = findValue(value_, options || [], valueKey); |
| |
| // Add backward compability to v1 API |
| const isMulti = isMulti_ === undefined ? multi : isMulti_; |
| const isClearable = isClearable_ === undefined ? clearable : isClearable_; |
| |
| // Sort is only applied when there are multiple selected values |
| const shouldAllowSort = |
| isMulti && sortable && Array.isArray(value) && value.length > 1; |
| |
| const MaybeSortableSelect = shouldAllowSort |
| ? SortableSelectComponent |
| : SelectComponent; |
| const components = { ...supersetDefaultComponents, ...components_ }; |
| |
| // Make multi-select sortable as per https://react-select.netlify.app/advanced |
| if (shouldAllowSort) { |
| components.MultiValue = getSortableMultiValue( |
| components.MultiValue || defaultComponents.MultiValue, |
| ); |
| |
| const sortableContainerProps: Partial<SortableContainerProps> = { |
| getHelperDimensions: ({ node }) => node.getBoundingClientRect(), |
| axis: 'xy', |
| onSortEnd: ({ oldIndex, newIndex }) => { |
| const newValue = arrayMove(value, oldIndex, newIndex); |
| if (restProps.onChange) { |
| restProps.onChange(newValue, { action: 'set-value' }); |
| } |
| }, |
| distance: 4, |
| }; |
| Object.assign(restProps, sortableContainerProps); |
| } |
| |
| // When values are rendered as labels, adjust valueContainer padding |
| const valueRenderedAsLabel = |
| valueRenderedAsLabel_ === undefined ? isMulti : valueRenderedAsLabel_; |
| if (valueRenderedAsLabel && !stylesConfig.valueContainer) { |
| Object.assign(stylesConfig, VALUE_LABELED_STYLES); |
| } |
| |
| // Handle onPaste event |
| if (onPaste) { |
| const Input = |
| (components.Input as SelectComponentsType['Input']) || |
| (defaultComponents.Input as SelectComponentsType['Input']); |
| components.Input = (props: InputProps) => ( |
| <Input {...props} onPaste={onPaste} /> |
| ); |
| } |
| // for CreaTable |
| if (SelectComponent === WindowedCreatableSelect) { |
| restProps.getNewOptionData = (inputValue: string, label: string) => ({ |
| label: label || inputValue, |
| [valueKey]: inputValue, |
| isNew: true, |
| }); |
| } |
| |
| // handle forcing dropdown overflow |
| // use only when setting overflow:visible isn't possible on the container element |
| if (forceOverflow) { |
| Object.assign(restProps, { |
| closeMenuOnScroll: (e: Event) => { |
| const target = e.target as HTMLElement; |
| return target && !target.classList?.contains('Select__menu-list'); |
| }, |
| menuPosition: 'fixed', |
| }); |
| } |
| |
| // Make sure always return StateManager for the refs. |
| // To get the real `Select` component, keep tap into `obj.select`: |
| // - for normal <Select /> component: StateManager -> Select, |
| // - for <Creatable />: StateManager -> Creatable -> Select |
| const setRef = (instance: any) => { |
| stateManager = |
| shouldAllowSort && instance && 'refs' in instance |
| ? instance.refs.wrappedInstance // obtain StateManger from SortableContainer |
| : instance; |
| if (typeof selectRef === 'function') { |
| selectRef(stateManager); |
| } else if (selectRef && 'current' in selectRef) { |
| selectRef.current = stateManager; |
| } |
| }; |
| |
| const theme = useTheme(); |
| |
| return ( |
| <MaybeSortableSelect |
| ref={setRef} |
| className={className} |
| classNamePrefix={classNamePrefix} |
| isMulti={isMulti} |
| isClearable={isClearable} |
| options={options} |
| value={value} |
| minMenuHeight={minMenuHeight} |
| maxMenuHeight={maxMenuHeight} |
| filterOption={ |
| // filterOption may be NULL |
| filterOption !== undefined |
| ? filterOption |
| : createFilter({ ignoreAccents }) |
| } |
| styles={{ ...DEFAULT_STYLES, ...stylesConfig } as SelectProps['styles']} |
| // merge default theme from `react-select`, default theme for Superset, |
| // and the theme from props. |
| theme={reactSelectTheme => |
| merge(reactSelectTheme, defaultTheme(theme), themeConfig) |
| } |
| formatOptionLabel={formatOptionLabel} |
| getOptionLabel={getOptionLabel} |
| getOptionValue={getOptionValue} |
| components={components} |
| {...restProps} |
| /> |
| ); |
| } |
| |
| // React.memo makes sure the component does no rerender given the same props |
| return React.memo(StyledSelect); |
| } |
| |
| export const Select = styled(WindowedSelect); |
| export const AsyncSelect = styled(WindowedAsyncSelect); |
| export const CreatableSelect = styled(WindowedCreatableSelect); |
| export const AsyncCreatableSelect = styled(WindowedAsyncCreatableSelect); |
| export const PaginatedSelect = withAsyncPaginate( |
| styled<OptionTypeBase, ComponentType<SelectProps<OptionTypeBase>>>( |
| BasicSelect, |
| ), |
| ); |
| export default Select; |