mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +03:00
feat: edit user form setup layout
This commit is contained in:
parent
07dc9c7f73
commit
99e22a4c69
@ -32,6 +32,7 @@ import { UserRoleEnum } from 'enums';
|
|||||||
/**
|
/**
|
||||||
* Hooks
|
* Hooks
|
||||||
*/
|
*/
|
||||||
|
import useIsMountedRef from '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';
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
@ -63,6 +64,7 @@ const CreateUserModal: React.FC<CreateUserModalPropsType> = ({ me }) => {
|
|||||||
resolver: yupResolver(CreateUserSchema),
|
resolver: yupResolver(CreateUserSchema),
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const isMountedRef = useIsMountedRef();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const handleError = useErrorHandler();
|
const handleError = useErrorHandler();
|
||||||
|
|
||||||
@ -84,13 +86,19 @@ const CreateUserModal: React.FC<CreateUserModalPropsType> = ({ me }) => {
|
|||||||
onClose();
|
onClose();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
|
if (error.response.status === 422) {
|
||||||
|
toast({ title: `${error.response.data.errors[0].message}`, status: 'error' });
|
||||||
|
} else {
|
||||||
toast({ title: `${error.message}`, status: 'error' });
|
toast({ title: `${error.message}`, status: 'error' });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
handleError(error);
|
handleError(error);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (isMountedRef.current) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
3
src/pages/users/components/CreateUserForm/index.ts
Normal file
3
src/pages/users/components/CreateUserForm/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import CreateUserForm from './CreateUserForm';
|
||||||
|
|
||||||
|
export default CreateUserForm;
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import CreateUserModal from './CreateUserModal';
|
|
||||||
|
|
||||||
export default CreateUserModal;
|
|
||||||
98
src/pages/users/components/UpdateUserForm/UpdateUserForm.tsx
Normal file
98
src/pages/users/components/UpdateUserForm/UpdateUserForm.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
Select,
|
||||||
|
Flex,
|
||||||
|
Text,
|
||||||
|
Divider,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { AddIcon } from '@chakra-ui/icons';
|
||||||
|
import { MdModeEdit } from 'react-icons/md';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types
|
||||||
|
*/
|
||||||
|
import { IUser } from 'interfaces';
|
||||||
|
import { UserRoleEnum } from 'enums';
|
||||||
|
|
||||||
|
type UpdateUserFormPropsType = {
|
||||||
|
user: IUser;
|
||||||
|
onClose: () => void;
|
||||||
|
isOpen: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UpdateUserForm: React.FC<UpdateUserFormPropsType> = ({ user, onClose, isOpen }) => {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<form>
|
||||||
|
<ModalHeader>
|
||||||
|
<Flex alignItems='center'>
|
||||||
|
<MdModeEdit />
|
||||||
|
<Text ml={2}>Edit user</Text>
|
||||||
|
</Flex>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<Divider />
|
||||||
|
<ModalBody pb={6}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>First name</FormLabel>
|
||||||
|
<Input placeholder='First name' value={user.first_name} />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl mt={4}>
|
||||||
|
<FormLabel>Last name</FormLabel>
|
||||||
|
<Input placeholder='Last name' value={user.last_name} />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl mt={4}>
|
||||||
|
<FormLabel>Login</FormLabel>
|
||||||
|
<Input placeholder='e.g student893' />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl mt={4}>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<Input placeholder='example@email.com' />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl mt={4}>
|
||||||
|
<FormLabel>Role</FormLabel>
|
||||||
|
<Select w='full' mr='2'>
|
||||||
|
<option value={UserRoleEnum.STUDENT}>Student</option>
|
||||||
|
<option value={UserRoleEnum.TEACHER}>Teacher</option>
|
||||||
|
<option value={UserRoleEnum.ADMIN}>Administrator</option>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl mt={4}>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<Input placeholder='******' />
|
||||||
|
</FormControl>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
variant='outline'
|
||||||
|
mr={3}
|
||||||
|
colorScheme='blue'
|
||||||
|
loadingText='Creating...'
|
||||||
|
leftIcon={<AddIcon />}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpdateUserForm;
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import yup from 'schema';
|
||||||
|
import { UserRoleEnum } from 'enums';
|
||||||
|
|
||||||
|
const UpdateUserSchema = yup.object().shape({
|
||||||
|
first_name: yup.string().required('First field name is required'),
|
||||||
|
last_name: yup.string().required('Last field name is required'),
|
||||||
|
login: yup.string().required('Login field is required'),
|
||||||
|
email: yup.string().email('Email is invalid').required('Email field is required'),
|
||||||
|
role: yup
|
||||||
|
.mixed<UserRoleEnum>()
|
||||||
|
.required('Role field is required')
|
||||||
|
.oneOf(Object.values(UserRoleEnum), `Available roles: ${Object.keys(UserRoleEnum).join(', ')}`),
|
||||||
|
password: yup.string().required('Password field is required').min(6, 'Password must be at least 6 characters.'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default UpdateUserSchema;
|
||||||
3
src/pages/users/components/UpdateUserForm/index.ts
Normal file
3
src/pages/users/components/UpdateUserForm/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import UpdateUserForm from './UpdateUserForm';
|
||||||
|
|
||||||
|
export default UpdateUserForm;
|
||||||
@ -1,14 +1,46 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Flex, Avatar, Text, Tooltip, IconButton } from '@chakra-ui/react';
|
import * as helpres from 'helpers';
|
||||||
|
import { Box, Flex, Avatar, Text, IconButton } from '@chakra-ui/react';
|
||||||
import { MdMoreHoriz } from 'react-icons/md';
|
import { MdMoreHoriz } from 'react-icons/md';
|
||||||
import UserBadge from 'components/UserBadge';
|
import UserBadge from 'components/UserBadge';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types
|
||||||
|
*/
|
||||||
import { IUser } from 'interfaces';
|
import { IUser } from 'interfaces';
|
||||||
|
import { UserRoleEnum } from 'enums';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks
|
||||||
|
*/
|
||||||
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
|
|
||||||
type UserItemPropsType = {
|
type UserItemPropsType = {
|
||||||
user: IUser;
|
user: IUser;
|
||||||
|
onEdit: (user: IUser) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserItem: React.FC<UserItemPropsType> = ({ user, onEdit }) => {
|
||||||
|
const {
|
||||||
|
userStore: { me },
|
||||||
|
} = useRootStore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user with role TEACHER can edit users with only STUDENT role
|
||||||
|
*/
|
||||||
|
const isDisabledActions = () => {
|
||||||
|
if (helpres.userContainRoles(user.roles, [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER]) && me !== null) {
|
||||||
|
/**
|
||||||
|
* Disable editing
|
||||||
|
*/
|
||||||
|
if (me.isTeacher || me.isStudent) {
|
||||||
|
return true;
|
||||||
|
} else return false;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserItem: React.FC<UserItemPropsType> = ({ user }) => {
|
|
||||||
return (
|
return (
|
||||||
<Box borderWidth='1px' borderRadius='lg' w='full' p='3'>
|
<Box borderWidth='1px' borderRadius='lg' w='full' p='3'>
|
||||||
<Flex alignItems='center' justifyContent='space-between' sx={{ gap: '20px' }}>
|
<Flex alignItems='center' justifyContent='space-between' sx={{ gap: '20px' }}>
|
||||||
@ -25,9 +57,13 @@ const UserItem: React.FC<UserItemPropsType> = ({ user }) => {
|
|||||||
<UserBadge roles={user.roles} />
|
<UserBadge roles={user.roles} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box>
|
<Box>
|
||||||
<Tooltip label='Edit user' openDelay={400}>
|
<IconButton
|
||||||
<IconButton aria-label='Actions' variant='ghost' icon={<MdMoreHoriz />} />
|
disabled={isDisabledActions()}
|
||||||
</Tooltip>
|
onClick={() => onEdit(user)}
|
||||||
|
aria-label='Actions'
|
||||||
|
variant='ghost'
|
||||||
|
icon={<MdMoreHoriz />}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -1,26 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons';
|
import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons';
|
||||||
import { Flex, Box, Stack, Text } from '@chakra-ui/layout';
|
import { Flex, Box, Stack, Text } from '@chakra-ui/layout';
|
||||||
import { Button } from '@chakra-ui/button';
|
import { Button } from '@chakra-ui/button';
|
||||||
import { IPaginationMeta, IUser } from 'interfaces';
|
|
||||||
import React, { useContext } from 'react';
|
|
||||||
import UserItem from './UserItem';
|
import UserItem from './UserItem';
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
import UpdateUserForm from '../UpdateUserForm';
|
||||||
import { useErrorHandler } from 'react-error-boundary';
|
|
||||||
import { UsersPageContextType } from 'types';
|
|
||||||
import { UsersPageContext } from 'contexts';
|
|
||||||
import { observer } from 'mobx-react';
|
|
||||||
|
|
||||||
type UserListPropsType = {
|
/**
|
||||||
users: IUser[];
|
* Types
|
||||||
pagination: IPaginationMeta;
|
*/
|
||||||
};
|
import { IPaginationMeta, IUser } from 'interfaces';
|
||||||
|
import { UsersPageContextType } from 'types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contexts
|
||||||
|
*/
|
||||||
|
import { UsersPageContext } from 'contexts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks
|
||||||
|
*/
|
||||||
|
import { useContext, useState } from 'react';
|
||||||
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
|
import { useDisclosure } from '@chakra-ui/hooks';
|
||||||
|
import { useErrorHandler } from 'react-error-boundary';
|
||||||
|
|
||||||
|
type UserListPropsType = { users: IUser[]; pagination: IPaginationMeta };
|
||||||
|
|
||||||
const UserList: React.FC<UserListPropsType> = ({ users, pagination }) => {
|
const UserList: React.FC<UserListPropsType> = ({ users, pagination }) => {
|
||||||
const { userStore } = useRootStore();
|
const { userStore } = useRootStore();
|
||||||
|
const [editingUser, setEditingUser] = useState<IUser | undefined>(undefined);
|
||||||
const { searchingRole, loading, setLoading, search } = useContext<UsersPageContextType>(UsersPageContext);
|
const { searchingRole, loading, setLoading, search } = useContext<UsersPageContextType>(UsersPageContext);
|
||||||
const handleError = useErrorHandler();
|
const handleError = useErrorHandler();
|
||||||
const pagesCount = Math.ceil(pagination.total / pagination.per_page);
|
const pagesCount = Math.ceil(pagination.total / pagination.per_page);
|
||||||
|
const { onOpen: onOpenEditModal, onClose: onCloseEditModal, isOpen: isOpenEditModal } = useDisclosure();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle next page
|
||||||
|
*/
|
||||||
const nextPage = async () => {
|
const nextPage = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -37,6 +54,9 @@ const UserList: React.FC<UserListPropsType> = ({ users, pagination }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle prev page
|
||||||
|
*/
|
||||||
const prevPage = async () => {
|
const prevPage = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -53,10 +73,20 @@ const UserList: React.FC<UserListPropsType> = ({ users, pagination }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set editing state when click on edit button
|
||||||
|
*/
|
||||||
|
const onEditUser = (user: IUser) => {
|
||||||
|
setEditingUser(user);
|
||||||
|
onOpenEditModal();
|
||||||
|
console.log(`[LOG]: Editing user "${user.id}"`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{pagination.total !== 0 ? (
|
{pagination.total !== 0 ? (
|
||||||
<>
|
<>
|
||||||
|
{editingUser && <UpdateUserForm user={editingUser} onClose={onCloseEditModal} isOpen={isOpenEditModal} />}
|
||||||
<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>
|
||||||
@ -69,7 +99,7 @@ const UserList: React.FC<UserListPropsType> = ({ users, pagination }) => {
|
|||||||
) : (
|
) : (
|
||||||
<Stack mt='4' spacing='2'>
|
<Stack mt='4' spacing='2'>
|
||||||
{users.map(user => (
|
{users.map(user => (
|
||||||
<UserItem key={user.id} user={user} />
|
<UserItem key={user.id} user={user} onEdit={onEditUser} />
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import { useLocation } from 'react-router';
|
|||||||
* Context
|
* Context
|
||||||
*/
|
*/
|
||||||
import { UsersPageContext } from 'contexts';
|
import { UsersPageContext } from 'contexts';
|
||||||
import CreateUserModal from './components/CreateUserModal';
|
import CreateUserForm from './components/CreateUserForm';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Users Page
|
* Users Page
|
||||||
@ -54,8 +54,8 @@ const UsersPage: React.FC<IPageProps> = ({ title }) => {
|
|||||||
<Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'>
|
<Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'>
|
||||||
<UserSearch />
|
<UserSearch />
|
||||||
<Flex sx={{ gap: '10px' }}>
|
<Flex sx={{ gap: '10px' }}>
|
||||||
<CreateUserModal me={userStore.me} />
|
<CreateUserForm me={userStore.me} />
|
||||||
<Button variant='outline' colorScheme='green' leftIcon={<FaFileExcel />}>
|
<Button disabled variant='outline' colorScheme='green' leftIcon={<FaFileExcel />}>
|
||||||
Import
|
Import
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user