teableio/teable

View on GitHub
packages/sdk/src/components/grid-enhancements/editor/GridSelectEditor.tsx

Summary

Maintainability
A
1 hr
Test Coverage
import type {
  ISelectFieldOptions,
  ISingleSelectCellValue,
  IMultipleSelectCellValue,
  ISelectFieldChoice,
} from '@teable/core';
import { FieldType, ColorUtils } from '@teable/core';
import type { ForwardRefRenderFunction } from 'react';
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
import colors from 'tailwindcss/colors';
import { useTableId } from '../../../hooks';
import type { MultipleSelectField, SingleSelectField } from '../../../model';
import { Field } from '../../../model';
import { SelectEditorMain } from '../../editor';
import type { IEditorRef } from '../../editor/type';
import type { IEditorProps } from '../../grid/components';
import { useGridPopupPosition } from '../hooks';
import type { IWrapperEditorProps } from './type';

const GridSelectEditorBase: ForwardRefRenderFunction<
  IEditorRef<string | string[] | undefined>,
  IWrapperEditorProps & IEditorProps
> = (props, ref) => {
  const { field, record, rect, style, isEditing, setEditing } = props;
  const tableId = useTableId();
  const defaultFocusRef = useRef<HTMLInputElement | null>(null);
  const editorRef = useRef<IEditorRef<string | string[] | undefined>>(null);
  const {
    id: fieldId,
    type: fieldType,
    options,
    displayChoiceMap,
  } = field as SingleSelectField | MultipleSelectField;
  const isMultiple = fieldType === FieldType.MultipleSelect;
  const cellValue = record.getCellValue(field.id) as
    | ISingleSelectCellValue
    | IMultipleSelectCellValue;
  const attachStyle = useGridPopupPosition(rect, 340);

  useEffect(() => {
    if (isMultiple) {
      editorRef.current?.setValue?.(cellValue);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [JSON.stringify(cellValue)]);

  useImperativeHandle(ref, () => ({
    focus: () => (editorRef.current || defaultFocusRef.current)?.focus?.(),
    setValue: (value?: string | string[]) => {
      editorRef.current?.setValue?.(value);
    },
  }));

  const selectOptions = useMemo(() => {
    const choices = (options as ISelectFieldOptions)?.choices || [];
    return choices.map(({ name, color }) => ({
      label: name,
      value: name,
      color:
        displayChoiceMap[name]?.color ??
        (ColorUtils.shouldUseLightTextOnColor(color) ? colors.white : colors.black),
      backgroundColor: displayChoiceMap[name]?.backgroundColor ?? ColorUtils.getHexForColor(color),
    }));
  }, [options, displayChoiceMap]);

  const onChange = (value?: string[] | string) => {
    record.updateCell(fieldId, isMultiple && value?.length === 0 ? null : value);
    if (!isMultiple) setTimeout(() => setEditing?.(false));
  };

  const onOptionAdd = useCallback(
    async (name: string) => {
      if (!tableId) return;

      const { choices = [] } = options as ISelectFieldOptions;
      const existColors = choices.map((v) => v.color);
      const choice = {
        name,
        color: ColorUtils.randomColor(existColors)[0],
      } as ISelectFieldChoice;

      const newChoices = [...choices, choice];

      await Field.convertField(tableId, fieldId, {
        type: fieldType,
        options: { ...options, choices: newChoices },
      });
    },
    [tableId, fieldType, fieldId, options]
  );

  return (
    <>
      {isEditing ? (
        <SelectEditorMain
          ref={editorRef}
          style={{
            ...style,
            ...attachStyle,
            height: 'auto',
          }}
          className="absolute rounded-sm border p-2 shadow-sm"
          value={cellValue === null ? undefined : cellValue}
          isMultiple={isMultiple}
          options={selectOptions}
          onChange={onChange}
          onOptionAdd={onOptionAdd}
        />
      ) : (
        <input className="size-0 opacity-0" ref={defaultFocusRef} />
      )}
    </>
  );
};

export const GridSelectEditor = forwardRef(GridSelectEditorBase);