refactor: refactored user list & alert dialog

This commit is contained in:
Sergey Yarkov 2021-11-08 21:57:06 +03:00
parent 479a6291eb
commit 2bcaad519e
16 changed files with 363 additions and 355 deletions

View File

@ -0,0 +1,63 @@
import React, { useState } from 'react';
import {
AlertDialog,
AlertDialogBody,
AlertDialogOverlay,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
Button,
} from '@chakra-ui/react';
import { useRef } from 'react';
export interface IConfirmDialogProps {
title?: string;
message?: string;
confirmMessage?: string;
isOpen: boolean;
isLoading?: boolean;
onClose: (isConfirmed: boolean) => void;
}
const ConfirmDialog: React.FC<IConfirmDialogProps> = ({
title,
message,
confirmMessage,
isOpen,
isLoading,
onClose,
}) => {
const cancelRef = useRef<HTMLButtonElement | null>(null);
return (
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={() => onClose(false)}>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
{title || 'Confirm that action'}
</AlertDialogHeader>
<AlertDialogBody>{message || 'Would you like to perform this action?'}</AlertDialogBody>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={() => onClose(false)}>
Cancel
</Button>
<Button
isLoading={isLoading}
loadingText='Loading...'
colorScheme='red'
onClick={() => onClose(true)}
ml={3}
>
{confirmMessage || 'Confirm'}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
);
};
export default ConfirmDialog;

View File

@ -14,8 +14,8 @@ export const ChangeEmailPageContext = createContext<ChangeEmailPageContextType>(
export const UsersPageContext = createContext<UsersPageContextType>({ export const UsersPageContext = createContext<UsersPageContextType>({
searchingRole: undefined, searchingRole: undefined,
setSearchingRole: () => {}, setSearchingRole: () => {},
loading: false, searchingPage: 1,
setLoading: () => {}, setSearchingPage: () => {},
search: undefined, search: undefined,
setSearch: () => {}, setSearch: () => {},
editingUser: undefined, editingUser: undefined,

View File

@ -92,6 +92,7 @@ const CourseItem: React.FC<CourseItemPropsType> = ({ course, onDelete, onSetStat
to={`/course/${course.id}`} to={`/course/${course.id}`}
as={ReactRouterLink} as={ReactRouterLink}
display='block' display='block'
h='full'
textDecoration='none' textDecoration='none'
_visited={{ textDecoration: 'none' }} _visited={{ textDecoration: 'none' }}
_hover={{ textDecoration: 'none' }} _hover={{ textDecoration: 'none' }}

View File

@ -56,15 +56,6 @@ const CourseList: React.FC<CourseListPropsType> = ({ render: Item }) => {
} }
}, [courseStore, selectedCategory, courseStatus]); }, [courseStore, selectedCategory, courseStatus]);
/**
* Handle delete course
* Set deleting course to context and open dialog
*/
const onDeleteCourse = (course: Pick<ICourse, 'id' | 'title'>) => {
setDeletingCourse(course);
onOpenDeleteDialog();
};
/** /**
* Handle update course status * Handle update course status
*/ */
@ -81,29 +72,38 @@ const CourseList: React.FC<CourseListPropsType> = ({ render: Item }) => {
} }
}; };
/**
* Loading
*/
if (courses === null || isLoading) return <LoadingList />; if (courses === null || isLoading) return <LoadingList />;
/**
* No courses
*/
if (courses.length === 0)
return (
<Box textAlign='center' mt='10' userSelect='none'>
<Text color='gray.500'>There are no courses in this category {courseStatus && 'or with this status'}.</Text>
</Box>
);
return ( return (
<Box> <Box>
{deletingCourse && <DeleteCourseDialog onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} />} {deletingCourse && (
<DeleteCourseDialog course={deletingCourse} onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} />
)}
{courses.length !== 0 ? (
<Grid templateColumns={{ sm: 'repeat(auto-fill, minmax(400px, 1fr))' }} gap='4'> <Grid templateColumns={{ sm: 'repeat(auto-fill, minmax(400px, 1fr))' }} gap='4'>
{courses.map(course => ( {courses.map(course => (
<Item key={course.id} course={course} onDelete={onDeleteCourse} onSetStatus={onSetStatus} /> <Item
key={course.id}
course={course}
onDelete={() => {
setDeletingCourse(course);
onOpenDeleteDialog();
}}
onSetStatus={onSetStatus}
/>
))} ))}
</Grid> </Grid>
) : (
<Box textAlign='center' mt='10' userSelect='none'>
<Text color='gray.500'>
There are no courses&nbsp;
{!selectedCategory && !courseStatus
? 'in the system yet'
: `in this category${courseStatus ? ' or with this status' : ''}`}
.
</Text>
</Box>
)}
</Box> </Box>
); );
}; };

