feat: editing user using form

This commit is contained in:
Sergey Yarkov 2021-09-25 21:34:47 +03:00
parent ee0bdb0e4a
commit 4091e68be0
10 changed files with 189 additions and 75 deletions

View File

@ -2,7 +2,7 @@ import config from 'config';
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { useRootStore } from 'hooks/useRootStore'; import { useRootStore } from 'hooks/useRootStore';
import { Flex, Box, Heading, Text, useColorMode } from '@chakra-ui/react'; import { Flex, Box, Heading, useColorMode } from '@chakra-ui/react';
import UserInfo from './UserInfo'; import UserInfo from './UserInfo';
import UserNotifications from './UserNotifications'; import UserNotifications from './UserNotifications';
import NavMobile from 'components/Nav/Mobile'; import NavMobile from 'components/Nav/Mobile';
@ -41,7 +41,6 @@ const Header: React.FC = () => {
<Heading as='p' fontSize='2xl' lineHeight='1'> <Heading as='p' fontSize='2xl' lineHeight='1'>
{config.metaData.appName} {config.metaData.appName}
</Heading> </Heading>
<Text as='small'>{config.metaData.appDescription}</Text>
</Box> </Box>
</Flex> </Flex>
{isDesktop ? ( {isDesktop ? (

View File

@ -1,10 +1,10 @@
import { createContext } from 'react'; import { createContext } from 'react';
import { ProfilePageViewStatusContextType, UsersPageContextType } from 'types'; import { ProfilePageContextType, UsersPageContextType } from 'types';
import RootStore from '../stores/RootStore'; import RootStore from '../stores/RootStore';
export const StoreContext = createContext<RootStore | undefined>(undefined); export const StoreContext = createContext<RootStore | undefined>(undefined);
export const ProfilePageContext = createContext<ProfilePageViewStatusContextType>({ export const ProfilePageContext = createContext<ProfilePageContextType>({
statusPageView: 'default', statusPageView: 'default',
setStatusPageView: () => {}, setStatusPageView: () => {},
pageData: {}, pageData: {},

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useContext, useState } from 'react';
import { import {
Modal, Modal,
ModalOverlay, ModalOverlay,
@ -38,6 +38,7 @@ import { useForm } from 'react-hook-form';
import { useRootStore } from 'hooks/useRootStore'; import { useRootStore } from 'hooks/useRootStore';
import { useErrorHandler } from 'react-error-boundary'; import { useErrorHandler } from 'react-error-boundary';
import { useToast } from '@chakra-ui/react'; import { useToast } from '@chakra-ui/react';
import { UsersPageContext } from 'contexts';
type CreateUserModalPropsType = { type CreateUserModalPropsType = {
me: IMe; me: IMe;
@ -54,6 +55,7 @@ type CreateUserInputType = {
const CreateUserModal: React.FC<CreateUserModalPropsType> = ({ me }) => { const CreateUserModal: React.FC<CreateUserModalPropsType> = ({ me }) => {
const { userStore } = useRootStore(); const { userStore } = useRootStore();
const { searchingRole, search } = useContext(UsersPageContext);
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const { const {
register, register,
@ -74,7 +76,12 @@ const CreateUserModal: React.FC<CreateUserModalPropsType> = ({ me }) => {
const onSubmit: SubmitHandler<CreateUserInputType> = async data => { const onSubmit: SubmitHandler<CreateUserInputType> = async data => {
try { try {
setLoading(true); setLoading(true);
await userStore.createUser(data); await userStore.createUser(data, {
page: userStore.pagination?.current_page,
limit: userStore.pagination?.per_page,
role: searchingRole,
search,
});
/** /**
* Clear form state * Clear form state
*/ */

View File

@ -25,7 +25,7 @@ type DeleteUserDialogPropsType = {
const DeleteUserDialog: React.FC<DeleteUserDialogPropsType> = ({ onClose, isOpen }) => { const DeleteUserDialog: React.FC<DeleteUserDialogPropsType> = ({ onClose, isOpen }) => {
const { userStore } = useRootStore(); const { userStore } = useRootStore();
const { deletingUser, setDeletingUser } = useContext(UsersPageContext); const { deletingUser, setDeletingUser, searchingRole, search } = useContext(UsersPageContext);
const isMountedRef = useIsMountedRef(); const isMountedRef = useIsMountedRef();
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const cancelRef = useRef<HTMLButtonElement | null>(null); const cancelRef = useRef<HTMLButtonElement | null>(null);
@ -38,8 +38,13 @@ const DeleteUserDialog: React.FC<DeleteUserDialogPropsType> = ({ onClose, isOpen
const onDelete = async (id: string) => { const onDelete = async (id: string) => {
try { try {
setIsLoading(true); setIsLoading(true);
await userStore.deleteUser(id); await userStore.deleteUser(id, {
toast({ title: 'User deleted', status: 'info' }); page: userStore.pagination?.current_page,
limit: userStore.pagination?.per_page,
role: searchingRole,
search,
});
toast({ title: 'User deleted.', status: 'info' });
onClose(); onClose();
setDeletingUser(undefined); setDeletingUser(undefined);
} catch (error: any) { } catch (error: any) {

View File

@ -1,4 +1,4 @@
import React, { useContext } from 'react'; import React, { useContext, useState } from 'react';
import { import {
Modal, Modal,
ModalOverlay, ModalOverlay,
@ -12,26 +12,33 @@ import {
FormHelperText, FormHelperText,
Input, Input,
Button, Button,
Select,
Flex, Flex,
Text, Text,
Divider, Divider,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons'; import { CheckIcon } from '@chakra-ui/icons';
import { MdModeEdit } from 'react-icons/md'; import { MdModeEdit } from 'react-icons/md';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import UpdateUserSchema from './EditUserForm.validator';
/** /**
* Types * Types
*/ */
import { UserRoleEnum } from 'enums'; import { UserRoleEnum } from 'enums';
/**
* Contexts
*/
import { UsersPageContext } from 'contexts';
/** /**
* Hooks * Hooks
*/ */
import { SubmitHandler, useForm } from 'react-hook-form'; import { SubmitHandler, useForm } from 'react-hook-form';
import UpdateUserSchema from './EditUserForm.validator'; import { useRootStore } from 'hooks/useRootStore';
import { UsersPageContext } from 'contexts'; import { useErrorHandler } from 'react-error-boundary';
import { useToast } from '@chakra-ui/react';
import useIsMountedRef from 'hooks/useIsMountedRef';
type UpdateUserInputType = { type UpdateUserInputType = {
first_name: string; first_name: string;
@ -47,12 +54,14 @@ type UpdateUserFormPropsType = {
}; };
const UpdateUserForm: React.FC<UpdateUserFormPropsType> = ({ onClose, isOpen }) => { const UpdateUserForm: React.FC<UpdateUserFormPropsType> = ({ onClose, isOpen }) => {
const { userStore } = useRootStore();
const { editingUser, setEditingUser } = useContext(UsersPageContext); const { editingUser, setEditingUser } = useContext(UsersPageContext);
const [isLoading, setIsLoading] = useState<boolean>(false);
const { const {
register, register,
handleSubmit, handleSubmit,
reset, reset,
formState: { errors }, formState: { errors, isDirty, dirtyFields },
} = useForm<UpdateUserInputType>({ } = useForm<UpdateUserInputType>({
resolver: yupResolver(UpdateUserSchema), resolver: yupResolver(UpdateUserSchema),
defaultValues: { defaultValues: {
@ -61,13 +70,44 @@ const UpdateUserForm: React.FC<UpdateUserFormPropsType> = ({ onClose, isOpen })
email: editingUser && editingUser.email, email: editingUser && editingUser.email,
}, },
}); });
const isMountedRef = useIsMountedRef();
const toast = useToast();
const handleError = useErrorHandler();
const onSubmit: SubmitHandler<UpdateUserInputType> = data => { if (editingUser === undefined) return null;
console.log(data);
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);
toast({ title: 'User updated.', status: 'info' });
onCloseModal();
} catch (error: any) {
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' });
}
} else {
handleError(error);
}
} finally {
if (isMountedRef.current) {
setIsLoading(false);
}
}
};
const onCloseModal = () => {
reset({});
setEditingUser(undefined);
onClose();
}; };
return ( return (
<Modal isOpen={isOpen} onClose={onClose}> <Modal isOpen={isOpen} onClose={onCloseModal}>
<ModalOverlay /> <ModalOverlay />
<ModalContent> <ModalContent>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
@ -80,45 +120,28 @@ const UpdateUserForm: React.FC<UpdateUserFormPropsType> = ({ onClose, isOpen })
<ModalCloseButton /> <ModalCloseButton />
<Divider /> <Divider />
<ModalBody pb={6}> <ModalBody pb={6}>
<FormControl> <FormControl isDisabled>
<FormLabel>Identificator</FormLabel>
<Input value={editingUser?.id} />
</FormControl>
<FormControl mt={4}>
<FormLabel>First name</FormLabel> <FormLabel>First name</FormLabel>
<Input <Input
placeholder='First name' placeholder='First name'
{...register('first_name')} {...register('first_name')}
isInvalid={errors.first_name ? true : false} isInvalid={errors.first_name ? true : false}
/> />
{errors.first_name ? <FormHelperText color='red.500'>{errors.first_name.message}</FormHelperText> : null} {errors.first_name && <FormHelperText color='red.500'>{errors.first_name.message}</FormHelperText>}
</FormControl> </FormControl>
<FormControl mt={4}> <FormControl mt={4}>
<FormLabel>Last name</FormLabel> <FormLabel>Last name</FormLabel>
<Input placeholder='Last name' {...register('last_name')} isInvalid={errors.last_name ? true : false} /> <Input placeholder='Last name' {...register('last_name')} isInvalid={errors.last_name ? true : false} />
{errors.last_name ? <FormHelperText color='red.500'>{errors.last_name.message}</FormHelperText> : null} {errors.last_name && <FormHelperText color='red.500'>{errors.last_name.message}</FormHelperText>}
</FormControl>
<FormControl mt={4}>
<FormLabel>Login</FormLabel>
<Input placeholder='e.g student893' {...register('login')} isInvalid={errors.login ? true : false} />
{errors.login ? <FormHelperText color='red.500'>{errors.login.message}</FormHelperText> : null}
</FormControl> </FormControl>
<FormControl mt={4}> <FormControl mt={4}>
<FormLabel>Email</FormLabel> <FormLabel>Email</FormLabel>
<Input placeholder='example@email.com' {...register('email')} isInvalid={errors.email ? true : false} /> <Input placeholder='example@email.com' {...register('email')} isInvalid={errors.email ? true : false} />
{errors.email ? <FormHelperText color='red.500'>{errors.email.message}</FormHelperText> : null} {errors.email && <FormHelperText color='red.500'>{errors.email.message}</FormHelperText>}
</FormControl>
<FormControl mt={4}>
<FormLabel>Role</FormLabel>
<Select w='full' mr='2' {...register('role')} isInvalid={errors.role ? true : false}>
<option value={UserRoleEnum.STUDENT}>Student</option>
<option value={UserRoleEnum.TEACHER}>Teacher</option>
<option value={UserRoleEnum.ADMIN}>Administrator</option>
</Select>
{errors.role ? <FormHelperText color='red.500'>{errors.role.message}</FormHelperText> : null}
</FormControl>
<FormControl mt={4}>
<FormLabel>Password</FormLabel>
<Input placeholder='******' {...register('password')} isInvalid={errors.password ? true : false} />
<FormHelperText color={errors.password ? 'red' : 'gray.500'}>
{errors.password ? errors.password.message : 'Must be at least 6 characters.'}
</FormHelperText>
</FormControl> </FormControl>
</ModalBody> </ModalBody>
@ -126,22 +149,16 @@ const UpdateUserForm: React.FC<UpdateUserFormPropsType> = ({ onClose, isOpen })
<Button <Button
type='submit' type='submit'
variant='outline' variant='outline'
mr={1} mr={3}
colorScheme='blue' colorScheme='blue'
isLoading={isLoading}
loadingText='Saving...' loadingText='Saving...'
leftIcon={<AddIcon />} isDisabled={!isDirty}
leftIcon={<CheckIcon />}
> >
Save Save
</Button> </Button>
<Button <Button onClick={onCloseModal}>Cancel</Button>
onClick={() => {
reset({});
setEditingUser(undefined);
onClose();
}}
>
Cancel
</Button>
</ModalFooter> </ModalFooter>
</form> </form>
</ModalContent> </ModalContent>

View File

@ -1,16 +1,10 @@
import yup from 'schema'; import yup from 'schema';
import { UserRoleEnum } from 'enums'; // import { UserRoleEnum } from 'enums';
const UpdateUserSchema = yup.object().shape({ const UpdateUserSchema = yup.object().shape({
first_name: yup.string().required('First field name is required'), first_name: yup.string().required('First field name is required'),
last_name: yup.string().required('Last 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'), 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; export default UpdateUserSchema;

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import * as helpres from 'helpers'; import * as helpres from 'helpers';
import { Box, Flex, Avatar, Text, IconButton } from '@chakra-ui/react'; import { Box, Flex, Avatar, Text, IconButton, Link } from '@chakra-ui/react';
import { Link as ReactRouterLink } from 'react-router-dom';
import UserBadge from 'components/UserBadge'; import UserBadge from 'components/UserBadge';
/** /**
@ -48,9 +49,9 @@ const UserItem: React.FC<UserItemPropsType> = ({ user, onEdit, onDelete }) => {
<Flex alignItems='center' flexBasis='230px'> <Flex alignItems='center' flexBasis='230px'>
<Avatar name={`${user.first_name} ${user.last_name}`} size='sm' mr='3' /> <Avatar name={`${user.first_name} ${user.last_name}`} size='sm' mr='3' />
<Box> <Box>
<Text fontSize='md' fontWeight='medium'> <Link as={ReactRouterLink} to={`/user/${user.id}`} fontSize='md' fontWeight='medium'>
{user.first_name}&nbsp;{user.last_name} {user.first_name}&nbsp;{user.last_name}
</Text> </Link>
<Text fontSize='sm'>{user.email}</Text> <Text fontSize='sm'>{user.email}</Text>
</Box> </Box>
</Flex> </Flex>

View File

@ -12,6 +12,7 @@ export type CreateUserParamsType = {
role: UserRoleEnum; role: UserRoleEnum;
password: string; password: string;
}; };
export type UpdateUserParamsType = { first_name?: string; last_name?: string; email?: string };
export default class UserService { export default class UserService {
public api: AxiosInstance; public api: AxiosInstance;
@ -20,11 +21,22 @@ export default class UserService {
this.api = api; this.api = api;
} }
/**
* Fetch authorized user
*
* @returns Authorized user data
*/
public async fetchMe(): Promise<IApiRespose<IMe>> { public async fetchMe(): Promise<IApiRespose<IMe>> {
const result = await this.api.get('v1/me'); const result = await this.api.get('v1/me');
return result.data; return result.data;
} }
/**
* Fetch all users
*
* @param params Fetch users params
* @returns Array of users
*/
public async fetchAll(params?: FetchUsersParamsType): Promise<IApiRespose<IUser[]>> { public async fetchAll(params?: FetchUsersParamsType): Promise<IApiRespose<IUser[]>> {
const { page = 1, limit = 6, role, search } = params || {}; const { page = 1, limit = 6, role, search } = params || {};
const result = await this.api.get('/v1/users', { const result = await this.api.get('/v1/users', {
@ -38,6 +50,12 @@ export default class UserService {
return result.data; return result.data;
} }
/**
* Create new user
*
* @param data Data for creating user
* @returns Created user
*/
public async create(data: CreateUserParamsType): Promise<IApiRespose<IUser>> { public async create(data: CreateUserParamsType): Promise<IApiRespose<IUser>> {
const result = await this.api.post('v1/users', { const result = await this.api.post('v1/users', {
first_name: data.first_name, first_name: data.first_name,
@ -50,17 +68,48 @@ export default class UserService {
return result.data; return result.data;
} }
/**
* Delete user by id
*
* @param id User id
* @returns Deleted user
*/
public async delete(id: string): Promise<IApiRespose<IUser>> { public async delete(id: string): Promise<IApiRespose<IUser>> {
const result = await this.api.delete(`v1/users/${id}`); const result = await this.api.delete(`v1/users/${id}`);
return result.data; return result.data;
} }
/**
* Update user by id
*
* @param id User id
* @param data Data to update
* @returns Updated user
*/
public async update(id: string, data: UpdateUserParamsType): Promise<IApiRespose<IUser>> {
const result = await this.api.patch(`v1/users/${id}`, data);
return result.data;
}
/**
* Update contacts of authorized user
*
* @param data New values for contacts
* @returns Updated contacts
*/
public async updateContacts(data: IUserContacts): Promise<IApiRespose<IUserContacts>> { public async updateContacts(data: IUserContacts): Promise<IApiRespose<IUserContacts>> {
const result = await this.api.put('v1/me/contacts', helpers.removeEmptyValues(data)); const result = await this.api.put('v1/me/contacts', helpers.removeEmptyValues(data));
return result.data; return result.data;
} }
public async updatePassword(oldPassword: string, newPassword: string): Promise<IApiRespose<any>> { /**
* Update password of authorized user
*
* @param oldPassword Old password
* @param newPassword New password
* @returns Empty object
*/
public async updatePassword(oldPassword: string, newPassword: string): Promise<IApiRespose<{}>> {
const result = await this.api.patch('v1/me/password', { const result = await this.api.patch('v1/me/password', {
oldPassword, oldPassword,
newPassword, newPassword,
@ -68,11 +117,24 @@ export default class UserService {
return result.data; return result.data;
} }
/**
* Request on update email of authorized user
*
* @param email New email
* @returns Expiration in seconds for confirmation code
*/
public async updateEmail(email: string): Promise<IApiRespose<{ expired_seconds: number }>> { public async updateEmail(email: string): Promise<IApiRespose<{ expired_seconds: number }>> {
const result = await this.api.patch('v1/me/email', { email }); const result = await this.api.patch('v1/me/email', { email });
return result.data; return result.data;
} }
/**
* Confirm updating email of authorized user
*
* @param email New email
* @param confirmationCode Code for confirmation
* @returns Updated email
*/
public async updateEmailConfirm( public async updateEmailConfirm(
email: string, email: string,
confirmationCode: string confirmationCode: string

View File

@ -6,7 +6,7 @@ import * as helpers from 'helpers';
/** /**
* Services * Services
*/ */
import UserService, { CreateUserParamsType, FetchUsersParamsType } from 'services/UserService'; import UserService, { CreateUserParamsType, UpdateUserParamsType, FetchUsersParamsType } from 'services/UserService';
/** /**
* Stores * Stores
@ -78,15 +78,37 @@ export default class UserStore {
} }
} }
public async createUser(data: CreateUserParamsType) { public async createUser(data: CreateUserParamsType, paramsContext?: FetchUsersParamsType) {
try { try {
const result = await this.userService.create(data); const result = await this.userService.create(data);
console.log(`[${this.constructor.name}]: ${result.message}`, result); console.log(`[${this.constructor.name}]: ${result.message}`, result);
if (this.users !== null) {
/**
* Fetch updated users data with new pagination data
*/
await this.loadUsersData(paramsContext);
}
return result;
} catch (error: any) {
throw error;
}
}
public async updateUser(id: string, params: UpdateUserParamsType) {
try {
const result = await this.userService.update(id, params);
console.log(`[${this.constructor.name}]: ${result.message}`, result);
runInAction(() => { runInAction(() => {
if (this.users !== null) { if (this.users !== null) {
this.users.unshift(result.data); const userIndex = this.users.findIndex(user => user.id === id);
if (userIndex !== -1) {
this.users[userIndex] = result.data;
}
} }
}); });
@ -96,17 +118,24 @@ export default class UserStore {
} }
} }
public async deleteUser(id: string) { /**
*
* @param id User id
* @param paramsContext Params context to refresh users store data
* @returns
*/
public async deleteUser(id: string, paramsContext?: FetchUsersParamsType) {
try { try {
const result = await this.userService.delete(id); const result = await this.userService.delete(id);
console.log(`[${this.constructor.name}]: ${result.message}`, result); console.log(`[${this.constructor.name}]: ${result.message}`, result);
runInAction(() => { if (this.users !== null) {
if (this.users !== null) { /**
this.users = this.users.filter(user => user.id !== id); * Fetch updated users data with new pagination data
} */
}); await this.loadUsersData(paramsContext);
}
return result; return result;
} catch (error: any) { } catch (error: any) {

View File

@ -27,7 +27,7 @@ export type ProfilePageDataType = {
*/ */
confirmEmailData?: ConfirmEmailContainerDataType; confirmEmailData?: ConfirmEmailContainerDataType;
}; };
export type ProfilePageViewStatusContextType = { export type ProfilePageContextType = {
/** /**
* Render current container by status * Render current container by status
*/ */