From 4091e68be094bba4309782586d472744e5fcf295 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sat, 25 Sep 2021 21:34:47 +0300 Subject: [PATCH] feat: editing user using form --- src/components/Header/Header.tsx | 3 +- src/contexts/index.ts | 4 +- .../CreateUserForm/CreateUserForm.tsx | 11 +- .../DeleteUserDialog/DeleteUserDialog.tsx | 11 +- .../components/EditUserForm/EditUserForm.tsx | 107 ++++++++++-------- .../EditUserForm/EditUserForm.validator.ts | 8 +- .../users/components/UserList/UserItem.tsx | 7 +- src/services/UserService.ts | 64 ++++++++++- src/stores/UserStore.ts | 47 ++++++-- src/types/index.d.ts | 2 +- 10 files changed, 189 insertions(+), 75 deletions(-) diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 8c8c237..7645df6 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -2,7 +2,7 @@ import config from 'config'; import React from 'react'; import { observer } from 'mobx-react'; 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 UserNotifications from './UserNotifications'; import NavMobile from 'components/Nav/Mobile'; @@ -41,7 +41,6 @@ const Header: React.FC = () => { {config.metaData.appName} - {config.metaData.appDescription} {isDesktop ? ( diff --git a/src/contexts/index.ts b/src/contexts/index.ts index fcde296..dd2ff8d 100644 --- a/src/contexts/index.ts +++ b/src/contexts/index.ts @@ -1,10 +1,10 @@ import { createContext } from 'react'; -import { ProfilePageViewStatusContextType, UsersPageContextType } from 'types'; +import { ProfilePageContextType, UsersPageContextType } from 'types'; import RootStore from '../stores/RootStore'; export const StoreContext = createContext(undefined); -export const ProfilePageContext = createContext({ +export const ProfilePageContext = createContext({ statusPageView: 'default', setStatusPageView: () => {}, pageData: {}, diff --git a/src/pages/users/components/CreateUserForm/CreateUserForm.tsx b/src/pages/users/components/CreateUserForm/CreateUserForm.tsx index 3082297..c94ae88 100644 --- a/src/pages/users/components/CreateUserForm/CreateUserForm.tsx +++ b/src/pages/users/components/CreateUserForm/CreateUserForm.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { Modal, ModalOverlay, @@ -38,6 +38,7 @@ import { useForm } from 'react-hook-form'; import { useRootStore } from 'hooks/useRootStore'; import { useErrorHandler } from 'react-error-boundary'; import { useToast } from '@chakra-ui/react'; +import { UsersPageContext } from 'contexts'; type CreateUserModalPropsType = { me: IMe; @@ -54,6 +55,7 @@ type CreateUserInputType = { const CreateUserModal: React.FC = ({ me }) => { const { userStore } = useRootStore(); + const { searchingRole, search } = useContext(UsersPageContext); const { isOpen, onOpen, onClose } = useDisclosure(); const { register, @@ -74,7 +76,12 @@ const CreateUserModal: React.FC = ({ me }) => { const onSubmit: SubmitHandler = async data => { try { 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 */ diff --git a/src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx b/src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx index d589a63..8cf2743 100644 --- a/src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx +++ b/src/pages/users/components/DeleteUserDialog/DeleteUserDialog.tsx @@ -25,7 +25,7 @@ type DeleteUserDialogPropsType = { const DeleteUserDialog: React.FC = ({ onClose, isOpen }) => { const { userStore } = useRootStore(); - const { deletingUser, setDeletingUser } = useContext(UsersPageContext); + const { deletingUser, setDeletingUser, searchingRole, search } = useContext(UsersPageContext); const isMountedRef = useIsMountedRef(); const [isLoading, setIsLoading] = useState(false); const cancelRef = useRef(null); @@ -38,8 +38,13 @@ const DeleteUserDialog: React.FC = ({ onClose, isOpen const onDelete = async (id: string) => { try { setIsLoading(true); - await userStore.deleteUser(id); - toast({ title: 'User deleted', status: 'info' }); + 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) { diff --git a/src/pages/users/components/EditUserForm/EditUserForm.tsx b/src/pages/users/components/EditUserForm/EditUserForm.tsx index 0a3dd77..91ebc36 100644 --- a/src/pages/users/components/EditUserForm/EditUserForm.tsx +++ b/src/pages/users/components/EditUserForm/EditUserForm.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React, { useContext, useState } from 'react'; import { Modal, ModalOverlay, @@ -12,26 +12,33 @@ import { FormHelperText, Input, Button, - Select, Flex, Text, Divider, } from '@chakra-ui/react'; -import { AddIcon } from '@chakra-ui/icons'; +import { CheckIcon } from '@chakra-ui/icons'; import { MdModeEdit } from 'react-icons/md'; import { yupResolver } from '@hookform/resolvers/yup'; +import UpdateUserSchema from './EditUserForm.validator'; /** * Types */ import { UserRoleEnum } from 'enums'; +/** + * Contexts + */ +import { UsersPageContext } from 'contexts'; + /** * Hooks */ import { SubmitHandler, useForm } from 'react-hook-form'; -import UpdateUserSchema from './EditUserForm.validator'; -import { UsersPageContext } from 'contexts'; +import { useRootStore } from 'hooks/useRootStore'; +import { useErrorHandler } from 'react-error-boundary'; +import { useToast } from '@chakra-ui/react'; +import useIsMountedRef from 'hooks/useIsMountedRef'; type UpdateUserInputType = { first_name: string; @@ -47,12 +54,14 @@ type UpdateUserFormPropsType = { }; const UpdateUserForm: React.FC = ({ onClose, isOpen }) => { + const { userStore } = useRootStore(); const { editingUser, setEditingUser } = useContext(UsersPageContext); + const [isLoading, setIsLoading] = useState(false); const { register, handleSubmit, reset, - formState: { errors }, + formState: { errors, isDirty, dirtyFields }, } = useForm({ resolver: yupResolver(UpdateUserSchema), defaultValues: { @@ -61,13 +70,44 @@ const UpdateUserForm: React.FC = ({ onClose, isOpen }) email: editingUser && editingUser.email, }, }); + const isMountedRef = useIsMountedRef(); + const toast = useToast(); + const handleError = useErrorHandler(); - const onSubmit: SubmitHandler = data => { - console.log(data); + if (editingUser === undefined) return null; + + const onSubmit: SubmitHandler = 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 ( - +
@@ -80,45 +120,28 @@ const UpdateUserForm: React.FC = ({ onClose, isOpen }) - + + Identificator + + + First name - {errors.first_name ? {errors.first_name.message} : null} + {errors.first_name && {errors.first_name.message}} Last name - {errors.last_name ? {errors.last_name.message} : null} - - - Login - - {errors.login ? {errors.login.message} : null} + {errors.last_name && {errors.last_name.message}} Email - {errors.email ? {errors.email.message} : null} - - - Role - - {errors.role ? {errors.role.message} : null} - - - Password - - - {errors.password ? errors.password.message : 'Must be at least 6 characters.'} - + {errors.email && {errors.email.message}} @@ -126,22 +149,16 @@ const UpdateUserForm: React.FC = ({ onClose, isOpen }) - +
diff --git a/src/pages/users/components/EditUserForm/EditUserForm.validator.ts b/src/pages/users/components/EditUserForm/EditUserForm.validator.ts index b3013f8..db41324 100644 --- a/src/pages/users/components/EditUserForm/EditUserForm.validator.ts +++ b/src/pages/users/components/EditUserForm/EditUserForm.validator.ts @@ -1,16 +1,10 @@ import yup from 'schema'; -import { UserRoleEnum } from 'enums'; +// 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() - .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; diff --git a/src/pages/users/components/UserList/UserItem.tsx b/src/pages/users/components/UserList/UserItem.tsx index 4b0a2dc..305aa02 100644 --- a/src/pages/users/components/UserList/UserItem.tsx +++ b/src/pages/users/components/UserList/UserItem.tsx @@ -1,6 +1,7 @@ import React from 'react'; 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'; /** @@ -48,9 +49,9 @@ const UserItem: React.FC = ({ user, onEdit, onDelete }) => { - + {user.first_name} {user.last_name} - + {user.email} diff --git a/src/services/UserService.ts b/src/services/UserService.ts index 5f18d8c..70acd5b 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -12,6 +12,7 @@ export type CreateUserParamsType = { role: UserRoleEnum; password: string; }; +export type UpdateUserParamsType = { first_name?: string; last_name?: string; email?: string }; export default class UserService { public api: AxiosInstance; @@ -20,11 +21,22 @@ export default class UserService { this.api = api; } + /** + * Fetch authorized user + * + * @returns Authorized user data + */ public async fetchMe(): Promise> { const result = await this.api.get('v1/me'); return result.data; } + /** + * Fetch all users + * + * @param params Fetch users params + * @returns Array of users + */ public async fetchAll(params?: FetchUsersParamsType): Promise> { const { page = 1, limit = 6, role, search } = params || {}; const result = await this.api.get('/v1/users', { @@ -38,6 +50,12 @@ export default class UserService { return result.data; } + /** + * Create new user + * + * @param data Data for creating user + * @returns Created user + */ public async create(data: CreateUserParamsType): Promise> { const result = await this.api.post('v1/users', { first_name: data.first_name, @@ -50,17 +68,48 @@ export default class UserService { return result.data; } + /** + * Delete user by id + * + * @param id User id + * @returns Deleted user + */ public async delete(id: string): Promise> { const result = await this.api.delete(`v1/users/${id}`); 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> { + 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> { const result = await this.api.put('v1/me/contacts', helpers.removeEmptyValues(data)); return result.data; } - public async updatePassword(oldPassword: string, newPassword: string): Promise> { + /** + * Update password of authorized user + * + * @param oldPassword Old password + * @param newPassword New password + * @returns Empty object + */ + public async updatePassword(oldPassword: string, newPassword: string): Promise> { const result = await this.api.patch('v1/me/password', { oldPassword, newPassword, @@ -68,11 +117,24 @@ export default class UserService { 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> { const result = await this.api.patch('v1/me/email', { email }); return result.data; } + /** + * Confirm updating email of authorized user + * + * @param email New email + * @param confirmationCode Code for confirmation + * @returns Updated email + */ public async updateEmailConfirm( email: string, confirmationCode: string diff --git a/src/stores/UserStore.ts b/src/stores/UserStore.ts index 7828fd1..322e20f 100644 --- a/src/stores/UserStore.ts +++ b/src/stores/UserStore.ts @@ -6,7 +6,7 @@ import * as helpers from 'helpers'; /** * Services */ -import UserService, { CreateUserParamsType, FetchUsersParamsType } from 'services/UserService'; +import UserService, { CreateUserParamsType, UpdateUserParamsType, FetchUsersParamsType } from 'services/UserService'; /** * Stores @@ -78,15 +78,37 @@ export default class UserStore { } } - public async createUser(data: CreateUserParamsType) { + public async createUser(data: CreateUserParamsType, paramsContext?: FetchUsersParamsType) { try { const result = await this.userService.create(data); 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(() => { 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 { const result = await this.userService.delete(id); console.log(`[${this.constructor.name}]: ${result.message}`, result); - runInAction(() => { - if (this.users !== null) { - this.users = this.users.filter(user => user.id !== id); - } - }); + if (this.users !== null) { + /** + * Fetch updated users data with new pagination data + */ + await this.loadUsersData(paramsContext); + } return result; } catch (error: any) { diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 7ad1ea0..60cc5d5 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -27,7 +27,7 @@ export type ProfilePageDataType = { */ confirmEmailData?: ConfirmEmailContainerDataType; }; -export type ProfilePageViewStatusContextType = { +export type ProfilePageContextType = { /** * Render current container by status */