From 5d70c629ea0e22f9fd492b6d9003e1fb9c01894c Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sun, 12 Dec 2021 23:03:28 +0300 Subject: [PATCH] refactor: students table list --- .../AddStudentsModal/AddSudentsModal.tsx | 41 +++++----- src/hooks/queries/course/useAttachStudents.ts | 28 +++++++ src/hooks/queries/course/useDetachStudents.ts | 29 +++++++ src/hooks/queries/index.ts | 4 + .../StudentTableList/BulkActionsMenu.tsx | 51 ++++++------ .../StudentTableList/StudentTableList.tsx | 79 +++++++------------ src/stores/pages/EditCourseStore.ts | 2 + 7 files changed, 139 insertions(+), 95 deletions(-) create mode 100644 src/hooks/queries/course/useAttachStudents.ts create mode 100644 src/hooks/queries/course/useDetachStudents.ts diff --git a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx index f7e56ed..1a24ceb 100644 --- a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx +++ b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx @@ -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; @@ -24,26 +30,19 @@ const AddStudentsModal: React.FC = ({ currentStudents, course, }) => { - const [isLoading, setIsLoading] = useState(false); - const isMountedRef = useIsMountedRef(); - const [students, setStudents] = useState>([]); - const toast = useToast(); + const { attachStudents, isLoading } = useAttachStudents(); + const [selected, setSelected] = useState>([]); - const handleSelectStudents = (students: Array) => setStudents(students); + const handleSelectStudents = (students: Array) => 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); } } }; diff --git a/src/hooks/queries/course/useAttachStudents.ts b/src/hooks/queries/course/useAttachStudents.ts new file mode 100644 index 0000000..280b96c --- /dev/null +++ b/src/hooks/queries/course/useAttachStudents.ts @@ -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; + +const useAttachStudents = () => { + const toast = useToast(); + const handleError = useErrorHandler(); + + const attachStudents = async (courseId: string, ids: Array) => { + 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>(attachStudents); + + return { attachStudents: execute, ...state }; +}; + +export { useAttachStudents }; diff --git a/src/hooks/queries/course/useDetachStudents.ts b/src/hooks/queries/course/useDetachStudents.ts new file mode 100644 index 0000000..6cf3876 --- /dev/null +++ b/src/hooks/queries/course/useDetachStudents.ts @@ -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; + +const useDetachStudents = () => { + const toast = useToast(); + const handleError = useErrorHandler(); + + const detachStudents = async (courseId: string, ids: Array) => { + 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>(detachStudents); + + return { detachStudents: execute, ...state }; +}; + +export { useDetachStudents }; diff --git a/src/hooks/queries/index.ts b/src/hooks/queries/index.ts index 937aeae..e2fefcb 100644 --- a/src/hooks/queries/index.ts +++ b/src/hooks/queries/index.ts @@ -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, diff --git a/src/pages/courses/edit/components/StudentTableList/BulkActionsMenu.tsx b/src/pages/courses/edit/components/StudentTableList/BulkActionsMenu.tsx index 28fefe1..13cd6ac 100644 --- a/src/pages/courses/edit/components/StudentTableList/BulkActionsMenu.tsx +++ b/src/pages/courses/edit/components/StudentTableList/BulkActionsMenu.tsx @@ -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>; - setIsLoading: React.Dispatch>; onRemoved: (removed: ICourse['students']) => void; }; -const BulkActionsMenu: React.FC = ({ - courseId, - selected, - rows, - setRows, - setIsLoading, - onRemoved, -}) => { - const toast = useToast(); +const BulkActionsMenu: React.FC = ({ 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' }); - setRows(rows.filter(r => selected.every(s => s.id !== r.id))); - onRemoved(selected); - } catch (error) { - console.error(error); - } finally { - setIsLoading(false); - } + const ids = selected.map(s => s.id); + await detachStudents(courseId, ids); + setRows(rows.filter(r => selected.every(s => s.id !== r.id))); + onRemoved(selected); }; + + // TODO bulk delete const handleDelete = async () => undefined; + + // TODO bulk export const handleExport = async () => undefined; return ( - }> + } + > Bulk Actions diff --git a/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx index 1a380d7..4039a54 100644 --- a/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx +++ b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx @@ -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; @@ -29,7 +30,6 @@ const StudentTableList: React.FC = props => { const { render: Row, course } = props; const { students } = course; - const [isLoading, setIsLoading] = useState(false); const [rows, setRows] = useState(students); const [selected, setSelected] = useState([]); const [search, setSearch] = useState(''); @@ -38,7 +38,12 @@ const StudentTableList: React.FC = 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 = 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 = 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 = 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 = 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 ( - <> - setRows(prev => [...prev, ...users])} - /> - - There are no students in the course yet - - - - ); - } - return ( = props => { - {isLoading && ( - - - Loading... - - - - )} - {selected.length !== 0 && ( + {!isEmptySelected && ( <> You have selected {selected.length} student(s). @@ -180,8 +157,7 @@ const StudentTableList: React.FC = 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 = props => { s.id === student.id)} + isSelected={isRowSelected(student)} onSelect={handleSelect} actions={{ onDelete: handleDelete, @@ -206,8 +182,13 @@ const StudentTableList: React.FC = props => { ))} + {isEmptyRows && ( + + Cannot find any user. + + )} - {students.length} student(s) + {rows.length} student(s) ); diff --git a/src/stores/pages/EditCourseStore.ts b/src/stores/pages/EditCourseStore.ts index 4845188..8aa2bde 100644 --- a/src/stores/pages/EditCourseStore.ts +++ b/src/stores/pages/EditCourseStore.ts @@ -32,6 +32,8 @@ export default class EditCourseStore { return result; } + // TODO update stor on detaching, attaching, deleting students + /** * Remove lesson from store *