teableio/teable

View on GitHub
packages/sdk/src/components/editor/user/EditorBase.tsx

Summary

Maintainability
A
1 hr
Test Coverage
import type { IUserCellValue } from '@teable/core';
import { Check } from '@teable/icons';
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  Skeleton,
  cn,
} from '@teable/ui-lib';
import type { ForwardRefRenderFunction } from 'react';
import { useCallback, useImperativeHandle, useRef, forwardRef } from 'react';
import { useTranslation } from '../../../context/app/i18n';
import type { ICellEditor, IEditorRef } from '../type';
import type { ICollaborator } from './types';
import { UserOption } from './UserOption';

export interface IUserEditorBaseProps extends ICellEditor<IUserCellValue | IUserCellValue[]> {
  isMultiple?: boolean;
  onChange?: (value?: IUserCellValue | IUserCellValue[]) => void;
  className?: string;
  collaborators?: ICollaborator[];
  isLoading?: boolean;
}

export type IUserEditorRef = IEditorRef<IUserCellValue | IUserCellValue[] | undefined>;

const UserEditorBaseRef: ForwardRefRenderFunction<IUserEditorRef, IUserEditorBaseProps> = (
  props,
  ref
) => {
  const {
    value: cellValue,
    style,
    className,
    isLoading,
    isMultiple,
    collaborators,
    onChange,
  } = props;
  const inputRef = useRef<HTMLInputElement | null>(null);
  const { t } = useTranslation();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current?.focus();
    },
  }));

  const onSelect = (value: IUserCellValue) => {
    if (isMultiple) {
      const innerValue = (cellValue || []) as IUserCellValue[];
      const newValue = innerValue.some((v) => v.id === value.id)
        ? innerValue.filter((v) => v.id !== value.id)
        : [...innerValue, value];
      onChange?.(newValue);
      return;
    }
    onChange?.(value.id === (cellValue as IUserCellValue)?.id ? undefined : value);
  };

  const activeStatus = useCallback(
    (value: string) => {
      const originValue = isMultiple
        ? (cellValue as IUserCellValue[])?.map((user) => user?.id)
        : [(cellValue as IUserCellValue)?.id];

      return originValue?.includes(value);
    },
    [cellValue, isMultiple]
  );

  return (
    <Command className={className} style={style}>
      <CommandInput ref={inputRef} placeholder={t('editor.user.searchPlaceholder')} />
      <CommandList>
        <CommandEmpty>{t('common.search.empty')}</CommandEmpty>
        <CommandGroup aria-valuetext="name">
          {isLoading ? (
            <CommandItem className="flex items-center space-x-4">
              <Skeleton className="size-7 rounded-full" />
              <Skeleton className="h-4 w-32" />
            </CommandItem>
          ) : (
            collaborators?.map(({ userId, userName, avatar, email }) => (
              <CommandItem
                key={userId}
                value={userName}
                onSelect={() => onSelect({ id: userId, title: userName, avatarUrl: avatar, email })}
                className="flex justify-between"
              >
                <UserOption name={userName} email={email} avatar={avatar} />
                <Check
                  className={cn('ml-2 h-4 w-4', activeStatus(userId) ? 'opacity-100' : 'opacity-0')}
                />
              </CommandItem>
            ))
          )}
        </CommandGroup>
      </CommandList>
    </Command>
  );
};

export const UserEditorBase = forwardRef(UserEditorBaseRef);