feat: added delete actions on users in table

This commit is contained in:
Sergey Yarkov 2021-12-11 23:30:26 +03:00
parent ef07c9c7be
commit 08d2e0ccde
7 changed files with 123 additions and 24 deletions

View File

@ -3,19 +3,27 @@ import React, { useState } from 'react';
import BaseModal from '@educt/components/Modal'; import BaseModal from '@educt/components/Modal';
import { MdGroup } from 'react-icons/md'; import { MdGroup } from 'react-icons/md';
import { UserRoleEnum } from '@educt/enums'; import { UserRoleEnum } from '@educt/enums';
import SelectUsersInput from '@educt/components/SelectUsersInput'; import SelectStudentsInput from '@educt/components/SelectStudentsInput';
import { IUser } from '@educt/interfaces'; import { ICourse, IUser } from '@educt/interfaces';
import { CourseServiceInstance } from '@educt/services'; import { CourseServiceInstance } from '@educt/services';
import { useToast } from '@chakra-ui/react'; import { useToast } from '@chakra-ui/react';
import useIsMountedRef from '@educt/hooks/useIsMountedRef'; import useIsMountedRef from '@educt/hooks/useIsMountedRef';
type AddStudentsModalPropsType = { type AddStudentsModalPropsType = {
course: Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>;
currentStudents: ICourse['students'];
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
courseId: string; onAdded: (users: Array<IUser>) => void;
}; };
const AddStudentsModal: React.FC<AddStudentsModalPropsType> = ({ isOpen, onClose, courseId }) => { const AddStudentsModal: React.FC<AddStudentsModalPropsType> = ({
isOpen,
onClose,
onAdded,
currentStudents,
course,
}) => {
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const isMountedRef = useIsMountedRef(); const isMountedRef = useIsMountedRef();
const [students, setStudents] = useState<Array<IUser>>([]); const [students, setStudents] = useState<Array<IUser>>([]);
@ -27,9 +35,10 @@ const AddStudentsModal: React.FC<AddStudentsModalPropsType> = ({ isOpen, onClose
try { try {
setIsLoading(true); setIsLoading(true);
const ids = students.map(s => s.id); const ids = students.map(s => s.id);
await CourseServiceInstance.attachStudentsList(courseId, ids); await CourseServiceInstance.attachStudentsList(course.id, ids);
toast({ title: 'Students successfully added.', status: 'success' }); toast({ title: 'Students added.', status: 'info' });
onAdded(students);
onClose(); onClose();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -51,7 +60,8 @@ const AddStudentsModal: React.FC<AddStudentsModalPropsType> = ({ isOpen, onClose
isLoading={isLoading} isLoading={isLoading}
loadingText='Saving...' loadingText='Saving...'
> >
<SelectUsersInput <SelectStudentsInput
currentStudents={currentStudents}
onSelect={selected => handleSelectStudents(selected)} onSelect={selected => handleSelectStudents(selected)}
placeholder='Search for students...' placeholder='Search for students...'
searchParams={{ role: UserRoleEnum.STUDENT }} searchParams={{ role: UserRoleEnum.STUDENT }}

View File

@ -16,7 +16,7 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { AddIcon, SearchIcon } from '@chakra-ui/icons'; import { AddIcon, SearchIcon } from '@chakra-ui/icons';
import { UserServiceInstance } from '@educt/services'; import { UserServiceInstance } from '@educt/services';
import { IUser } from '@educt/interfaces'; import { ICourse, IUser } from '@educt/interfaces';
import { useErrorHandler } from 'react-error-boundary'; import { useErrorHandler } from 'react-error-boundary';
import { BeatLoader } from 'react-spinners'; import { BeatLoader } from 'react-spinners';
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso';
@ -26,14 +26,22 @@ type SelectUsersInputPropsType = {
placeholder?: string | undefined; placeholder?: string | undefined;
searchParams?: FetchUsersParamsType | undefined; searchParams?: FetchUsersParamsType | undefined;
onSelect: (selected: Array<IUser>) => void; onSelect: (selected: Array<IUser>) => void;
currentStudents: ICourse['students'];
}; };
const SelectUsersInput: React.FC<SelectUsersInputPropsType> = ({ placeholder, searchParams: params, onSelect }) => { const SelectUsersInput: React.FC<SelectUsersInputPropsType> = ({
placeholder,
searchParams: params,
onSelect,
currentStudents,
}) => {
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [search, setSearch] = useState<string>(''); const [search, setSearch] = useState<string>('');
const [selected, setSelected] = useState<Array<IUser>>([]); const [selected, setSelected] = useState<Array<IUser>>([]);
const [users, setUsers] = useState<Array<IUser>>([]); const [users, setUsers] = useState<Array<IUser>>([]);
const filtered = users.filter(u => selected.every(s => s.id !== u.id)); const filtered = users
.filter(u => selected.every(s => s.id !== u.id))
.filter(u => currentStudents.every(s => s.id !== u.id));
const handleError = useErrorHandler(); const handleError = useErrorHandler();
const isEmpty = filtered.length === 0; const isEmpty = filtered.length === 0;

View File

@ -0,0 +1,3 @@
import SelectStudentsInput from './SelectStudentsInput';
export default SelectStudentsInput;

View File

@ -1,3 +0,0 @@
import SelectUsersInput from './SelectUsersInput';
export default SelectUsersInput;

View File

@ -51,15 +51,23 @@ export const RegisteredCell: React.FC<RegisteredCellPropsType> = memo(({ registe
<Td>{new Date(registered).toLocaleDateString()}</Td> <Td>{new Date(registered).toLocaleDateString()}</Td>
)); ));
type ActionsCellPropsType = Pick<IUser, 'id'>; type ActionsCellPropsType = {
export const ActionsCell: React.FC<ActionsCellPropsType> = memo(({ id }) => ( onEdit: () => void;
onRemove: () => void;
onDelete: () => void;
};
export const ActionsCell: React.FC<ActionsCellPropsType> = memo(({ onEdit, onRemove, onDelete }) => (
<Td textAlign='center'> <Td textAlign='center'>
<Menu isLazy> <Menu isLazy>
<MenuButton as={IconButton} aria-label='Actions' icon={<MdMoreHoriz />} variant='ghost' /> <MenuButton as={IconButton} aria-label='Actions' icon={<MdMoreHoriz />} variant='ghost' />
<MenuList> <MenuList>
<MenuItem icon={<MdEdit />}>Edit</MenuItem> <MenuItem onClick={onEdit} icon={<MdEdit />}>
<MenuItem icon={<MdRemove />}>Remove from course</MenuItem> Edit
<MenuItem icon={<DeleteIcon />} color='red.400'> </MenuItem>
<MenuItem onClick={onRemove} icon={<MdRemove />}>
Remove from course
</MenuItem>
<MenuItem onClick={onDelete} icon={<DeleteIcon />} color='red.400'>
Delete Delete
</MenuItem> </MenuItem>
</MenuList> </MenuList>

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { Table, Tbody, Button, Input, InputGroup, InputLeftElement, Flex, Box, Text } from '@chakra-ui/react'; import { Table, Tbody, Button, Input, InputGroup, InputLeftElement, Flex, Box, Text, useToast } 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';
@ -18,6 +18,7 @@ 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 { UserServiceInstance } from '@educt/services';
type StudentTableListPropsType = { type StudentTableListPropsType = {
render: React.FC<StudentTableRowPropsType>; render: React.FC<StudentTableRowPropsType>;
@ -28,6 +29,7 @@ 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>('');
@ -36,6 +38,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
onOpen: onOpenAddStudentModal, onOpen: onOpenAddStudentModal,
onClose: onCloseAddStudentModal, onClose: onCloseAddStudentModal,
} = useDisclosure(); } = useDisclosure();
const toast = useToast();
/** /**
* Search for a student by input * Search for a student by input
@ -73,10 +76,52 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
setSelected(prev => prev.filter(s => s.id !== student.id)); setSelected(prev => prev.filter(s => s.id !== student.id));
}, []); }, []);
/**
* Delete student handler
*/
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));
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
};
/**
* Edit student handler
*/
const handleEdit = (id: string) => {
return () => {
console.log(`editing id: ${id}`);
};
};
/**
* Remove from course student handler
*/
const handleRemove = (id: string) => {
return () => {
console.log(`removing id: ${id}`);
};
};
if (rows.length === 0) { if (rows.length === 0) {
return ( return (
<> <>
<AddStudentsModal isOpen={isOpenAddStudentModal} onClose={onCloseAddStudentModal} courseId={course.id} /> <AddStudentsModal
isOpen={isOpenAddStudentModal}
onClose={onCloseAddStudentModal}
course={course}
currentStudents={rows}
onAdded={users => setRows(prev => [...prev, ...users])}
/>
<Box textAlign='center' mt='10'> <Box textAlign='center' mt='10'>
<Text>There are no students in the course yet</Text> <Text>There are no students in the course yet</Text>
<Button mt='2' onClick={onOpenAddStudentModal} size='sm' colorScheme='blue' variant='outline'> <Button mt='2' onClick={onOpenAddStudentModal} size='sm' colorScheme='blue' variant='outline'>
@ -89,7 +134,13 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
return ( return (
<Box> <Box>
<AddStudentsModal isOpen={isOpenAddStudentModal} onClose={onCloseAddStudentModal} courseId={course.id} /> <AddStudentsModal
isOpen={isOpenAddStudentModal}
onClose={onCloseAddStudentModal}
course={course}
currentStudents={rows}
onAdded={users => setRows(prev => [...prev, ...users])}
/>
<Flex justifyContent='space-between' flexDir={{ base: 'column', lg: 'row' }}> <Flex justifyContent='space-between' flexDir={{ base: 'column', lg: 'row' }}>
<Flex mb='2'> <Flex mb='2'>
<InputGroup mr='2' borderRadius='lg'> <InputGroup mr='2' borderRadius='lg'>
@ -112,6 +163,14 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
<AddButton onClick={onOpenAddStudentModal} /> <AddButton onClick={onOpenAddStudentModal} />
</Flex> </Flex>
</Flex> </Flex>
{isLoading && (
<Flex alignItems='center' justifyContent={'flex-end'}>
<BeatLoader size={6} color='gray' />
<Text fontSize='sm' ml='3' mt='2' mb='2'>
Loading...
</Text>
</Flex>
)}
<Table overflow='hidden' borderRadius='lg' mt='2'> <Table overflow='hidden' borderRadius='lg' mt='2'>
<StudentTableHead onSelectAll={handleSelectAll} /> <StudentTableHead onSelectAll={handleSelectAll} />
<Tbody> <Tbody>
@ -121,6 +180,11 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
student={student} student={student}
isSelected={!!selected.find(s => s.id === student.id)} isSelected={!!selected.find(s => s.id === student.id)}
onSelect={handleSelect} onSelect={handleSelect}
actions={{
onDelete: handleDelete,
onEdit: handleEdit,
onRemove: handleRemove,
}}
/> />
))} ))}
</Tbody> </Tbody>

View File

@ -11,15 +11,24 @@ export type StudentTableRowPropsType = {
isSelected: boolean; isSelected: boolean;
student: ICourse['students'][number]; student: ICourse['students'][number];
onSelect: (e: React.ChangeEvent<HTMLInputElement>, student: ICourse['students'][number]) => void; onSelect: (e: React.ChangeEvent<HTMLInputElement>, student: ICourse['students'][number]) => void;
actions: {
onEdit: (id: string) => () => void;
onRemove: (id: string) => () => void;
onDelete: (id: string) => () => void;
};
}; };
const StudentTableItem: React.FC<StudentTableRowPropsType> = ({ student, isSelected, onSelect }) => { const StudentTableItem: React.FC<StudentTableRowPropsType> = ({ student, isSelected, onSelect, actions }) => {
return ( return (
<Tr> <Tr>
<CheckBoxCell student={student} isSelected={isSelected} onSelect={onSelect} /> <CheckBoxCell student={student} isSelected={isSelected} onSelect={onSelect} />
<InfoCell id={student.id} fullname={student.fullname} email={student.email} /> <InfoCell id={student.id} fullname={student.fullname} email={student.email} />
<RegisteredCell registered={student.created_at} /> <RegisteredCell registered={student.created_at} />
<ActionsCell id={student.id} /> <ActionsCell
onEdit={actions.onEdit(student.id)}
onRemove={actions.onRemove(student.id)}
onDelete={actions.onDelete(student.id)}
/>
</Tr> </Tr>
); );
}; };