| /** |
| * 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 { ensureIsArray, t } from '@superset-ui/core'; |
| import AntdSelect, { LabeledValue as AntdLabeledValue } from 'antd/lib/select'; |
| import { ReactElement, RefObject } from 'react'; |
| import Icons from 'src/components/Icons'; |
| import { StyledHelperText, StyledLoadingText, StyledSpin } from './styles'; |
| import { LabeledValue, RawValue, SelectOptionsType, V } from './types'; |
| |
| const { Option } = AntdSelect; |
| |
| export const SELECT_ALL_VALUE: RawValue = 'Select All'; |
| export const selectAllOption = { |
| value: SELECT_ALL_VALUE, |
| label: String(SELECT_ALL_VALUE), |
| }; |
| |
| export function isObject(value: unknown): value is Record<string, unknown> { |
| return ( |
| value !== null && |
| typeof value === 'object' && |
| Array.isArray(value) === false |
| ); |
| } |
| |
| export function isLabeledValue(value: unknown): value is AntdLabeledValue { |
| return isObject(value) && 'value' in value && 'label' in value; |
| } |
| |
| export function getValue( |
| option: string | number | AntdLabeledValue | null | undefined, |
| ) { |
| return isLabeledValue(option) ? option.value : option; |
| } |
| |
| export function isEqual(a: V | LabeledValue, b: V | LabeledValue, key: string) { |
| const actualA = isObject(a) && key in a ? a[key] : a; |
| const actualB = isObject(b) && key in b ? b[key] : b; |
| // When comparing the values we use the equality |
| // operator to automatically convert different types |
| // eslint-disable-next-line eqeqeq |
| return actualA == actualB; |
| } |
| |
| export function getOption( |
| value: V, |
| options?: V | LabeledValue | (V | LabeledValue)[], |
| checkLabel = false, |
| ): V | LabeledValue { |
| const optionsArray = ensureIsArray(options); |
| return optionsArray.find( |
| x => |
| isEqual(x, value, 'value') || (checkLabel && isEqual(x, value, 'label')), |
| ); |
| } |
| |
| export function hasOption( |
| value: V, |
| options?: V | LabeledValue | (V | LabeledValue)[], |
| checkLabel = false, |
| ): boolean { |
| return getOption(value, options, checkLabel) !== undefined; |
| } |
| |
| /** |
| * It creates a comparator to check for a specific property. |
| * Can be used with string and number property values. |
| * */ |
| export const propertyComparator = |
| (property: string) => (a: AntdLabeledValue, b: AntdLabeledValue) => { |
| if (typeof a[property] === 'string' && typeof b[property] === 'string') { |
| return a[property].localeCompare(b[property]); |
| } |
| return (a[property] as number) - (b[property] as number); |
| }; |
| |
| export const sortSelectedFirstHelper = ( |
| a: AntdLabeledValue, |
| b: AntdLabeledValue, |
| selectValue: |
| | string |
| | number |
| | RawValue[] |
| | AntdLabeledValue |
| | AntdLabeledValue[] |
| | undefined, |
| ) => |
| selectValue && a.value !== undefined && b.value !== undefined |
| ? Number(hasOption(b.value, selectValue)) - |
| Number(hasOption(a.value, selectValue)) |
| : 0; |
| |
| export const sortComparatorWithSearchHelper = ( |
| a: AntdLabeledValue, |
| b: AntdLabeledValue, |
| inputValue: string, |
| sortCallback: (a: AntdLabeledValue, b: AntdLabeledValue) => number, |
| sortComparator: ( |
| a: AntdLabeledValue, |
| b: AntdLabeledValue, |
| search?: string | undefined, |
| ) => number, |
| ) => sortCallback(a, b) || sortComparator(a, b, inputValue); |
| |
| export const sortComparatorForNoSearchHelper = ( |
| a: AntdLabeledValue, |
| b: AntdLabeledValue, |
| sortCallback: (a: AntdLabeledValue, b: AntdLabeledValue) => number, |
| sortComparator: ( |
| a: AntdLabeledValue, |
| b: AntdLabeledValue, |
| search?: string | undefined, |
| ) => number, |
| ) => sortCallback(a, b) || sortComparator(a, b, ''); |
| |
| // use a function instead of component since every rerender of the |
| // Select component will create a new component |
| export const getSuffixIcon = ( |
| isLoading: boolean | undefined, |
| showSearch: boolean, |
| isDropdownVisible: boolean, |
| ) => { |
| if (isLoading) { |
| return <StyledSpin size="small" />; |
| } |
| if (showSearch && isDropdownVisible) { |
| return <Icons.SearchOutlined iconSize="s" />; |
| } |
| return <Icons.DownOutlined iconSize="s" />; |
| }; |
| |
| export const dropDownRenderHelper = ( |
| originNode: ReactElement & { ref?: RefObject<HTMLElement> }, |
| isDropdownVisible: boolean, |
| isLoading: boolean | undefined, |
| optionsLength: number, |
| helperText: string | undefined, |
| errorComponent?: JSX.Element, |
| ) => { |
| if (!isDropdownVisible) { |
| originNode.ref?.current?.scrollTo({ top: 0 }); |
| } |
| if (isLoading && optionsLength === 0) { |
| return <StyledLoadingText>{t('Loading...')}</StyledLoadingText>; |
| } |
| if (errorComponent) { |
| return errorComponent; |
| } |
| return ( |
| <> |
| {helperText && ( |
| <StyledHelperText role="note">{helperText}</StyledHelperText> |
| )} |
| {originNode} |
| </> |
| ); |
| }; |
| |
| export const handleFilterOptionHelper = ( |
| search: string, |
| option: AntdLabeledValue, |
| optionFilterProps: string[], |
| filterOption: boolean | Function, |
| ) => { |
| if (typeof filterOption === 'function') { |
| return filterOption(search, option); |
| } |
| |
| if (filterOption) { |
| const searchValue = search.trim().toLowerCase(); |
| if (optionFilterProps?.length) { |
| return optionFilterProps.some(prop => { |
| const optionProp = option?.[prop] |
| ? String(option[prop]).trim().toLowerCase() |
| : ''; |
| return optionProp.includes(searchValue); |
| }); |
| } |
| } |
| |
| return false; |
| }; |
| |
| export const hasCustomLabels = (options: SelectOptionsType) => |
| options?.some(opt => !!opt?.customLabel); |
| |
| export const renderSelectOptions = (options: SelectOptionsType) => |
| options.map(opt => { |
| const isOptObject = typeof opt === 'object'; |
| const label = isOptObject ? opt?.label || opt.value : opt; |
| const value = isOptObject ? opt.value : opt; |
| const { customLabel, ...optProps } = opt; |
| return ( |
| <Option {...optProps} key={value} label={label} value={value}> |
| {isOptObject && customLabel ? customLabel : label} |
| </Option> |
| ); |
| }); |
| |
| export const mapValues = (values: SelectOptionsType, labelInValue: boolean) => |
| labelInValue |
| ? values.map(opt => ({ |
| key: opt.value, |
| value: opt.value, |
| label: opt.label, |
| })) |
| : values.map(opt => opt.value); |
| |
| export const mapOptions = (values: SelectOptionsType) => |
| values.map(opt => ({ |
| children: opt.label, |
| key: opt.value, |
| ...opt, |
| })); |