From d898b2ab1a23df610156159b6c29fd51567e3f70 Mon Sep 17 00:00:00 2001 From: "kasia.oczkowska" Date: Fri, 13 Dec 2024 13:52:22 +0000 Subject: [PATCH] feat: introduce inline error messages in EditUser form --- packages/web/src/helpers/errors.js | 18 ++ packages/web/src/hooks/useAdminUpdateUser.js | 13 -- packages/web/src/locales/en.json | 2 + packages/web/src/pages/EditUser/index.jsx | 206 ++++++++++++++----- 4 files changed, 169 insertions(+), 70 deletions(-) create mode 100644 packages/web/src/helpers/errors.js diff --git a/packages/web/src/helpers/errors.js b/packages/web/src/helpers/errors.js new file mode 100644 index 00000000..dc73867d --- /dev/null +++ b/packages/web/src/helpers/errors.js @@ -0,0 +1,18 @@ +// Helpers to extract errors received from the API + +export const getGeneralErrorMessage = ({ error, fallbackMessage }) => { + if (!error) { + return; + } + + const errors = error?.response?.data?.errors; + const generalError = errors?.general; + + if (generalError && Array.isArray(generalError)) { + return generalError.join(' '); + } + + if (!errors) { + return error?.message || fallbackMessage; + } +}; diff --git a/packages/web/src/hooks/useAdminUpdateUser.js b/packages/web/src/hooks/useAdminUpdateUser.js index d971f252..21caa801 100644 --- a/packages/web/src/hooks/useAdminUpdateUser.js +++ b/packages/web/src/hooks/useAdminUpdateUser.js @@ -1,12 +1,8 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import api from 'helpers/api'; -import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; -import useFormatMessage from 'hooks/useFormatMessage'; export default function useAdminUpdateUser(userId) { const queryClient = useQueryClient(); - const enqueueSnackbar = useEnqueueSnackbar(); - const formatMessage = useFormatMessage(); const query = useMutation({ mutationFn: async (payload) => { @@ -19,15 +15,6 @@ export default function useAdminUpdateUser(userId) { queryKey: ['admin', 'users'], }); }, - onError: () => { - enqueueSnackbar(formatMessage('editUser.error'), { - variant: 'error', - persist: true, - SnackbarProps: { - 'data-test': 'snackbar-error', - }, - }); - }, }); return query; diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index c9422557..699bec75 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -226,6 +226,8 @@ "userForm.email": "Email", "userForm.role": "Role", "userForm.password": "Password", + "userForm.mandatoryInput": "{inputName} is required.", + "userForm.validateEmail": "Email must be valid.", "createUser.submit": "Create", "createUser.successfullyCreated": "The user has been created.", "createUser.invitationEmailInfo": "Invitation email will be sent if SMTP credentials are valid. Otherwise, you can share the invitation link manually: ", diff --git a/packages/web/src/pages/EditUser/index.jsx b/packages/web/src/pages/EditUser/index.jsx index becbb854..8bba455e 100644 --- a/packages/web/src/pages/EditUser/index.jsx +++ b/packages/web/src/pages/EditUser/index.jsx @@ -5,9 +5,12 @@ import Stack from '@mui/material/Stack'; import Chip from '@mui/material/Chip'; import Typography from '@mui/material/Typography'; import MuiTextField from '@mui/material/TextField'; +import Alert from '@mui/material/Alert'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import * as React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; import Can from 'components/Can'; import Container from 'components/Container'; @@ -20,11 +23,45 @@ import useFormatMessage from 'hooks/useFormatMessage'; import useRoles from 'hooks/useRoles.ee'; import useAdminUpdateUser from 'hooks/useAdminUpdateUser'; import useAdminUser from 'hooks/useAdminUser'; +import useCurrentUserAbility from 'hooks/useCurrentUserAbility'; +import { getGeneralErrorMessage } from 'helpers/errors'; function generateRoleOptions(roles) { return roles?.map(({ name: label, id: value }) => ({ label, value })); } +const getValidationSchema = (formatMessage, canUpdateRole) => { + const getMandatoryFieldMessage = (fieldTranslationId) => + formatMessage('userForm.mandatoryInput', { + inputName: formatMessage(fieldTranslationId), + }); + + return yup.object().shape({ + fullName: yup + .string() + .trim() + .required(getMandatoryFieldMessage('userForm.fullName')), + email: yup + .string() + .trim() + .email(formatMessage('userForm.validateEmail')) + .required(getMandatoryFieldMessage('userForm.email')), + ...(canUpdateRole + ? { + roleId: yup + .string() + .required(getMandatoryFieldMessage('userForm.role')), + } + : {}), + }); +}; + +const defaultValues = { + fullName: '', + email: '', + roleId: '', +}; + export default function EditUser() { const formatMessage = useFormatMessage(); const { userId } = useParams(); @@ -36,13 +73,15 @@ export default function EditUser() { const roles = data?.data; const enqueueSnackbar = useEnqueueSnackbar(); const navigate = useNavigate(); + const currentUserAbility = useCurrentUserAbility(); + const canUpdateRole = currentUserAbility.can('update', 'Role'); - const handleUserUpdate = async (userDataToUpdate) => { + const handleUserUpdate = async (userDataToUpdate, e, setError) => { try { await updateUser({ fullName: userDataToUpdate.fullName, email: userDataToUpdate.email, - roleId: userDataToUpdate.role?.id, + roleId: userDataToUpdate.roleId, }); enqueueSnackbar(formatMessage('editUser.successfullyUpdated'), { @@ -55,7 +94,31 @@ export default function EditUser() { navigate(URLS.USERS); } catch (error) { - throw new Error('Failed while updating!'); + const errors = error?.response?.data?.errors; + + if (errors) { + const fieldNames = Object.keys(defaultValues); + Object.entries(errors).forEach(([fieldName, fieldErrors]) => { + if (fieldNames.includes(fieldName) && Array.isArray(fieldErrors)) { + setError(fieldName, { + type: 'fieldRequestError', + message: fieldErrors.join(', '), + }); + } + }); + } + + const generalError = getGeneralErrorMessage({ + error, + fallbackMessage: formatMessage('editUser.error'), + }); + + if (generalError) { + setError('root.general', { + type: 'requestError', + message: generalError, + }); + } } }; @@ -80,65 +143,94 @@ export default function EditUser() { )} {!isUserLoading && ( -
- - - - {formatMessage('editUser.status')} - + ( + + + + {formatMessage('editUser.status')} + - - + + - - - - - - ( - - )} - loading={isRolesLoading} + error={!!errors?.fullName} + helperText={errors?.fullName?.message} /> - - - {formatMessage('editUser.submit')} - - - + + + + ( + + )} + loading={isRolesLoading} + showHelperText={false} + /> + + + {errors?.root?.general && ( + + {errors?.root?.general?.message} + + )} + + + {formatMessage('editUser.submit')} + +
+ )} + /> )}