mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 18:29:15 +03:00
refactor: students table list
This commit is contained in:
parent
ac7a8a0872
commit
5d70c629ea
@ -1,13 +1,19 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import BaseModal from '@educt/components/Modal';
|
|
||||||
import { MdGroup } from 'react-icons/md';
|
import { MdGroup } from 'react-icons/md';
|
||||||
import { UserRoleEnum } from '@educt/enums';
|
import BaseModal from '@educt/components/Modal';
|
||||||
import SelectStudentsInput from '@educt/components/SelectStudentsInput';
|
import SelectStudentsInput from '@educt/components/SelectStudentsInput';
|
||||||
|
import { UserRoleEnum } from '@educt/enums';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types
|
||||||
|
*/
|
||||||
import { ICourse, IUser } from '@educt/interfaces';
|
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 = {
|
type AddStudentsModalPropsType = {
|
||||||
course: Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>;
|
course: Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>;
|
||||||
@ -24,26 +30,19 @@ const AddStudentsModal: React.FC<AddStudentsModalPropsType> = ({
|
|||||||
currentStudents,
|
currentStudents,
|
||||||
course,
|
course,
|
||||||
}) => {
|
}) => {
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const { attachStudents, isLoading } = useAttachStudents();
|
||||||
const isMountedRef = useIsMountedRef();
|
const [selected, setSelected] = useState<Array<IUser>>([]);
|
||||||
const [students, setStudents] = useState<Array<IUser>>([]);
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const handleSelectStudents = (students: Array<IUser>) => setStudents(students);
|
const handleSelectStudents = (students: Array<IUser>) => setSelected(students);
|
||||||
const handleAttachStudents = async () => {
|
const handleAttachStudents = async () => {
|
||||||
if (students.length !== 0) {
|
if (selected.length !== 0) {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
const ids = selected.map(s => s.id);
|
||||||
const ids = students.map(s => s.id);
|
await attachStudents(course.id, ids);
|
||||||
await CourseServiceInstance.attachStudentsList(course.id, ids);
|
onAdded(selected);
|
||||||
|
|
||||||
toast({ title: 'Students added.', status: 'info' });
|
|
||||||
onAdded(students);
|
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
|
||||||
if (isMountedRef.current) setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
28
src/hooks/queries/course/useAttachStudents.ts
Normal file
28
src/hooks/queries/course/useAttachStudents.ts
Normal 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 };
|
||||||
29
src/hooks/queries/course/useDetachStudents.ts
Normal file
29
src/hooks/queries/course/useDetachStudents.ts
Normal 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 };
|
||||||
@ -16,6 +16,8 @@ import { useCreateCourse } from './course/useCreateCourse';
|
|||||||
import { useUpdateCourse } from './course/useUpdateCourse';
|
import { useUpdateCourse } from './course/useUpdateCourse';
|
||||||
import { useFetchCourse } from './course/useFetchCourse';
|
import { useFetchCourse } from './course/useFetchCourse';
|
||||||
import { useSetCourseStatus } from './course/useSetCourseStatus';
|
import { useSetCourseStatus } from './course/useSetCourseStatus';
|
||||||
|
import { useAttachStudents } from './course/useAttachStudents';
|
||||||
|
import { useDetachStudents } from './course/useDetachStudents';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User
|
* User
|
||||||
@ -32,6 +34,8 @@ export {
|
|||||||
useUpdateCourse,
|
useUpdateCourse,
|
||||||
useFetchCourse,
|
useFetchCourse,
|
||||||
useSetCourseStatus,
|
useSetCourseStatus,
|
||||||
|
useAttachStudents,
|
||||||
|
useDetachStudents,
|
||||||
useDeleteLesson,
|
useDeleteLesson,
|
||||||
useCreateUser,
|
useCreateUser,
|
||||||
useDeleteUser,
|
useDeleteUser,
|
||||||
|
|||||||
@ -1,50 +1,51 @@
|
|||||||
import React from 'react';
|
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 { ChevronDownIcon, DeleteIcon } from '@chakra-ui/icons';
|
||||||
import { MdRemove, MdImportExport } from 'react-icons/md';
|
import { MdRemove, MdImportExport } from 'react-icons/md';
|
||||||
|
|
||||||
import type { ICourse } from '@educt/interfaces';
|
import type { ICourse } from '@educt/interfaces';
|
||||||
import { CourseServiceInstance } from '@educt/services';
|
|
||||||
|
/**
|
||||||
|
* Hooks
|
||||||
|
*/
|
||||||
|
import { useDetachStudents } from '@educt/hooks/queries';
|
||||||
|
|
||||||
type BulkActionsMenuPropsType = {
|
type BulkActionsMenuPropsType = {
|
||||||
courseId: string;
|
courseId: string;
|
||||||
selected: ICourse['students'];
|
selected: ICourse['students'];
|
||||||
rows: ICourse['students'];
|
rows: ICourse['students'];
|
||||||
setRows: React.Dispatch<React.SetStateAction<ICourse['students']>>;
|
setRows: React.Dispatch<React.SetStateAction<ICourse['students']>>;
|
||||||
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
onRemoved: (removed: ICourse['students']) => void;
|
onRemoved: (removed: ICourse['students']) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BulkActionsMenu: React.FC<BulkActionsMenuPropsType> = ({
|
const BulkActionsMenu: React.FC<BulkActionsMenuPropsType> = ({ courseId, selected, rows, setRows, onRemoved }) => {
|
||||||
courseId,
|
const { detachStudents, isLoading } = useDetachStudents();
|
||||||
selected,
|
|
||||||
rows,
|
|
||||||
setRows,
|
|
||||||
setIsLoading,
|
|
||||||
onRemoved,
|
|
||||||
}) => {
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const handleDetach = async () => {
|
const handleDetach = async () => {
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
const ids = selected.map(s => s.id);
|
const ids = selected.map(s => s.id);
|
||||||
await CourseServiceInstance.detachStudentsList(courseId, ids);
|
await detachStudents(courseId, ids);
|
||||||
toast({ title: 'Students removed form course.', status: 'info' });
|
|
||||||
setRows(rows.filter(r => selected.every(s => s.id !== r.id)));
|
setRows(rows.filter(r => selected.every(s => s.id !== r.id)));
|
||||||
onRemoved(selected);
|
onRemoved(selected);
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO bulk delete
|
||||||
const handleDelete = async () => undefined;
|
const handleDelete = async () => undefined;
|
||||||
|
|
||||||
|
// TODO bulk export
|
||||||
const handleExport = async () => undefined;
|
const handleExport = async () => undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu isLazy>
|
<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
|
Bulk Actions
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
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 { MdSearch } from 'react-icons/md';
|
||||||
import AddStudentsModal from '@educt/components/Modals/AddStudentsModal';
|
import AddStudentsModal from '@educt/components/Modals/AddStudentsModal';
|
||||||
import { BeatLoader } from 'react-spinners';
|
|
||||||
import { AddButton } from '@educt/components/Buttons';
|
import { AddButton } from '@educt/components/Buttons';
|
||||||
import BulkActionsMenu from './BulkActionsMenu';
|
import BulkActionsMenu from './BulkActionsMenu';
|
||||||
import StudentTableHead from './StudentTableHead';
|
import StudentTableHead from './StudentTableHead';
|
||||||
|
import { AddIcon } from '@chakra-ui/icons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Types
|
* Types
|
||||||
@ -18,7 +18,8 @@ import type { ICourse } from '@educt/interfaces';
|
|||||||
*/
|
*/
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useDisclosure } from '@chakra-ui/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 = {
|
type StudentTableListPropsType = {
|
||||||
render: React.FC<StudentTableRowPropsType>;
|
render: React.FC<StudentTableRowPropsType>;
|
||||||
@ -29,7 +30,6 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
|
|||||||
const { render: Row, course } = props;
|
const { render: Row, course } = props;
|
||||||
const { students } = course;
|
const { students } = course;
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
||||||
const [rows, setRows] = useState<ICourse['students']>(students);
|
const [rows, setRows] = useState<ICourse['students']>(students);
|
||||||
const [selected, setSelected] = useState<ICourse['students']>([]);
|
const [selected, setSelected] = useState<ICourse['students']>([]);
|
||||||
const [search, setSearch] = useState<string>('');
|
const [search, setSearch] = useState<string>('');
|
||||||
@ -38,7 +38,12 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
|
|||||||
onOpen: onOpenAddStudentModal,
|
onOpen: onOpenAddStudentModal,
|
||||||
onClose: onCloseAddStudentModal,
|
onClose: onCloseAddStudentModal,
|
||||||
} = useDisclosure();
|
} = 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
|
* 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
|
* Select all students in a table
|
||||||
*/
|
*/
|
||||||
@ -82,14 +94,10 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
|
|||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
return async () => {
|
return async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
const deleted = await deleteUser(id);
|
||||||
const { data } = await UserServiceInstance.delete(id);
|
setRows(prev => prev.filter(s => s.id !== deleted.id));
|
||||||
toast({ title: 'Student deleted', status: 'info' });
|
|
||||||
setRows(prev => prev.filter(s => s.id !== data.id));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -99,6 +107,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
|
|||||||
*/
|
*/
|
||||||
const handleEdit = (id: string) => {
|
const handleEdit = (id: string) => {
|
||||||
return () => {
|
return () => {
|
||||||
|
// TODO open the edit user modal
|
||||||
console.log(`editing id: ${id}`);
|
console.log(`editing id: ${id}`);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -109,38 +118,14 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
|
|||||||
const handleRemove = (id: string) => {
|
const handleRemove = (id: string) => {
|
||||||
return async () => {
|
return async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
await detachStudents(course.id, [id]);
|
||||||
await CourseServiceInstance.detachStudentsList(course.id, [id]);
|
|
||||||
toast({ title: 'Student removed.', status: 'info' });
|
|
||||||
setRows(prev => prev.filter(s => s.id !== id));
|
setRows(prev => prev.filter(s => s.id !== id));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(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 (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<AddStudentsModal
|
<AddStudentsModal
|
||||||
@ -160,17 +145,9 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
|
|||||||
<Input value={search} size='sm' onChange={handleSearch} type='text' placeholder='Search for student...' />
|
<Input value={search} size='sm' onChange={handleSearch} type='text' placeholder='Search for student...' />
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</Box>
|
</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>
|
||||||
<Flex alignItems='center' justifyContent='space-between' pr={{ base: '8px', lg: '0' }}>
|
<Flex alignItems='center' justifyContent='space-between' pr={{ base: '8px', lg: '0' }}>
|
||||||
{selected.length !== 0 && (
|
{!isEmptySelected && (
|
||||||
<>
|
<>
|
||||||
<Text mr='4'>
|
<Text mr='4'>
|
||||||
You have selected <b>{selected.length}</b> student(s).
|
You have selected <b>{selected.length}</b> student(s).
|
||||||
@ -180,8 +157,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
|
|||||||
selected={selected}
|
selected={selected}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
setRows={setRows}
|
setRows={setRows}
|
||||||
setIsLoading={setIsLoading}
|
onRemoved={onRemoved}
|
||||||
onRemoved={removed => setSelected(prev => prev.filter(s => removed.every(r => s.id !== r.id)))}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -195,7 +171,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
|
|||||||
<Row
|
<Row
|
||||||
key={student.id}
|
key={student.id}
|
||||||
student={student}
|
student={student}
|
||||||
isSelected={!!selected.find(s => s.id === student.id)}
|
isSelected={isRowSelected(student)}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
actions={{
|
actions={{
|
||||||
onDelete: handleDelete,
|
onDelete: handleDelete,
|
||||||
@ -206,8 +182,13 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
|
|||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</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'>
|
<Text mt='4' fontSize='sm' color='gray.500'>
|
||||||
{students.length} student(s)
|
{rows.length} student(s)
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -32,6 +32,8 @@ export default class EditCourseStore {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO update stor on detaching, attaching, deleting students
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove lesson from store
|
* Remove lesson from store
|
||||||
*
|
*
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user