mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 18:29:15 +03:00
refactor: refactored user list & alert dialog
This commit is contained in:
parent
479a6291eb
commit
2bcaad519e
63
src/components/ConfirmDialog/ConfirmDialog.tsx
Normal file
63
src/components/ConfirmDialog/ConfirmDialog.tsx
Normal 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;
|
||||
@ -14,8 +14,8 @@ export const ChangeEmailPageContext = createContext<ChangeEmailPageContextType>(
|
||||
export const UsersPageContext = createContext<UsersPageContextType>({
|
||||
searchingRole: undefined,
|
||||
setSearchingRole: () => {},
|
||||
loading: false,
|
||||
setLoading: () => {},
|
||||
searchingPage: 1,
|
||||
setSearchingPage: () => {},
|
||||
search: undefined,
|
||||
setSearch: () => {},
|
||||
editingUser: undefined,
|
||||
|
||||
@ -92,6 +92,7 @@ const CourseItem: React.FC<CourseItemPropsType> = ({ course, onDelete, onSetStat
|
||||
to={`/course/${course.id}`}
|
||||
as={ReactRouterLink}
|
||||
display='block'
|
||||
h='full'
|
||||
textDecoration='none'
|
||||
_visited={{ textDecoration: 'none' }}
|
||||
_hover={{ textDecoration: 'none' }}
|
||||
|
||||
@ -56,15 +56,6 @@ const CourseList: React.FC<CourseListPropsType> = ({ render: Item }) => {
|
||||
}
|
||||
}, [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
|
||||
*/
|
||||
@ -81,29 +72,38 @@ const CourseList: React.FC<CourseListPropsType> = ({ render: Item }) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Loading
|
||||
*/
|
||||
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 (
|
||||
<Box>
|
||||
{deletingCourse && <DeleteCourseDialog onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} />}
|
||||
<Grid templateColumns={{ sm: 'repeat(auto-fill, minmax(400px, 1fr))' }} gap='4'>
|
||||
{courses.map(course => (
|
||||
<Item key={course.id} course={course} onDelete={onDeleteCourse} onSetStatus={onSetStatus} />
|
||||
))}
|
||||
</Grid>
|
||||
{deletingCourse && (
|
||||
<DeleteCourseDialog course={deletingCourse} onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} />
|
||||
)}
|
||||
{courses.length !== 0 ? (
|
||||
<Grid templateColumns={{ sm: 'repeat(auto-fill, minmax(400px, 1fr))' }} gap='4'>
|
||||
{courses.map(course => (
|
||||
<Item
|
||||
key={course.id}
|
||||
course={course}
|
||||
onDelete={() => {
|
||||
setDeletingCourse(course);
|
||||
onOpenDeleteDialog();
|
||||
}}
|
||||
onSetStatus={onSetStatus}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<Box textAlign='center' mt='10' userSelect='none'>
|
||||
<Text color='gray.500'>
|
||||
There are no courses
|
||||
{!selectedCategory && !courseStatus
|
||||
? 'in the system yet'
|
||||
: `in this category${courseStatus ? ' or with this status' : ''}`}
|
||||
.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@ -10,6 +10,11 @@ import {
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
/**
|
||||
* Components
|
||||
*/
|
||||
import ConfirmDialog from '@educt/components/ConfirmDialog/ConfirmDialog';
|
||||
|
||||
/**
|
||||
* Contexts
|
||||
*/
|
||||
@ -18,87 +23,62 @@ import { CoursesPageContext } from '@educt/contexts';
|
||||
/**
|
||||
* Hooks
|
||||
*/
|
||||
import useIsMountedRef from '@educt/hooks/useIsMountedRef';
|
||||
import { useErrorHandler } from 'react-error-boundary';
|
||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
import { ICourse } from '@educt/interfaces';
|
||||
|
||||
type DeleteCourseDialogProps = {
|
||||
type DeleteCourseDialogPropsType = {
|
||||
course: Pick<ICourse, 'id' | 'title'>;
|
||||
onClose: () => void;
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
const DeleteCourseDialog: React.FC<DeleteCourseDialogProps> = ({ onClose, isOpen }) => {
|
||||
const DeleteCourseDialog: React.FC<DeleteCourseDialogPropsType> = ({ onClose, isOpen, course }) => {
|
||||
const { courseStore } = useRootStore();
|
||||
const { deletingCourse, setDeletingCourse } = useContext(CoursesPageContext);
|
||||
const isMountedRef = useIsMountedRef();
|
||||
const { setDeletingCourse } = useContext(CoursesPageContext);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const cancelRef = useRef<HTMLButtonElement | null>(null);
|
||||
const handleError = useErrorHandler();
|
||||
const toast = useToast();
|
||||
|
||||
/**
|
||||
* Delete course handler
|
||||
*
|
||||
* @param isConfirmed Is dialog confirmed
|
||||
* @param id Course id
|
||||
*/
|
||||
const onDelete = async (id: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await courseStore.deleteCourse(id);
|
||||
toast({ title: 'Course deleted.', status: 'info' });
|
||||
onClose();
|
||||
setDeletingCourse(undefined);
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
toast({ title: error.message, status: 'error' });
|
||||
} else {
|
||||
handleError(error);
|
||||
}
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
const deleteCourseHandler = async (isConfirmed: boolean, id: string) => {
|
||||
if (isConfirmed) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await courseStore.deleteCourse(id);
|
||||
toast({ title: 'Course deleted.', status: 'info' });
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
toast({ title: error.message, status: 'error' });
|
||||
} else {
|
||||
handleError(error);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setDeletingCourse(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close dialog
|
||||
*/
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (deletingCourse === undefined) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
||||
<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}
|
||||
loadingText='Deleting...'
|
||||
onClick={() => onDelete(deletingCourse.id)}
|
||||
ml={3}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
</>
|
||||
<ConfirmDialog
|
||||
isOpen={isOpen}
|
||||
isLoading={isLoading}
|
||||
onClose={isConfirmed => deleteCourseHandler(isConfirmed, course.id)}
|
||||
title='Confirm deleting'
|
||||
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'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -45,9 +45,9 @@ type CreateCourseInputType = {
|
||||
description: string;
|
||||
teacher_id: string;
|
||||
};
|
||||
type CreateCourseFormProps = {};
|
||||
type CreateCourseFormPropsType = {};
|
||||
|
||||
const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
|
||||
const CreateCourseForm: React.FC<CreateCourseFormPropsType> = () => {
|
||||
const {
|
||||
userStore: { userService },
|
||||
categoryStore: { categoryService },
|
||||
@ -58,8 +58,6 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
control,
|
||||
watch,
|
||||
reset,
|
||||
} = useForm<CreateCourseInputType>({
|
||||
resolver: yupResolver(CreateCourseSchema),
|
||||
});
|
||||
@ -205,6 +203,7 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
|
||||
{errors.category_id?.message}
|
||||
</Text>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
isRequired
|
||||
id='teacher_id'
|
||||
@ -231,6 +230,7 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
|
||||
{errors.teacher_id?.message}
|
||||
</Text>
|
||||
</FormControl>
|
||||
|
||||
<FormControl isRequired id='description' gridColumn='1' isInvalid={!!errors.description}>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<Textarea
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
@ -21,18 +21,17 @@ import {
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { MdSupervisorAccount } from 'react-icons/md';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { SubmitHandler } from 'react-hook-form';
|
||||
import CreateUserSchema from './CreateUserModal.validator';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
import { IMe } from '@educt/interfaces';
|
||||
import { UserRoleEnum } from '@educt/enums';
|
||||
import { SubmitHandler } from 'react-hook-form';
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
*/
|
||||
import { useContext, useState } from 'react';
|
||||
import useIsMountedRef from '@educt/hooks/useIsMountedRef';
|
||||
import { useDisclosure } from '@chakra-ui/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@ -45,9 +44,12 @@ import { useToast } from '@chakra-ui/react';
|
||||
*/
|
||||
import { UsersPageContext } from '@educt/contexts';
|
||||
|
||||
type CreateUserModalPropsType = {
|
||||
me: IMe;
|
||||
};
|
||||
/**
|
||||
* Schema
|
||||
*/
|
||||
import CreateUserSchema from './CreateUserForm.validator';
|
||||
|
||||
type CreateUserFormPropsType = {};
|
||||
|
||||
type CreateUserInputType = {
|
||||
first_name: string;
|
||||
@ -58,7 +60,7 @@ type CreateUserInputType = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
const CreateUserModal: React.FC<CreateUserModalPropsType> = ({ me }) => {
|
||||
const CreateUserModal: React.FC<CreateUserFormPropsType> = () => {
|
||||
const { userStore } = useRootStore();
|
||||
const { searchingRole, search } = useContext(UsersPageContext);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
@ -75,8 +77,12 @@ const CreateUserModal: React.FC<CreateUserModalPropsType> = ({ me }) => {
|
||||
const toast = useToast();
|
||||
const handleError = useErrorHandler();
|
||||
|
||||
const { me } = userStore;
|
||||
|
||||
if (me === null) return null;
|
||||
|
||||
/**
|
||||
* Create new user handler
|
||||
* Submit handler
|
||||
*/
|
||||
const onSubmit: SubmitHandler<CreateUserInputType> = async data => {
|
||||
try {
|
||||
|
||||
@ -1,102 +1,87 @@
|
||||
import React, { useContext, useRef, useState } from 'react';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogBody,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogContent,
|
||||
AlertDialogOverlay,
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
import { IUser } from '@educt/interfaces';
|
||||
|
||||
/**
|
||||
* Components
|
||||
*/
|
||||
import ConfirmDialog from '@educt/components/ConfirmDialog/ConfirmDialog';
|
||||
|
||||
/**
|
||||
* Contexts
|
||||
*/
|
||||
import { UsersPageContext } from '@educt/contexts';
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
*/
|
||||
import { useContext, useState } from 'react';
|
||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
import useIsMountedRef from '@educt/hooks/useIsMountedRef';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useErrorHandler } from 'react-error-boundary';
|
||||
|
||||
type DeleteUserDialogPropsType = {
|
||||
onClose: () => void;
|
||||
user: IUser;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const DeleteUserDialog: React.FC<DeleteUserDialogPropsType> = ({ onClose, isOpen }) => {
|
||||
const DeleteUserDialog: React.FC<DeleteUserDialogPropsType> = ({ isOpen, onClose, user }) => {
|
||||
const { userStore } = useRootStore();
|
||||
const { deletingUser, setDeletingUser, searchingRole, search } = useContext(UsersPageContext);
|
||||
const isMountedRef = useIsMountedRef();
|
||||
const { setDeletingUser, searchingRole, searchingPage, search } = useContext(UsersPageContext);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const cancelRef = useRef<HTMLButtonElement | null>(null);
|
||||
const handleError = useErrorHandler();
|
||||
const toast = useToast();
|
||||
|
||||
const { pagination } = userStore;
|
||||
|
||||
/**
|
||||
* Delete user handler
|
||||
*
|
||||
* @param isConfirmed Is dialog confirmed
|
||||
* @param id User id
|
||||
*/
|
||||
const onDelete = async (id: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await userStore.deleteUser(id, {
|
||||
page: userStore.pagination?.current_page,
|
||||
limit: userStore.pagination?.per_page,
|
||||
role: searchingRole,
|
||||
search,
|
||||
});
|
||||
toast({ title: 'User deleted.', status: 'info' });
|
||||
onClose();
|
||||
setDeletingUser(undefined);
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
toast({ title: error.message, status: 'error' });
|
||||
} else {
|
||||
handleError(error);
|
||||
}
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
const deleteUserHandler = async (isConfirmed: boolean, id: string) => {
|
||||
if (isConfirmed) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await userStore.deleteUser(id, {
|
||||
page: searchingPage,
|
||||
limit: pagination?.per_page,
|
||||
role: searchingRole,
|
||||
search,
|
||||
});
|
||||
toast({ title: 'User deleted.', status: 'info' });
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
toast({ title: error.message, status: 'error' });
|
||||
} else {
|
||||
handleError(error);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setDeletingUser(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close dialog
|
||||
*/
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (deletingUser === undefined) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
|
||||
Confirm deleting
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogBody>
|
||||
The user named "{deletingUser.first_name} {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}
|
||||
loadingText='Deleting...'
|
||||
onClick={() => onDelete(deletingUser.id)}
|
||||
ml={3}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
</>
|
||||
<ConfirmDialog
|
||||
isOpen={isOpen}
|
||||
isLoading={isLoading}
|
||||
onClose={isConfirmed => deleteUserHandler(isConfirmed, user.id)}
|
||||
title='Confirm deleting'
|
||||
message={`The user named "${user.fullname}" will be removed from the system.`}
|
||||
confirmMessage='Delete'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ import { yupResolver } from '@hookform/resolvers/yup';
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
import { IMe } from '@educt/interfaces';
|
||||
import { IMe, IUser } from '@educt/interfaces';
|
||||
import { UserRoleEnum } from '@educt/enums';
|
||||
import { SubmitHandler } from 'react-hook-form';
|
||||
|
||||
@ -49,9 +49,9 @@ import { UsersPageContext } from '@educt/contexts';
|
||||
import UpdateUserSchema from './EditUserForm.validator';
|
||||
|
||||
type UpdateUserFormPropsType = {
|
||||
onClose: () => void;
|
||||
user: IUser;
|
||||
isOpen: boolean;
|
||||
me: IMe;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
type UpdateUserInputType = {
|
||||
@ -63,9 +63,9 @@ type UpdateUserInputType = {
|
||||
password: string | null;
|
||||
};
|
||||
|
||||
const UpdateUserForm: React.FC<UpdateUserFormPropsType> = ({ onClose, isOpen, me }) => {
|
||||
const UpdateUserForm: React.FC<UpdateUserFormPropsType> = ({ user, onClose, isOpen }) => {
|
||||
const { userStore } = useRootStore();
|
||||
const { editingUser, setEditingUser } = useContext(UsersPageContext);
|
||||
const { setEditingUser } = useContext(UsersPageContext);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const {
|
||||
register,
|
||||
@ -75,10 +75,10 @@ const UpdateUserForm: React.FC<UpdateUserFormPropsType> = ({ onClose, isOpen, me
|
||||
} = useForm<UpdateUserInputType>({
|
||||
resolver: yupResolver(UpdateUserSchema),
|
||||
defaultValues: {
|
||||
first_name: editingUser && editingUser.first_name,
|
||||
last_name: editingUser && editingUser.last_name,
|
||||
email: editingUser && editingUser.email,
|
||||
role: editingUser && editingUser.roles[0].slug,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
email: user.email,
|
||||
role: user.roles[0].slug,
|
||||
login: '',
|
||||
password: '',
|
||||
},
|
||||
@ -87,13 +87,18 @@ const UpdateUserForm: React.FC<UpdateUserFormPropsType> = ({ onClose, isOpen, me
|
||||
const toast = useToast();
|
||||
const handleError = useErrorHandler();
|
||||
|
||||
if (editingUser === undefined) return null;
|
||||
const { me } = userStore;
|
||||
|
||||
if (me === null) return null;
|
||||
|
||||
/**
|
||||
* Submit handler
|
||||
*/
|
||||
const onSubmit: SubmitHandler<UpdateUserInputType> = async data => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
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' });
|
||||
onCloseModal();
|
||||
} catch (error: any) {
|
||||
@ -136,7 +141,7 @@ const UpdateUserForm: React.FC<UpdateUserFormPropsType> = ({ onClose, isOpen, me
|
||||
<Stack spacing='4'>
|
||||
<FormControl isDisabled>
|
||||
<FormLabel>Identificator</FormLabel>
|
||||
<Input value={editingUser?.id} />
|
||||
<Input value={user.id} />
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
|
||||
@ -14,7 +14,6 @@ import DeleteUserDialog from '../DeleteUserDialog';
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
import type { IUser } from '@educt/interfaces';
|
||||
import type { UsersPageContextType } from '@educt/types';
|
||||
import type { UserItemPropsType } from './UserItem';
|
||||
|
||||
@ -27,137 +26,111 @@ import { UsersPageContext } from '@educt/contexts';
|
||||
* Hooks
|
||||
*/
|
||||
import { useEffect, useContext } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
import { useErrorHandler } from 'react-error-boundary';
|
||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
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 { searchingRole, loading, setLoading, search, editingUser, setEditingUser, deletingUser, setDeletingUser } =
|
||||
useContext<UsersPageContextType>(UsersPageContext);
|
||||
const location = useLocation();
|
||||
const {
|
||||
searchingRole,
|
||||
search,
|
||||
searchingPage,
|
||||
setSearchingPage,
|
||||
editingUser,
|
||||
setEditingUser,
|
||||
deletingUser,
|
||||
setDeletingUser,
|
||||
} = useContext<UsersPageContextType>(UsersPageContext);
|
||||
const handleError = useErrorHandler();
|
||||
const { onOpen: onOpenEditModal, onClose: onCloseEditModal, isOpen: isOpenEditModal } = useDisclosure();
|
||||
const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure();
|
||||
const { users, pagination, me } = userStore;
|
||||
const { users, pagination, me, isLoading } = userStore;
|
||||
|
||||
/**
|
||||
* Fetch users handler
|
||||
*/
|
||||
useEffect(() => {
|
||||
userStore.loadUsersData().catch(error => handleError(error));
|
||||
}, [handleError, userStore, location.search]);
|
||||
userStore
|
||||
.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 />;
|
||||
|
||||
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 (
|
||||
<>
|
||||
{editingUser && <EditUserForm me={me} onClose={onCloseEditModal} isOpen={isOpenEditModal} />}
|
||||
{deletingUser && <DeleteUserDialog onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} />}
|
||||
<Box>
|
||||
<Flex mt='7' p='0 10px' fontWeight='bold' alignItems='center' justifyContent='space-between'>
|
||||
<Text>Total: ({pagination.total})</Text>
|
||||
<Text>Actions</Text>
|
||||
</Flex>
|
||||
{!loading ? (
|
||||
<Stack mt='4' spacing='2'>
|
||||
{users.map(user => (
|
||||
<Item key={user.id} user={user} onEdit={onEditUser} onDelete={onDeleteUser} />
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<LoadingList />
|
||||
)}
|
||||
</Box>
|
||||
<Flex margin='2rem 0' flexDirection={{ base: 'column', sm: 'column', md: 'row' }}>
|
||||
<Box textAlign={{ base: 'center', sm: 'center', md: 'left' }} mb={{ base: '2', md: '0' }}>
|
||||
Page <b>{pagination.current_page}</b> of {pagesCount}
|
||||
<Box>
|
||||
{editingUser && <EditUserForm user={editingUser} isOpen={isOpenEditModal} onClose={onCloseEditModal} />}
|
||||
{deletingUser && (
|
||||
<DeleteUserDialog user={deletingUser} isOpen={isOpenDeleteDialog} onClose={onCloseDeleteDialog} />
|
||||
)}
|
||||
{users.length !== 0 ? (
|
||||
<>
|
||||
<Box>
|
||||
<Flex mt='7' p='0 10px' fontWeight='bold' alignItems='center' justifyContent='space-between'>
|
||||
<Text>Total: ({pagination.total})</Text>
|
||||
<Text>Actions</Text>
|
||||
</Flex>
|
||||
{!isLoading ? (
|
||||
<Stack mt='4' spacing='2'>
|
||||
{users.map(user => (
|
||||
<Item
|
||||
key={user.id}
|
||||
user={user}
|
||||
onEdit={user => {
|
||||
setEditingUser(user);
|
||||
onOpenEditModal();
|
||||
}}
|
||||
onDelete={user => {
|
||||
setDeletingUser(user);
|
||||
onOpenDeleteDialog();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<LoadingList />
|
||||
)}
|
||||
</Box>
|
||||
<Flex margin='2rem 0' flexDirection={{ base: 'column', sm: 'column', md: 'row' }}>
|
||||
<Box textAlign={{ base: 'center', sm: 'center', md: 'left' }} mb={{ base: '2', md: '0' }}>
|
||||
Page <b>{pagination.current_page}</b> of {pagesCount}
|
||||
</Box>
|
||||
<Flex ml='auto' mr='auto' justifyContent='center' flexWrap='wrap' alignItems='center' sx={{ gap: '30px' }}>
|
||||
<Button
|
||||
onClick={() => setSearchingPage(prevP => prevP - 1)}
|
||||
disabled={pagination.current_page <= 1}
|
||||
variant='link'
|
||||
leftIcon={<ChevronLeftIcon />}
|
||||
>
|
||||
Previous page
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setSearchingPage(prevP => prevP + 1)}
|
||||
disabled={pagination.current_page === pagesCount}
|
||||
variant='link'
|
||||
rightIcon={<ChevronRightIcon />}
|
||||
>
|
||||
Next page
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
) : (
|
||||
<Box textAlign='center' mt='10' userSelect='none'>
|
||||
<Text color='gray.500'>Cannot find any users.</Text>
|
||||
</Box>
|
||||
<Flex ml='auto' mr='auto' justifyContent='center' flexWrap='wrap' alignItems='center' sx={{ gap: '30px' }}>
|
||||
<Button
|
||||
onClick={prevPage}
|
||||
disabled={pagination.current_page <= 1}
|
||||
variant='link'
|
||||
leftIcon={<ChevronLeftIcon />}
|
||||
>
|
||||
Previous page
|
||||
</Button>
|
||||
<Button
|
||||
onClick={nextPage}
|
||||
disabled={pagination.current_page === pagesCount}
|
||||
variant='link'
|
||||
rightIcon={<ChevronRightIcon />}
|
||||
>
|
||||
Next page
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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 { SearchIcon } from '@chakra-ui/icons';
|
||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
import { UserRoleEnum } from '@educt/enums';
|
||||
import { useErrorHandler } from 'react-error-boundary';
|
||||
import { UsersPageContext } from '@educt/contexts';
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
const UserSearch: React.FC = () => {
|
||||
const { userStore } = useRootStore();
|
||||
const { searchingRole, setSearchingRole, setLoading, search, setSearch } =
|
||||
const { searchingRole, setSearchingRole, setSearchingPage, search, setSearch } =
|
||||
useContext<UsersPageContextType>(UsersPageContext);
|
||||
const { pagination } = userStore;
|
||||
const handleError = useErrorHandler();
|
||||
|
||||
if (pagination === undefined) return null;
|
||||
|
||||
const onSearchRole: React.ChangeEventHandler<HTMLSelectElement> | undefined = async e => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const value = 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);
|
||||
}
|
||||
/**
|
||||
* Role change handler
|
||||
*/
|
||||
const onSearchRole: React.ChangeEventHandler<HTMLSelectElement> | undefined = e => {
|
||||
setSearchingPage(1);
|
||||
setSearchingRole(e.target.value as SelectRoleStateType);
|
||||
};
|
||||
|
||||
const onSearch: React.ChangeEventHandler<HTMLInputElement> | undefined = async e => {
|
||||
try {
|
||||
const value = e.target.value;
|
||||
setLoading(true);
|
||||
setSearch(value);
|
||||
await userStore.loadUsersData({
|
||||
page: 1,
|
||||
limit: pagination.per_page,
|
||||
role: searchingRole,
|
||||
search: value,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
/**
|
||||
* Search box change handler
|
||||
*/
|
||||
const onSearch: React.ChangeEventHandler<HTMLInputElement> | undefined = e => {
|
||||
setSearchingPage(1);
|
||||
setSearch(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -71,4 +57,4 @@ const UserSearch: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(UserSearch);
|
||||
export default UserSearch;
|
||||
|
||||
@ -43,9 +43,9 @@ const UsersPage: React.FC<IPageProps> = () => {
|
||||
<Flex mt='5' flexDir='column' h='full'>
|
||||
<Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'>
|
||||
<UserSearch />
|
||||
<CreateUserForm me={me} />
|
||||
<CreateUserForm />
|
||||
</Flex>
|
||||
<UserList render={UserItem} />
|
||||
<UserList render={UserItem} limit={6} />
|
||||
</Flex>
|
||||
</Box>
|
||||
</UsersPageContextProvider>
|
||||
|
||||
@ -33,7 +33,7 @@ export const ChangeEmailPageContextProvider: React.FC = ({ children }) => {
|
||||
*/
|
||||
export const UsersPageContextProvider: React.FC = ({ children }) => {
|
||||
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 [editingUser, setEditingUser] = useState<IUser | undefined>(undefined);
|
||||
const [deletingUser, setDeletingUser] = useState<IUser | undefined>(undefined);
|
||||
@ -43,8 +43,8 @@ export const UsersPageContextProvider: React.FC = ({ children }) => {
|
||||
value={{
|
||||
searchingRole,
|
||||
setSearchingRole,
|
||||
loading,
|
||||
setLoading,
|
||||
searchingPage,
|
||||
setSearchingPage,
|
||||
search,
|
||||
setSearch,
|
||||
editingUser,
|
||||
|
||||
@ -30,14 +30,21 @@ export default class UserStore {
|
||||
|
||||
public pagination: IPaginationMeta | undefined;
|
||||
|
||||
public isLoading: boolean = false;
|
||||
|
||||
constructor(root: RootStore, api: AxiosInstance) {
|
||||
this.root = root;
|
||||
this.userService = new UserService(api);
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
private setLoading(loading: boolean) {
|
||||
this.isLoading = loading;
|
||||
}
|
||||
|
||||
public async loadUsersData(params?: FetchUsersParamsType) {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
const result = await this.userService.fetchAll(params);
|
||||
|
||||
runInAction(() => {
|
||||
@ -48,6 +55,8 @@ export default class UserStore {
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
12
src/types/index.d.ts
vendored
12
src/types/index.d.ts
vendored
@ -59,12 +59,6 @@ export type UsersPageContextType = {
|
||||
searchingRole: 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
|
||||
*/
|
||||
@ -77,6 +71,12 @@ export type UsersPageContextType = {
|
||||
editingUser: 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
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user