refactor: students table list

This commit is contained in:
Sergey Yarkov 2021-12-12 23:03:28 +03:00
parent ac7a8a0872
commit 5d70c629ea
7 changed files with 139 additions and 95 deletions

View File

@ -1,13 +1,19 @@
import React, { useState } from 'react';
import BaseModal from '@educt/components/Modal';
import React from 'react';
import { MdGroup } from 'react-icons/md';
import { UserRoleEnum } from '@educt/enums';
import BaseModal from '@educt/components/Modal';
import SelectStudentsInput from '@educt/components/SelectStudentsInput';
import { UserRoleEnum } from '@educt/enums';
/**
* Types
*/
import { ICourse, IUser } from '@educt/interfaces';
import { CourseServiceInstance } from '@educt/services';
import { useToast } from '@chakra-ui/react';
import useIsMountedRef from '@educt/hooks/useIsMountedRef';
/**
* Hooks
*/
import { useState } from 'react';
import { useAttachStudents } from '@educt/hooks/queries';
type AddStudentsModalPropsType = {
course: Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>;
@ -24,26 +30,19 @@ const AddStudentsModal: React.FC<AddStudentsModalPropsType> = ({
currentStudents,
course,
}) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const isMountedRef = useIsMountedRef();
const [students, setStudents] = useState<Array<IUser>>([]);
const toast = useToast();
const { attachStudents, isLoading } = useAttachStudents();
const [selected, setSelected] = useState<Array<IUser>>([]);
const handleSelectStudents = (students: Array<IUser>) => setStudents(students);
const handleSelectStudents = (students: Array<IUser>) => setSelected(students);
const handleAttachStudents = async () => {
if (students.length !== 0) {
if (selected.length !== 0) {
try {
setIsLoading(true);
const ids = students.map(s => s.id);
await CourseServiceInstance.attachStudentsList(course.id, ids);
toast({ title: 'Students added.', status: 'info' });
onAdded(students);
const ids = selected.map(s => s.id);
await attachStudents(course.id, ids);
onAdded(selected);
onClose();
} catch (error) {
console.error(error);
} finally {
if (isMountedRef.current) setIsLoading(false);
}
}
};

View File

@ -0,0 +1,28 @@
import { useToast } from '@chakra-ui/toast';
import { useErrorHandler } from 'react-error-boundary';
import useAsync from '@educt/hooks/useAsync';
import { CourseServiceInstance } from '@educt/services';
type AttachedStudentsDataType = Record<string, never>;
const useAttachStudents = () => {
const toast = useToast();
const handleError = useErrorHandler();
const attachStudents = async (courseId: string, ids: Array<string>) => {
try {
const result = await CourseServiceInstance.attachStudentsList(courseId, ids);
toast({ title: 'Student(s) attached to the course.', status: 'info' });
return result.data;
} catch (error) {
handleError(error);
return Promise.reject(error);
}
};
const { execute, ...state } = useAsync<AttachedStudentsDataType, Parameters<typeof attachStudents>>(attachStudents);
return { attachStudents: execute, ...state };
};
export { useAttachStudents };

View File

@ -0,0 +1,29 @@
import { useToast } from '@chakra-ui/toast';
import { useErrorHandler } from 'react-error-boundary';
import useAsync from '@educt/hooks/useAsync';
import { CourseServiceInstance } from '@educt/services';
type DetachedStudentsDataType = Record<string, never>;
const useDetachStudents = () => {
const toast = useToast();
const handleError = useErrorHandler();
const detachStudents = async (courseId: string, ids: Array<string>) => {
try {
const result = await CourseServiceInstance.detachStudentsList(courseId, ids);
toast({ title: 'Student(s) detached from course.', status: 'info' });
return result.data;
} catch (error) {
handleError(error);
return Promise.reject(error);
}
};
const { execute, ...state } = useAsync<DetachedStudentsDataType, Parameters<typeof detachStudents>>(detachStudents);
return { detachStudents: execute, ...state };
};
export { useDetachStudents };

View File

@ -16,6 +16,8 @@ import { useCreateCourse } from './course/useCreateCourse';
import { useUpdateCourse } from './course/useUpdateCourse';
import { useFetchCourse } from './course/useFetchCourse';
import { useSetCourseStatus } from './course/useSetCourseStatus';
import { useAttachStudents } from './course/useAttachStudents';
import { useDetachStudents } from './course/useDetachStudents';
/**
* User
@ -32,6 +34,8 @@ export {
useUpdateCourse,
useFetchCourse,
useSetCourseStatus,
useAttachStudents,
useDetachStudents,
useDeleteLesson,
useCreateUser,
useDeleteUser,

View File

@ -1,50 +1,51 @@
import React from 'react';
import { Button, Menu, MenuButton, MenuList, MenuItem, useToast } from '@chakra-ui/react';
import { Button, Menu, MenuButton, MenuList, MenuItem } from '@chakra-ui/react';
import { ChevronDownIcon, DeleteIcon } from '@chakra-ui/icons';
import { MdRemove, MdImportExport } from 'react-icons/md';
import type { ICourse } from '@educt/interfaces';
import { CourseServiceInstance } from '@educt/services';
/**
* Hooks
*/
import { useDetachStudents } from '@educt/hooks/queries';
type BulkActionsMenuPropsType = {
courseId: string;
selected: ICourse['students'];
rows: ICourse['students'];
setRows: React.Dispatch<React.SetStateAction<ICourse['students']>>;
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
onRemoved: (removed: ICourse['students']) => void;
};
const BulkActionsMenu: React.FC<BulkActionsMenuPropsType> = ({
courseId,
selected,
rows,
setRows,
setIsLoading,
onRemoved,
}) => {
const toast = useToast();
const BulkActionsMenu: React.FC<BulkActionsMenuPropsType> = ({ courseId, selected, rows, setRows, onRemoved }) => {
const { detachStudents, isLoading } = useDetachStudents();
const handleDetach = async () => {
try {
setIsLoading(true);
const ids = selected.map(s => s.id);
await CourseServiceInstance.detachStudentsList(courseId, ids);
toast({ title: 'Students removed form course.', status: 'info' });
await detachStudents(courseId, ids);
setRows(rows.filter(r => selected.every(s => s.id !== r.id)));
onRemoved(selected);
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
// TODO bulk delete
const handleDelete = async () => undefined;
// TODO bulk export
const handleExport = async () => undefined;
return (
<Menu isLazy>
<MenuButton as={Button} size='sm' variant='outline' mr='3' ml='auto' rightIcon={<ChevronDownIcon />}>
<MenuButton
as={Button}
isLoading={isLoading}
loadingText='Loading...'
size='sm'
variant='outline'
mr='3'
ml='auto'
rightIcon={<ChevronDownIcon />}
>
Bulk Actions
</MenuButton>
<MenuList>

View File

@ -1,11 +1,11 @@
import React from 'react';
import { Table, Tbody, Button, Input, InputGroup, InputLeftElement, Flex, Box, Text, useToast } from '@chakra-ui/react';
import { Table, Tbody, Button, Input, InputGroup, InputLeftElement, Flex, Box, Text } from '@chakra-ui/react';
import { MdSearch } from 'react-icons/md';
import AddStudentsModal from '@educt/components/Modals/AddStudentsModal';
import { BeatLoader } from 'react-spinners';
import { AddButton } from '@educt/components/Buttons';
import BulkActionsMenu from './BulkActionsMenu';
import StudentTableHead from './StudentTableHead';
import { AddIcon } from '@chakra-ui/icons';
/**
* Types
@ -18,7 +18,8 @@ import type { ICourse } from '@educt/interfaces';
*/
import { useCallback, useState } from 'react';
import { useDisclosure } from '@chakra-ui/react';
import { CourseServiceInstance, UserServiceInstance } from '@educt/services';
import { useDetachStudents } from '@educt/hooks/queries';
import { useDeleteUser } from '@educt/hooks/queries';
type StudentTableListPropsType = {
render: React.FC<StudentTableRowPropsType>;
@ -29,7 +30,6 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
const { render: Row, course } = props;
const { students } = course;
const [isLoading, setIsLoading] = useState<boolean>(false);
const [rows, setRows] = useState<ICourse['students']>(students);
const [selected, setSelected] = useState<ICourse['students']>([]);
const [search, setSearch] = useState<string>('');
@ -38,7 +38,12 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
onOpen: onOpenAddStudentModal,
onClose: onCloseAddStudentModal,
} = useDisclosure();
const toast = useToast();
const { deleteUser } = useDeleteUser();
const { detachStudents } = useDetachStudents();
const isEmptyRows = rows.length === 0;
const isEmptySelected = selected.length === 0;
const isRowSelected = (student: ICourse['students'][number]) => !!selected.find(s => s.id === student.id);
/**
* Search for a student by input
@ -52,6 +57,13 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
]);
};
/**
* Delete student from state after removing
*/
const onRemoved = (removedStudents: ICourse['students']) => {
setSelected(prev => prev.filter(s => removedStudents.every(r => s.id !== r.id)));
};
/**
* Select all students in a table
*/
@ -82,14 +94,10 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
const handleDelete = (id: string) => {
return async () => {
try {
setIsLoading(true);
const { data } = await UserServiceInstance.delete(id);
toast({ title: 'Student deleted', status: 'info' });
setRows(prev => prev.filter(s => s.id !== data.id));
const deleted = await deleteUser(id);
setRows(prev => prev.filter(s => s.id !== deleted.id));
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
};
@ -99,6 +107,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
*/
const handleEdit = (id: string) => {
return () => {
// TODO open the edit user modal
console.log(`editing id: ${id}`);
};
};
@ -109,38 +118,14 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
const handleRemove = (id: string) => {
return async () => {
try {
setIsLoading(true);
await CourseServiceInstance.detachStudentsList(course.id, [id]);
toast({ title: 'Student removed.', status: 'info' });
await detachStudents(course.id, [id]);
setRows(prev => prev.filter(s => s.id !== id));
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
};
if (rows.length === 0) {
return (
<>
<AddStudentsModal
isOpen={isOpenAddStudentModal}
onClose={onCloseAddStudentModal}
course={course}
currentStudents={rows}
onAdded={users => setRows(prev => [...prev, ...users])}
/>
<Box textAlign='center' mt='10'>
<Text>There are no students in the course yet</Text>
<Button mt='2' onClick={onOpenAddStudentModal} size='sm' colorScheme='blue' variant='outline'>
Add student
</Button>
</Box>
</>
);
}
return (
<Box>
<AddStudentsModal
@ -160,17 +145,9 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
<Input value={search} size='sm' onChange={handleSearch} type='text' placeholder='Search for student...' />
</InputGroup>
</Box>
{isLoading && (
<Flex alignItems='center'>
<Text fontSize='sm' ml='3' mr='3' mt='2' mb='2'>
Loading...
</Text>
<BeatLoader size={4} color='gray' />
</Flex>
)}
</Flex>
<Flex alignItems='center' justifyContent='space-between' pr={{ base: '8px', lg: '0' }}>
{selected.length !== 0 && (
{!isEmptySelected && (
<>
<Text mr='4'>
You have selected <b>{selected.length}</b> student(s).
@ -180,8 +157,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
selected={selected}
rows={rows}
setRows={setRows}
setIsLoading={setIsLoading}
onRemoved={removed => setSelected(prev => prev.filter(s => removed.every(r => s.id !== r.id)))}
onRemoved={onRemoved}
/>
</>
)}
@ -195,7 +171,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
<Row
key={student.id}
student={student}
isSelected={!!selected.find(s => s.id === student.id)}
isSelected={isRowSelected(student)}
onSelect={handleSelect}
actions={{
onDelete: handleDelete,
@ -206,8 +182,13 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
))}
</Tbody>
</Table>
{isEmptyRows && (
<Box textAlign='center' m='6'>
<Text color='gray.500'>Cannot find any user.</Text>
</Box>
)}
<Text mt='4' fontSize='sm' color='gray.500'>
{students.length} student(s)
{rows.length} student(s)
</Text>
</Box>
);

View File

@ -32,6 +32,8 @@ export default class EditCourseStore {
return result;
}
// TODO update stor on detaching, attaching, deleting students
/**
* Remove lesson from store
*