mirror of
https://github.com/apache/superset.git
synced 2026-04-24 02:25:13 +00:00
323 lines
11 KiB
TypeScript
323 lines
11 KiB
TypeScript
/**
|
|
* 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;
|