Coursemology/coursemology2

View on GitHub
client/app/bundles/course/users/components/tables/ManageUsersTable/index.tsx

Summary

Maintainability
C
1 day
Test Coverage
import { ReactElement, useMemo } from 'react';
import { MenuItem, Typography } from '@mui/material';
import {
  CourseUserMiniEntity,
  CourseUserRoles,
} from 'types/course/courseUsers';

import Note from 'lib/components/core/Note';
import Table, { ColumnTemplate } from 'lib/components/table';
import {
  COURSE_USER_ROLES,
  DEFAULT_TABLE_ROWS_PER_PAGE,
} from 'lib/constants/sharedConstants';
import { useAppSelector } from 'lib/hooks/store';
import useTranslation from 'lib/hooks/useTranslation';
import tableTranslations from 'lib/translations/table';

import { getManageCourseUserPermissions } from '../../../selectors';

import ActiveTableToolbar from './ActiveTableToolbar';
import AlgorithmMenu from './AlgorithmMenu';
import PhantomSwitch from './PhantomSwitch';
import RoleMenu from './RoleMenu';
import TimelineMenu from './TimelineMenu';
import translations from './translations';
import UserNameField from './UserNameField';

interface ManageUsersTableProps {
  users: CourseUserMiniEntity[];
  manageStaff?: boolean;
  renderRowActionComponent?: (user: CourseUserMiniEntity) => ReactElement;
  csvDownloadFilename: string;
  timelinesMap?: Record<number, string>;
  className?: string;
}

const ManageUsersTable = (props: ManageUsersTableProps): JSX.Element => {
  const { users, manageStaff, timelinesMap, renderRowActionComponent } = props;

  const { t } = useTranslation();

  const permissions = useAppSelector(getManageCourseUserPermissions);

  const timelines = useMemo(
    () =>
      timelinesMap &&
      Object.entries(timelinesMap).map(([id, timelineTitle]) => (
        <MenuItem key={id} value={id}>
          {timelineTitle ?? t(translations.defaultTimeline)}
        </MenuItem>
      )),
    [timelinesMap],
  );

  if (!users?.length) return <Note message={t(translations.noUsers)} />;

  const columns: ColumnTemplate<CourseUserMiniEntity>[] = [
    {
      of: 'name',
      title: t(tableTranslations.name),
      sortable: true,
      searchable: true,
      cell: (user) => <UserNameField for={user} />,
      csvDownloadable: true,
    },
    {
      of: 'email',
      sortable: true,
      searchable: true,
      title: t(tableTranslations.email),
      cell: (user) => user.email,
      csvDownloadable: true,
    },
    {
      of: 'phantom',
      sortable: true,
      title: t(tableTranslations.phantom),
      cell: (user) => <PhantomSwitch for={user} />,
      csvDownloadable: true,
      sortProps: {
        sort: (a, b) => +(a.phantom ?? 0) - +(b.phantom ?? 0),
      },
    },
    {
      of: 'groups',
      sortable: true,
      title: t(tableTranslations.groups),
      filterable: true,
      filterProps: {
        getValue: (user) => user.groups ?? [],
        shouldInclude: (user, filterValue?: string[]): boolean => {
          if (!user.groups) return false;
          if (!filterValue?.length) return true;

          const filterSet = new Set(filterValue);
          return user.groups.some((group) => filterSet.has(group));
        },
      },
      cell: (user) => (
        <ul className="m-0 list-none p-0">
          {user.groups?.map((group) => (
            <Typography key={group} component="li" variant="body2">
              {group}
            </Typography>
          ))}
        </ul>
      ),
      unless: manageStaff,
      csvDownloadable: true,
      csvValue: (value?: number[]) => value?.join('; ') ?? '',
      sortProps: {
        sort: (a, b) =>
          a.groups?.join(';')?.localeCompare(b.groups?.join(';') ?? '') ?? 0,
      },
    },
    {
      of: 'referenceTimelineId',
      sortable: true,
      title: t(tableTranslations.referenceTimeline),
      filterable: true,
      filterProps: {
        beforeFilter: (value: string) => parseInt(value, 10),
        shouldInclude: (user, filterValue?: number | null) =>
          user.referenceTimelineId === filterValue,
        getLabel: (value?: number | null) =>
          (timelinesMap && value && timelinesMap[value]) ||
          t(translations.defaultTimeline),
      },
      cell: (user) => (
        <TimelineMenu
          for={user}
          timelines={timelines!}
          timelinesMap={timelinesMap!}
        />
      ),
      unless:
        !permissions?.canManageReferenceTimelines ||
        !timelines ||
        !timelinesMap,
      csvDownloadable: true,
      csvValue: (value?: number): string => {
        let title = t(translations.defaultTimeline);
        if (timelinesMap && value) title = timelinesMap[value] || title;
        return title;
      },
    },
    {
      of: 'timelineAlgorithm',
      sortable: true,
      title: t(tableTranslations.timelineAlgorithm),
      cell: (user) => <AlgorithmMenu for={user} />,
      unless: !permissions?.canManagePersonalTimes,
      csvDownloadable: true,
    },
    {
      of: 'role',
      sortable: true,
      title: t(tableTranslations.role),
      cell: (user) => <RoleMenu for={user} />,
      unless: !manageStaff || !permissions?.canManageCourseUsers,
      csvDownloadable: true,
      csvValue: (value: CourseUserRoles) => COURSE_USER_ROLES[value],
    },
    {
      id: 'actions',
      title: t(tableTranslations.actions),
      cell: (user) => renderRowActionComponent?.(user),
      unless: !renderRowActionComponent,
    },
  ];

  return (
    <Table
      className={`border-none ${props.className ?? ''}`}
      columns={columns}
      csvDownload={{ filename: props.csvDownloadFilename }}
      data={users}
      getRowClassName={(user): string => `course_user course_user_${user.id}`}
      getRowEqualityData={(user): CourseUserMiniEntity => user}
      getRowId={(user): string => user.id.toString()}
      indexing={{ indices: true, rowSelectable: !manageStaff }}
      pagination={{
        rowsPerPage: [DEFAULT_TABLE_ROWS_PER_PAGE],
        showAllRows: true,
      }}
      search={{
        searchPlaceholder: t(translations.searchText),
        searchProps: {
          shouldInclude: (user, filterValue?: string): boolean => {
            if (!user.name && !user.email) return false;
            if (!filterValue?.length) return true;

            return (
              user.name
                .toLowerCase()
                .trim()
                .includes(filterValue.toLowerCase().trim()) ||
              user.email
                .toLowerCase()
                .trim()
                .includes(filterValue.toLowerCase().trim())
            );
          },
        },
      }}
      toolbar={{
        show: true,
        activeToolbar: (selectedUsers): JSX.Element => (
          <ActiveTableToolbar
            selectedRows={selectedUsers}
            timelinesMap={timelinesMap}
          />
        ),
      }}
    />
  );
};

export default ManageUsersTable;