View File

@ -10,6 +10,11 @@ import {
useToast, useToast,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
/**
* Components
*/
import ConfirmDialog from '@educt/components/ConfirmDialog/ConfirmDialog';
/** /**
* Contexts * Contexts
*/ */
@ -18,36 +23,35 @@ import { CoursesPageContext } from '@educt/contexts';
/** /**
* Hooks * Hooks
*/ */
import useIsMountedRef from '@educt/hooks/useIsMountedRef';
import { useErrorHandler } from 'react-error-boundary'; import { useErrorHandler } from 'react-error-boundary';
import { useRootStore } from '@educt/hooks/useRootStore'; import { useRootStore } from '@educt/hooks/useRootStore';
import { ICourse } from '@educt/interfaces';
type DeleteCourseDialogProps = { type DeleteCourseDialogPropsType = {
course: Pick<ICourse, 'id' | 'title'>;
onClose: () => void; onClose: () => void;
isOpen: boolean; isOpen: boolean;
}; };
const DeleteCourseDialog: React.FC<DeleteCourseDialogProps> = ({ onClose, isOpen }) => { const DeleteCourseDialog: React.FC<DeleteCourseDialogPropsType> = ({ onClose, isOpen, course }) => {
const { courseStore } = useRootStore(); const { courseStore } = useRootStore();
const { deletingCourse, setDeletingCourse } = useContext(CoursesPageContext); const { setDeletingCourse } = useContext(CoursesPageContext);
const isMountedRef = useIsMountedRef();
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const cancelRef = useRef<HTMLButtonElement | null>(null);
const handleError = useErrorHandler(); const handleError = useErrorHandler();
const toast = useToast(); const toast = useToast();
/** /**
* Delete course handler * Delete course handler
* *
* @param isConfirmed Is dialog confirmed
* @param id Course id * @param id Course id
*/ */
const onDelete = async (id: string) => { const deleteCourseHandler = async (isConfirmed: boolean, id: string) => {
if (isConfirmed) {
try { try {
setIsLoading(true); setIsLoading(true);
await courseStore.deleteCourse(id); await courseStore.deleteCourse(id);
toast({ title: 'Course deleted.', status: 'info' }); toast({ title: 'Course deleted.', status: 'info' });
onClose();
setDeletingCourse(undefined);
} catch (error: any) { } catch (error: any) {
if (error.response) { if (error.response) {
toast({ title: error.message, status: 'error' }); toast({ title: error.message, status: 'error' });
@ -55,50 +59,26 @@ const DeleteCourseDialog: React.FC<DeleteCourseDialogProps> = ({ onClose, isOpen
handleError(error); handleError(error);
} }
} finally { } finally {
if (isMountedRef.current) {
setIsLoading(false); setIsLoading(false);
setDeletingCourse(undefined);
} }
} }
/**
* Close dialog
*/
onClose();
}; };
if (deletingCourse === undefined) return null;
return ( return (
<> <ConfirmDialog
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}> isOpen={isOpen}
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
Confirm deleting
</AlertDialogHeader>
<AlertDialogBody>
The course with title "{deletingCourse.title}" will be removed from the system. After deleting a course,
the lessons in it will also be deleted.
</AlertDialogBody>
<AlertDialogFooter>
<Button
ref={cancelRef}
onClick={() => {
setDeletingCourse(undefined);
onClose();
}}
>
Cancel
</Button>
<Button
colorScheme='red'
isLoading={isLoading} isLoading={isLoading}
loadingText='Deleting...' onClose={isConfirmed => deleteCourseHandler(isConfirmed, course.id)}
onClick={() => onDelete(deletingCourse.id)} title='Confirm deleting'
ml={3} message={`The course with title "${course.title}" will be removed from the system. After deleting a course, the lessons in it will also be deleted.`}
> confirmMessage='Delete'
Delete />
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
</>
); );
}; };

View File

@ -45,9 +45,9 @@ type CreateCourseInputType = {
description: string; description: string;
teacher_id: string; teacher_id: string;
}; };
type CreateCourseFormProps = {}; type CreateCourseFormPropsType = {};
const CreateCourseForm: React.FC<CreateCourseFormProps> = () => { const CreateCourseForm: React.FC<CreateCourseFormPropsType> = () => {
const { const {
userStore: { userService }, userStore: { userService },
categoryStore: { categoryService }, categoryStore: { categoryService },
@ -58,8 +58,6 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
control, control,
watch,
reset,
} = useForm<CreateCourseInputType>({ } = useForm<CreateCourseInputType>({
resolver: yupResolver(CreateCourseSchema), resolver: yupResolver(CreateCourseSchema),
}); });
@ -205,6 +203,7 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
{errors.category_id?.message} {errors.category_id?.message}
</Text> </Text>
</FormControl> </FormControl>
<FormControl <FormControl
isRequired isRequired
id='teacher_id' id='teacher_id'
@ -231,6 +230,7 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
{errors.teacher_id?.message} {errors.teacher_id?.message}
</Text> </Text>
</FormControl> </FormControl>
<FormControl isRequired id='description' gridColumn='1' isInvalid={!!errors.description}> <FormControl isRequired id='description' gridColumn='1' isInvalid={!!errors.description}>
<FormLabel>Description</FormLabel> <FormLabel>Description</FormLabel>
<Textarea <Textarea

View File

@ -1,4 +1,4 @@
import React, { useContext, useState } from 'react'; import React from 'react';
import { import {
Modal, Modal,
ModalOverlay, ModalOverlay,
@ -21,18 +21,17 @@ import {
import { AddIcon } from '@chakra-ui/icons'; import { AddIcon } from '@chakra-ui/icons';
import { MdSupervisorAccount } from 'react-icons/md'; import { MdSupervisorAccount } from 'react-icons/md';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { SubmitHandler } from 'react-hook-form';
import CreateUserSchema from './CreateUserModal.validator';
/** /**
* Types * Types
*/ */
import { IMe } from '@educt/interfaces';
import { UserRoleEnum } from '@educt/enums'; import { UserRoleEnum } from '@educt/enums';
import { SubmitHandler } from 'react-hook-form';
/** /**
* Hooks * Hooks
*/ */
import { useContext, useState } from 'react';
import useIsMountedRef from '@educt/hooks/useIsMountedRef'; import useIsMountedRef from '@educt/hooks/useIsMountedRef';
import { useDisclosure } from '@chakra-ui/hooks'; import { useDisclosure } from '@chakra-ui/hooks';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
@ -45,9 +44,12 @@ import { useToast } from '@chakra-ui/react';
*/ */
import { UsersPageContext } from '@educt/contexts'; import { UsersPageContext } from '@educt/contexts';
type CreateUserModalPropsType = { /**
me: IMe; * Schema
}; */
import CreateUserSchema from './CreateUserForm.validator';
type CreateUserFormPropsType = {};
type CreateUserInputType = { type CreateUserInputType = {
first_name: string; first_name: string;
@ -58,7 +60,7 @@ type CreateUserInputType = {
password: string; password: string;
}; };
const CreateUserModal: React.FC<CreateUserModalPropsType> = ({ me }) => { const CreateUserModal: React.FC<CreateUserFormPropsType> = () => {
const { userStore } = useRootStore(); const { userStore } = useRootStore();
const { searchingRole, search } = useContext(UsersPageContext); const { searchingRole, search } = useContext(UsersPageContext);
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
@ -75,8 +77,12 @@ const CreateUserModal: React.FC<CreateUserModalPropsType> = ({ me }) => {
const toast = useToast(); const toast = useToast();
const handleError = useErrorHandler(); const handleError = useErrorHandler();
const { me } = userStore;
if (me === null) return null;
/** /**
* Create new user handler * Submit handler
*/ */
const onSubmit: SubmitHandler<CreateUserInputType> = async data => { const onSubmit: SubmitHandler<CreateUserInputType> = async data => {
try { try {

View File

@ -1,52 +1,60 @@
import React, { useContext, useRef, useState } from 'react'; import React from 'react';
import {
AlertDialog, /**
AlertDialogBody, * Types
AlertDialogFooter, */
AlertDialogHeader, import { IUser } from '@educt/interfaces';
AlertDialogContent,
AlertDialogOverlay, /**
Button, * Components
} from '@chakra-ui/react'; */
import ConfirmDialog from '@educt/components/ConfirmDialog/ConfirmDialog';
/**
* Contexts
*/
import { UsersPageContext } from '@educt/contexts'; import { UsersPageContext } from '@educt/contexts';
/** /**
* Hooks * Hooks
*/ */
import { useContext, useState } from 'react';
import { useRootStore } from '@educt/hooks/useRootStore'; import { useRootStore } from '@educt/hooks/useRootStore';
import useIsMountedRef from '@educt/hooks/useIsMountedRef';
import { useToast } from '@chakra-ui/react'; import { useToast } from '@chakra-ui/react';
import { useErrorHandler } from 'react-error-boundary'; import { useErrorHandler } from 'react-error-boundary';
type DeleteUserDialogPropsType = { type DeleteUserDialogPropsType = {
onClose: () => void; user: IUser;
isOpen: boolean; isOpen: boolean;
onClose: () => void;
}; };
const DeleteUserDialog: React.FC<DeleteUserDialogPropsType> = ({ onClose, isOpen }) => { const DeleteUserDialog: React.FC<DeleteUserDialogPropsType> = ({ isOpen, onClose, user }) => {
const { userStore } = useRootStore(); const { userStore } = useRootStore();
const { deletingUser, setDeletingUser, searchingRole, search } = useContext(UsersPageContext); const { setDeletingUser, searchingRole, searchingPage, search } = useContext(UsersPageContext);
const isMountedRef = useIsMountedRef();
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const cancelRef = useRef<HTMLButtonElement | null>(null);
const handleError = useErrorHandler(); const handleError = useErrorHandler();
const toast = useToast(); const toast = useToast();
const { pagination } = userStore;
/** /**
* Delete user handler * Delete user handler
*
* @param isConfirmed Is dialog confirmed
* @param id User id
*/ */
const onDelete = async (id: string) => { const deleteUserHandler = async (isConfirmed: boolean, id: string) => {
if (isConfirmed) {
try { try {
setIsLoading(true); setIsLoading(true);
await userStore.deleteUser(id, { await userStore.deleteUser(id, {
page: userStore.pagination?.current_page, page: searchingPage,
limit: userStore.pagination?.per_page, limit: pagination?.per_page,
role: searchingRole, role: searchingRole,
search, search,
}); });
toast({ title: 'User deleted.', status: 'info' }); toast({ title: 'User deleted.', status: 'info' });
onClose();
setDeletingUser(undefined);
} catch (error: any) { } catch (error: any) {
if (error.response) { if (error.response) {
toast({ title: error.message, status: 'error' }); toast({ title: error.message, status: 'error' });
@ -54,49 +62,26 @@ const DeleteUserDialog: React.FC<DeleteUserDialogPropsType> = ({ onClose, isOpen
handleError(error); handleError(error);
} }
} finally { } finally {
if (isMountedRef.current) {
setIsLoading(false); setIsLoading(false);
setDeletingUser(undefined);
} }
} }
/**
* Close dialog
*/
onClose();
}; };
if (deletingUser === undefined) return null;
return ( return (
<> <ConfirmDialog
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}> isOpen={isOpen}
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
Confirm deleting
</AlertDialogHeader>
<AlertDialogBody>
The user named "{deletingUser.first_name}&nbsp;{deletingUser.last_name}" will be removed from the system.
</AlertDialogBody>
<AlertDialogFooter>
<Button
ref={cancelRef}
onClick={() => {
setDeletingUser(undefined);
onClose();
}}
>
Cancel
</Button>
<Button
colorScheme='red'
isLoading={isLoading} isLoading={isLoading}
loadingText='Deleting...' onClose={isConfirmed => deleteUserHandler(isConfirmed, user.id)}
onClick={() => onDelete(deletingUser.id)} title='Confirm deleting'
ml={3} message={`The user named "${user.fullname}" will be removed from the system.`}
> confirmMessage='Delete'
Delete />
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
</>
); );
}; };

View File

@ -25,7 +25,7 @@ import { yupResolver } from '@hookform/resolvers/yup';
/** /**
* Types * Types
*/ */
import { IMe } from '@educt/interfaces'; import { IMe, IUser } from '@educt/interfaces';
import { UserRoleEnum } from '@educt/enums'; import { UserRoleEnum } from '@educt/enums';
import { SubmitHandler } from 'react-hook-form'; import { SubmitHandler } from 'react-hook-form';
@ -49,9 +49,9 @@ import { UsersPageContext } from '@educt/contexts';
import UpdateUserSchema from './EditUserForm.validator'; import UpdateUserSchema from './EditUserForm.validator';
type UpdateUserFormPropsType = { type UpdateUserFormPropsType = {
onClose: () => void; user: IUser;
isOpen: boolean; isOpen: boolean;
me: IMe; onClose: () => void;
}; };
type UpdateUserInputType = { type UpdateUserInputType = {
@ -63,9 +63,9 @@ type UpdateUserInputType = {
password: string | null; password: string | null;
}; };
const UpdateUserForm: React.FC<UpdateUserFormPropsType> = ({ onClose, isOpen, me }) => { const UpdateUserForm: React.FC<UpdateUserFormPropsType> = ({ user, onClose, isOpen }) => {
const { userStore } = useRootStore(); const { userStore } = useRootStore();
const { editingUser, setEditingUser } = useContext(UsersPageContext); const { setEditingUser } = useContext(UsersPageContext);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const { const {
register, register,
@ -75,10 +75,10 @@ const UpdateUserForm: React.FC<UpdateUserFormPropsType> = ({ onClose, isOpen, me
} = useForm<UpdateUserInputType>({ } = useForm<UpdateUserInputType>({
resolver: yupResolver(UpdateUserSchema), resolver: yupResolver(UpdateUserSchema),
defaultValues: { defaultValues: {
first_name: editingUser && editingUser.first_name, first_name: user.first_name,
last_name: editingUser && editingUser.last_name, last_name: user.last_name,
email: editingUser && editingUser.email, email: user.email,
role: editingUser && editingUser.roles[0].slug, role: user.roles[0].slug,
login: '', login: '',
password: '', password: '',
}, },
@ -87,13 +87,18 @@ const UpdateUserForm: React.FC<UpdateUserFormPropsType> = ({ onClose, isOpen, me
const toast = useToast(); const toast = useToast();
const handleError = useErrorHandler(); const handleError = useErrorHandler();
if (editingUser === undefined) return null; const { me } = userStore;
if (me === null) return null;
/**
* Submit handler
*/
const onSubmit: SubmitHandler<UpdateUserInputType> = async data => { const onSubmit: SubmitHandler<UpdateUserInputType> = async data => {
try { try {
setIsLoading(true); setIsLoading(true);
const params = Object.fromEntries(Object.keys(dirtyFields).map(k => [k, data[k as keyof UpdateUserInputType]])); const params = Object.fromEntries(Object.keys(dirtyFields).map(k => [k, data[k as keyof UpdateUserInputType]]));
await userStore.updateUser(editingUser.id, params); await userStore.updateUser(user.id, params);
toast({ title: 'User updated.', status: 'info' }); toast({ title: 'User updated.', status: 'info' });
onCloseModal(); onCloseModal();
} catch (error: any) { } catch (error: any) {
@ -136,7 +141,7 @@ const UpdateUserForm: React.FC<UpdateUserFormPropsType> = ({ onClose, isOpen, me
<Stack spacing='4'> <Stack spacing='4'>
<FormControl isDisabled> <FormControl isDisabled>
<FormLabel>Identificator</FormLabel> <FormLabel>Identificator</FormLabel>
<Input value={editingUser?.id} /> <Input value={user.id} />
</FormControl> </FormControl>
<FormControl> <FormControl>

View File

@ -14,7 +14,6 @@ import DeleteUserDialog from '../DeleteUserDialog';
/** /**
* Types * Types
*/ */
import type { IUser } from '@educt/interfaces';
import type { UsersPageContextType } from '@educt/types'; import type { UsersPageContextType } from '@educt/types';
import type { UserItemPropsType } from './UserItem'; import type { UserItemPropsType } from './UserItem';
@ -27,107 +26,75 @@ import { UsersPageContext } from '@educt/contexts';
* Hooks * Hooks
*/ */
import { useEffect, useContext } from 'react'; import { useEffect, useContext } from 'react';
import { useLocation } from 'react-router';
import { useErrorHandler } from 'react-error-boundary'; import { useErrorHandler } from 'react-error-boundary';
import { useRootStore } from '@educt/hooks/useRootStore'; import { useRootStore } from '@educt/hooks/useRootStore';
import { useDisclosure } from '@chakra-ui/hooks'; import { useDisclosure } from '@chakra-ui/hooks';
type UserListPropsType = { render: React.FC<UserItemPropsType> }; type UserListPropsType = { render: React.FC<UserItemPropsType>; limit?: number };
const UserList: React.FC<UserListPropsType> = ({ render: Item }) => { const UserList: React.FC<UserListPropsType> = ({ render: Item, limit }) => {
const { userStore } = useRootStore(); const { userStore } = useRootStore();
const { searchingRole, loading, setLoading, search, editingUser, setEditingUser, deletingUser, setDeletingUser } = const {
useContext<UsersPageContextType>(UsersPageContext); searchingRole,
const location = useLocation(); search,
searchingPage,
setSearchingPage,
editingUser,
setEditingUser,
deletingUser,
setDeletingUser,
} = useContext<UsersPageContextType>(UsersPageContext);
const handleError = useErrorHandler(); const handleError = useErrorHandler();
const { onOpen: onOpenEditModal, onClose: onCloseEditModal, isOpen: isOpenEditModal } = useDisclosure(); const { onOpen: onOpenEditModal, onClose: onCloseEditModal, isOpen: isOpenEditModal } = useDisclosure();
const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure(); const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure();
const { users, pagination, me } = userStore; const { users, pagination, me, isLoading } = userStore;
/**
* Fetch users handler
*/
useEffect(() => { useEffect(() => {
userStore.loadUsersData().catch(error => handleError(error)); userStore
}, [handleError, userStore, location.search]); .loadUsersData({
page: searchingPage,
limit: limit || pagination?.per_page,
search,
role: searchingRole,
})
.catch(error => handleError(error));
}, [handleError, userStore, search, searchingRole, searchingPage]);
if (pagination === undefined || users === null || me === null) return <LoadingList />; if (pagination === undefined || users === null || me === null) return <LoadingList />;
const pagesCount = Math.ceil(pagination.total / pagination.per_page); const pagesCount = Math.ceil(pagination.total / pagination.per_page);
/**
* Handle next page
*/
const nextPage = async () => {
try {
setLoading(true);
await userStore.loadUsersData({
page: pagination.current_page + 1,
limit: pagination.per_page,
role: searchingRole,
search,
});
} catch (error: any) {
handleError(error);
} finally {
setLoading(false);
}
};
/**
* Handle prev page
*/
const prevPage = async () => {
try {
setLoading(true);
await userStore.loadUsersData({
page: pagination.current_page - 1,
limit: pagination.per_page,
role: searchingRole,
search,
});
} catch (error: any) {
handleError(error);
} finally {
setLoading(false);
}
};
/**
* Set editing state when click on edit button
* and open editing user form modal
*/
const onEditUser = (user: IUser) => {
setEditingUser(user);
onOpenEditModal();
};
/**
* Set deleting state when click on delete button
* and open delete user dialog
*/
const onDeleteUser = (user: IUser) => {
setDeletingUser(user);
onOpenDeleteDialog();
};
if (pagination.total === 0)
return (
<Box textAlign='center' mt='10' userSelect='none'>
<Text color='gray.500'>Cannot find any users.</Text>
</Box>
);
return ( return (
<Box>
{editingUser && <EditUserForm user={editingUser} isOpen={isOpenEditModal} onClose={onCloseEditModal} />}
{deletingUser && (
<DeleteUserDialog user={deletingUser} isOpen={isOpenDeleteDialog} onClose={onCloseDeleteDialog} />
)}
{users.length !== 0 ? (
<> <>
{editingUser && <EditUserForm me={me} onClose={onCloseEditModal} isOpen={isOpenEditModal} />}
{deletingUser && <DeleteUserDialog onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} />}
<Box> <Box>
<Flex mt='7' p='0 10px' fontWeight='bold' alignItems='center' justifyContent='space-between'> <Flex mt='7' p='0 10px' fontWeight='bold' alignItems='center' justifyContent='space-between'>
<Text>Total: ({pagination.total})</Text> <Text>Total: ({pagination.total})</Text>
<Text>Actions</Text> <Text>Actions</Text>
</Flex> </Flex>
{!loading ? ( {!isLoading ? (
<Stack mt='4' spacing='2'> <Stack mt='4' spacing='2'>
{users.map(user => ( {users.map(user => (
<Item key={user.id} user={user} onEdit={onEditUser} onDelete={onDeleteUser} /> <Item
key={user.id}
user={user}
onEdit={user => {
setEditingUser(user);
onOpenEditModal();
}}
onDelete={user => {
setDeletingUser(user);
onOpenDeleteDialog();
}}
/>
))} ))}
</Stack> </Stack>
) : ( ) : (
@ -140,7 +107,7 @@ const UserList: React.FC<UserListPropsType> = ({ render: Item }) => {
</Box> </Box>
<Flex ml='auto' mr='auto' justifyContent='center' flexWrap='wrap' alignItems='center' sx={{ gap: '30px' }}> <Flex ml='auto' mr='auto' justifyContent='center' flexWrap='wrap' alignItems='center' sx={{ gap: '30px' }}>
<Button <Button
onClick={prevPage} onClick={() => setSearchingPage(prevP => prevP - 1)}
disabled={pagination.current_page <= 1} disabled={pagination.current_page <= 1}
variant='link' variant='link'
leftIcon={<ChevronLeftIcon />} leftIcon={<ChevronLeftIcon />}
@ -148,7 +115,7 @@ const UserList: React.FC<UserListPropsType> = ({ render: Item }) => {
Previous page Previous page
</Button> </Button>
<Button <Button
onClick={nextPage} onClick={() => setSearchingPage(prevP => prevP + 1)}
disabled={pagination.current_page === pagesCount} disabled={pagination.current_page === pagesCount}
variant='link' variant='link'
rightIcon={<ChevronRightIcon />} rightIcon={<ChevronRightIcon />}
@ -158,6 +125,12 @@ const UserList: React.FC<UserListPropsType> = ({ render: Item }) => {
</Flex> </Flex>
</Flex> </Flex>
</> </>
) : (
<Box textAlign='center' mt='10' userSelect='none'>
<Text color='gray.500'>Cannot find any users.</Text>
</Box>
)}
</Box>
); );
}; };

View File

@ -1,58 +1,44 @@
import React, { useContext } from 'react'; import React from 'react';
import { observer } from 'mobx-react';
import { Flex, Select, Input, InputGroup, InputLeftElement } from '@chakra-ui/react'; import { Flex, Select, Input, InputGroup, InputLeftElement } from '@chakra-ui/react';
import { SearchIcon } from '@chakra-ui/icons'; import { SearchIcon } from '@chakra-ui/icons';
import { useRootStore } from '@educt/hooks/useRootStore';
import { UserRoleEnum } from '@educt/enums'; /**
import { useErrorHandler } from 'react-error-boundary'; * Types
import { UsersPageContext } from '@educt/contexts'; */
import { UsersPageContextType } from '@educt/types'; import { UsersPageContextType } from '@educt/types';
import { observer } from 'mobx-react'; import { UserRoleEnum } from '@educt/enums';
/**
* Contexts
*/
import { UsersPageContext } from '@educt/contexts';
/**
* Hooks
*/
import { useContext } from 'react';
type SelectRoleStateType = UserRoleEnum | 'any'; type SelectRoleStateType = UserRoleEnum | 'any';
const UserSearch: React.FC = () => { const UserSearch: React.FC = () => {
const { userStore } = useRootStore(); const { searchingRole, setSearchingRole, setSearchingPage, search, setSearch } =
const { searchingRole, setSearchingRole, setLoading, search, setSearch } =
useContext<UsersPageContextType>(UsersPageContext); useContext<UsersPageContextType>(UsersPageContext);
const { pagination } = userStore;
const handleError = useErrorHandler();
if (pagination === undefined) return null; /**
* Role change handler
const onSearchRole: React.ChangeEventHandler<HTMLSelectElement> | undefined = async e => { */
try { const onSearchRole: React.ChangeEventHandler<HTMLSelectElement> | undefined = e => {
setLoading(true); setSearchingPage(1);
const value = e.target.value as SelectRoleStateType; setSearchingRole(e.target.value as SelectRoleStateType);
await userStore.loadUsersData({
page: 1,
limit: pagination.per_page,
role: value,
search,
});
setSearchingRole(value);
} catch (error) {
handleError(error);
} finally {
setLoading(false);
}
}; };
const onSearch: React.ChangeEventHandler<HTMLInputElement> | undefined = async e => { /**
try { * Search box change handler
const value = e.target.value; */
setLoading(true); const onSearch: React.ChangeEventHandler<HTMLInputElement> | undefined = e => {
setSearch(value); setSearchingPage(1);
await userStore.loadUsersData({ setSearch(e.target.value);
page: 1,
limit: pagination.per_page,
role: searchingRole,
search: value,
});
} catch (error) {
handleError(error);
} finally {
setLoading(false);
}
}; };
return ( return (
@ -71,4 +57,4 @@ const UserSearch: React.FC = () => {
); );
}; };
export default observer(UserSearch); export default UserSearch;

View File

@ -43,9 +43,9 @@ const UsersPage: React.FC<IPageProps> = () => {
<Flex mt='5' flexDir='column' h='full'> <Flex mt='5' flexDir='column' h='full'>
<Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'> <Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'>
<UserSearch /> <UserSearch />
<CreateUserForm me={me} /> <CreateUserForm />
</Flex> </Flex>
<UserList render={UserItem} /> <UserList render={UserItem} limit={6} />
</Flex> </Flex>
</Box> </Box>
</UsersPageContextProvider> </UsersPageContextProvider>

View File

@ -33,7 +33,7 @@ export const ChangeEmailPageContextProvider: React.FC = ({ children }) => {
*/ */
export const UsersPageContextProvider: React.FC = ({ children }) => { export const UsersPageContextProvider: React.FC = ({ children }) => {
const [searchingRole, setSearchingRole] = useState<SearchingRoleStateType>(undefined); const [searchingRole, setSearchingRole] = useState<SearchingRoleStateType>(undefined);
const [loading, setLoading] = useState<boolean>(false); const [searchingPage, setSearchingPage] = useState<number>(1);
const [search, setSearch] = useState<string | undefined>(undefined); const [search, setSearch] = useState<string | undefined>(undefined);
const [editingUser, setEditingUser] = useState<IUser | undefined>(undefined); const [editingUser, setEditingUser] = useState<IUser | undefined>(undefined);
const [deletingUser, setDeletingUser] = useState<IUser | undefined>(undefined); const [deletingUser, setDeletingUser] = useState<IUser | undefined>(undefined);
@ -43,8 +43,8 @@ export const UsersPageContextProvider: React.FC = ({ children }) => {
value={{ value={{
searchingRole, searchingRole,
setSearchingRole, setSearchingRole,
loading, searchingPage,
setLoading, setSearchingPage,
search, search,
setSearch, setSearch,
editingUser, editingUser,

View File

@ -30,14 +30,21 @@ export default class UserStore {
public pagination: IPaginationMeta | undefined; public pagination: IPaginationMeta | undefined;
public isLoading: boolean = false;
constructor(root: RootStore, api: AxiosInstance) { constructor(root: RootStore, api: AxiosInstance) {
this.root = root; this.root = root;
this.userService = new UserService(api); this.userService = new UserService(api);
makeAutoObservable(this); makeAutoObservable(this);
} }
private setLoading(loading: boolean) {
this.isLoading = loading;
}
public async loadUsersData(params?: FetchUsersParamsType) { public async loadUsersData(params?: FetchUsersParamsType) {
try { try {
this.setLoading(true);
const result = await this.userService.fetchAll(params); const result = await this.userService.fetchAll(params);
runInAction(() => { runInAction(() => {
@ -48,6 +55,8 @@ export default class UserStore {
return result; return result;
} catch (error: any) { } catch (error: any) {
throw error; throw error;
} finally {
this.setLoading(false);
} }
} }

12
src/types/index.d.ts vendored
View File

@ -59,12 +59,6 @@ export type UsersPageContextType = {
searchingRole: SearchingRoleStateType; searchingRole: SearchingRoleStateType;
setSearchingRole: React.Dispatch<React.SetStateAction<SearchingRoleStateType>>; setSearchingRole: React.Dispatch<React.SetStateAction<SearchingRoleStateType>>;
/**
* Set loading state while loading users
*/
loading: boolean;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
/** /**
* Serach user by fullname or email * Serach user by fullname or email
*/ */
@ -77,6 +71,12 @@ export type UsersPageContextType = {
editingUser: IUser | undefined; editingUser: IUser | undefined;
setEditingUser: React.Dispatch<React.SetStateAction<IUser | undefined>>; setEditingUser: React.Dispatch<React.SetStateAction<IUser | undefined>>;
/**
* Search users by page
*/
searchingPage: number;
setSearchingPage: React.Dispatch<React.SetStateAction<number>>;
/** /**
* Set deleting user state when press on delete button * Set deleting user state when press on delete button
*/ */