Coursemology/coursemology2

View on GitHub
client/app/bundles/course/assessment/question/programming/components/common/DataFileRow.tsx

Summary

Maintainability
A
3 hrs
Test Coverage
import { Delete, Undo } from '@mui/icons-material';
import { IconButton, TableCell, TableRow } from '@mui/material';
import { DataFile } from 'types/course/assessment/question/programming';

const KB = 1000;
const SIZES = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];

const formatReadableBytes = (bytes: number, decimals = 3): string => {
  if (bytes === 0) return 'Zero bytes';

  const i = Math.floor(Math.log(bytes) / Math.log(KB));
  return `${parseFloat((bytes / KB ** i).toFixed(decimals))} ${SIZES[i]}`;
};

type Marked<T> = [T];
type MaybeMarked<T> = T | Marked<T>;

const mark = <T,>(thing: T): Marked<T> => [thing];

const unmark = <T,>(markedThing: Marked<T>): T => markedThing[0];

export const isMarked = <T,>(thing: MaybeMarked<T>): thing is Marked<T> =>
  Array.isArray(thing);

export const unwrap = <T,>(thing: MaybeMarked<T>): T =>
  isMarked(thing) ? thing[0] : thing;

export interface DraftableDataFile extends DataFile {
  raw?: File;
}

export const isDraftable = (
  file: DataFile | DraftableDataFile,
): file is DraftableDataFile => 'raw' in file;

interface DataFileRowProps {
  of: MaybeMarked<DraftableDataFile>;
  onChange?: (file: MaybeMarked<DraftableDataFile>) => void;
  onDelete?: () => void;
  disabled?: boolean;
}

const DataFileRow = (props: DataFileRowProps): JSX.Element => {
  const file = unwrap(props.of);
  const toBeDeleted = isMarked(props.of);

  const handleClickDelete = (): void => {
    if (file.raw) {
      props.onDelete?.();
    } else {
      if (toBeDeleted) return;

      // TypeScript's type narrowing cannot handle the fact that `toBeDeleted`
      // is a return value of a type guard, so we need to type-assert here.
      props.onChange?.(mark(props.of as DraftableDataFile));
    }
  };

  const handleClickUndoDelete = (): void => {
    if (!toBeDeleted) return;

    props.onChange?.(unmark(props.of as Marked<DraftableDataFile>));
  };

  return (
    <TableRow
      className={`${file.raw ? 'bg-lime-50' : ''} ${
        toBeDeleted ? 'bg-neutral-200 line-through' : ''
      }`}
    >
      <TableCell className="break-all">{file.filename}</TableCell>

      <TableCell className="whitespace-nowrap">
        {formatReadableBytes(file.size, 2)}
      </TableCell>

      <TableCell>
        {toBeDeleted ? (
          <IconButton
            color="info"
            disabled={props.disabled}
            edge="end"
            onClick={handleClickUndoDelete}
          >
            <Undo />
          </IconButton>
        ) : (
          <IconButton
            color="error"
            disabled={props.disabled}
            edge="end"
            onClick={handleClickDelete}
          >
            <Delete />
          </IconButton>
        )}
      </TableCell>
    </TableRow>
  );
};

export default DataFileRow;