airbnb/caravel

View on GitHub
superset-frontend/src/components/Select/utils.tsx

Summary

Maintainability
A
2 hrs
Test Coverage
/**
 * 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,
  }));