diff --git a/packages/e2e-tests/fixtures/admin-setup-page.js b/packages/e2e-tests/fixtures/admin-setup-page.js index 704a9caf..6d5b85c2 100644 --- a/packages/e2e-tests/fixtures/admin-setup-page.js +++ b/packages/e2e-tests/fixtures/admin-setup-page.js @@ -1,4 +1,4 @@ -import { BasePage } from "./base-page"; +import { BasePage } from './base-page'; const { faker } = require('@faker-js/faker'); const { expect } = require('@playwright/test'); @@ -6,16 +6,18 @@ export class AdminSetupPage extends BasePage { path = '/installation'; /** - * @param {import('@playwright/test').Page} page - */ + * @param {import('@playwright/test').Page} page + */ constructor(page) { super(page); this.fullNameTextField = this.page.getByTestId('fullName-text-field'); this.emailTextField = this.page.getByTestId('email-text-field'); this.passwordTextField = this.page.getByTestId('password-text-field'); - this.repeatPasswordTextField = this.page.getByTestId('repeat-password-text-field'); - this.createAdminButton = this.page.getByTestId('signUp-button'); + this.repeatPasswordTextField = this.page.getByTestId( + 'repeat-password-text-field' + ); + this.createAdminButton = this.page.getByTestId('installation-button'); this.invalidFields = this.page.locator('p.Mui-error'); this.successAlert = this.page.getByTestId('success-alert'); } @@ -46,7 +48,7 @@ export class AdminSetupPage extends BasePage { await this.repeatPasswordTextField.fill(testUser.wronglyRepeatedPassword); } - async submitAdminForm() { + async submitAdminForm() { await this.createAdminButton.click(); } @@ -59,7 +61,10 @@ export class AdminSetupPage extends BasePage { } async expectSuccessMessageToContainLoginLink() { - await expect(await this.successAlert.locator('a')).toHaveAttribute('href', '/login'); + await expect(await this.successAlert.locator('a')).toHaveAttribute( + 'href', + '/login' + ); } generateUser() { @@ -69,7 +74,7 @@ export class AdminSetupPage extends BasePage { fullName: faker.person.fullName(), email: faker.internet.email(), password: faker.internet.password(), - wronglyRepeatedPassword: faker.internet.password() + wronglyRepeatedPassword: faker.internet.password(), }; } -}; +} diff --git a/packages/web/src/components/Form/index.jsx b/packages/web/src/components/Form/index.jsx index 614873f7..d501e5a8 100644 --- a/packages/web/src/components/Form/index.jsx +++ b/packages/web/src/components/Form/index.jsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { FormProvider, useForm, useWatch } from 'react-hook-form'; import PropTypes from 'prop-types'; import isEqual from 'lodash/isEqual'; +import useFormatMessage from 'hooks/useFormatMessage'; const noop = () => null; @@ -18,6 +19,8 @@ function Form(props) { ...formProps } = props; + const formatMessage = useFormatMessage(); + const methods = useForm({ defaultValues, reValidateMode, @@ -25,6 +28,8 @@ function Form(props) { mode, }); + const { setError } = methods; + const form = useWatch({ control: methods.control }); const prevDefaultValues = React.useRef(defaultValues); @@ -44,9 +49,53 @@ function Form(props) { } }, [defaultValues]); + const handleErrors = React.useCallback( + function (errors) { + if (!errors) return; + + let shouldSetGenericGeneralError = true; + const fieldNames = Object.keys(defaultValues); + + Object.entries(errors).forEach(([fieldName, fieldErrors]) => { + if (fieldNames.includes(fieldName) && Array.isArray(fieldErrors)) { + shouldSetGenericGeneralError = false; + setError(fieldName, { + type: 'fieldRequestError', + message: fieldErrors.join(', '), + }); + } + }); + + // in case of general errors + if (Array.isArray(errors.general)) { + for (const error of errors.general) { + shouldSetGenericGeneralError = false; + setError('root.general', { type: 'requestError', message: error }); + } + } + + if (shouldSetGenericGeneralError) { + setError('root.general', { + type: 'requestError', + message: formatMessage('form.genericError'), + }); + } + }, + [defaultValues, formatMessage, setError], + ); + return ( -
+ { + try { + return await onSubmit?.(data); + } catch (errors) { + handleErrors(errors); + } + })} + {...formProps} + > {render ? render(methods) : children}
diff --git a/packages/web/src/components/InstallationForm/index.jsx b/packages/web/src/components/InstallationForm/index.jsx index 80d66b6c..bba4d314 100644 --- a/packages/web/src/components/InstallationForm/index.jsx +++ b/packages/web/src/components/InstallationForm/index.jsx @@ -2,11 +2,10 @@ import * as React from 'react'; import { Link as RouterLink } from 'react-router-dom'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; -import { Alert } from '@mui/material'; +import Alert from '@mui/material/Alert'; import LoadingButton from '@mui/lab/LoadingButton'; import * as yup from 'yup'; import { yupResolver } from '@hookform/resolvers/yup'; -import { enqueueSnackbar } from 'notistack'; import { useQueryClient } from '@tanstack/react-query'; import Link from '@mui/material/Link'; @@ -16,21 +15,41 @@ import * as URLS from 'config/urls'; import Form from 'components/Form'; import TextField from 'components/TextField'; -const validationSchema = yup.object().shape({ - fullName: yup.string().trim().required('installationForm.mandatoryInput'), - email: yup - .string() - .trim() - .email('installationForm.validateEmail') - .required('installationForm.mandatoryInput'), - password: yup.string().required('installationForm.mandatoryInput'), - confirmPassword: yup - .string() - .required('installationForm.mandatoryInput') - .oneOf([yup.ref('password')], 'installationForm.passwordsMustMatch'), -}); +const getValidationSchema = (formatMessage) => { + const getMandatoryInputMessage = (inputNameId) => + formatMessage('installationForm.mandatoryInput', { + inputName: formatMessage(inputNameId), + }); -const initialValues = { + return yup.object().shape({ + fullName: yup + .string() + .trim() + .required( + getMandatoryInputMessage('installationForm.fullNameFieldLabel'), + ), + email: yup + .string() + .trim() + .required(getMandatoryInputMessage('installationForm.emailFieldLabel')) + .email(formatMessage('installationForm.validateEmail')), + password: yup + .string() + .required(getMandatoryInputMessage('installationForm.passwordFieldLabel')) + .min(6, formatMessage('installationForm.passwordMinLength')), + confirmPassword: yup + .string() + .required( + getMandatoryInputMessage('installationForm.confirmPasswordFieldLabel'), + ) + .oneOf( + [yup.ref('password')], + formatMessage('installationForm.passwordsMustMatch'), + ), + }); +}; + +const defaultValues = { fullName: '', email: '', password: '', @@ -39,7 +58,7 @@ const initialValues = { function InstallationForm() { const formatMessage = useFormatMessage(); - const install = useInstallation(); + const { mutateAsync: install, isSuccess, isPending } = useInstallation(); const queryClient = useQueryClient(); const handleOnRedirect = () => { @@ -48,21 +67,16 @@ function InstallationForm() { }); }; - const handleSubmit = async (values) => { - const { fullName, email, password } = values; + const handleSubmit = async ({ fullName, email, password }) => { try { - await install.mutateAsync({ + await install({ fullName, email, password, }); } catch (error) { - enqueueSnackbar( - error?.message || formatMessage('installationForm.error'), - { - variant: 'error', - }, - ); + const errors = error?.response?.data?.errors; + throw errors || error; } }; @@ -82,11 +96,13 @@ function InstallationForm() { {formatMessage('installationForm.title')}
( + render={({ formState: { errors } }) => ( <> + + + + {errors?.root?.general && ( + + {errors.root.general.message} + + )} + + {isSuccess && ( + + {formatMessage('installationForm.success', { + link: (str) => ( + + {str} + + ), + })} + + )} {formatMessage('installationForm.submit')} )} /> - {install.isSuccess && ( - - {formatMessage('installationForm.success', { - link: (str) => ( - - {str} - - ), - })} - - )} ); } diff --git a/packages/web/src/components/SignUpForm/index.ee.jsx b/packages/web/src/components/SignUpForm/index.ee.jsx index e931d394..a7daa326 100644 --- a/packages/web/src/components/SignUpForm/index.ee.jsx +++ b/packages/web/src/components/SignUpForm/index.ee.jsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import LoadingButton from '@mui/lab/LoadingButton'; +import Alert from '@mui/material/Alert'; import * as yup from 'yup'; import { yupResolver } from '@hookform/resolvers/yup'; @@ -12,24 +13,41 @@ import Form from 'components/Form'; import TextField from 'components/TextField'; import useFormatMessage from 'hooks/useFormatMessage'; import useCreateAccessToken from 'hooks/useCreateAccessToken'; -import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import useRegisterUser from 'hooks/useRegisterUser'; -const validationSchema = yup.object().shape({ - fullName: yup.string().trim().required('signupForm.mandatoryInput'), - email: yup - .string() - .trim() - .email('signupForm.validateEmail') - .required('signupForm.mandatoryInput'), - password: yup.string().required('signupForm.mandatoryInput'), - confirmPassword: yup - .string() - .required('signupForm.mandatoryInput') - .oneOf([yup.ref('password')], 'signupForm.passwordsMustMatch'), -}); +const getValidationSchema = (formatMessage) => { + const getMandatoryInputMessage = (inputNameId) => + formatMessage('signupForm.mandatoryInput', { + inputName: formatMessage(inputNameId), + }); -const initialValues = { + return yup.object().shape({ + fullName: yup + .string() + .trim() + .required(getMandatoryInputMessage('signupForm.fullNameFieldLabel')), + email: yup + .string() + .trim() + .required(getMandatoryInputMessage('signupForm.emailFieldLabel')) + .email(formatMessage('signupForm.validateEmail')), + password: yup + .string() + .required(getMandatoryInputMessage('signupForm.passwordFieldLabel')) + .min(6, formatMessage('signupForm.passwordMinLength')), + confirmPassword: yup + .string() + .required( + getMandatoryInputMessage('signupForm.confirmPasswordFieldLabel'), + ) + .oneOf( + [yup.ref('password')], + formatMessage('signupForm.passwordsMustMatch'), + ), + }); +}; + +const defaultValues = { fullName: '', email: '', password: '', @@ -40,7 +58,6 @@ function SignUpForm() { const navigate = useNavigate(); const authentication = useAuthentication(); const formatMessage = useFormatMessage(); - const enqueueSnackbar = useEnqueueSnackbar(); const { mutateAsync: registerUser, isPending: isRegisterUserPending } = useRegisterUser(); const { mutateAsync: createAccessToken, isPending: loginLoading } = @@ -67,27 +84,8 @@ function SignUpForm() { const { token } = data; authentication.updateToken(token); } catch (error) { - const errors = error?.response?.data?.errors - ? Object.values(error.response.data.errors) - : []; - - if (errors.length) { - for (const [error] of errors) { - enqueueSnackbar(error, { - variant: 'error', - SnackbarProps: { - 'data-test': 'snackbar-sign-up-error', - }, - }); - } - } else { - enqueueSnackbar(error?.message || formatMessage('signupForm.error'), { - variant: 'error', - SnackbarProps: { - 'data-test': 'snackbar-sign-up-error', - }, - }); - } + const errors = error?.response?.data?.errors; + throw errors || error; } }; @@ -108,11 +106,13 @@ function SignUpForm() { ( + render={({ formState: { errors } }) => ( <> + {errors?.root?.general && ( + + {errors.root.general.message} + + )} + formatMessage({ id }, values); + + const customFormatMessage = React.useCallback( + (id, values = {}) => formatMessage({ id }, values), + [formatMessage], + ); + + return customFormatMessage; } diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index a4c0dd2b..c9422557 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -130,6 +130,7 @@ "webhookUrlInfo.description": "You'll need to configure your application with this webhook URL.", "webhookUrlInfo.helperText": "We've generated a custom webhook URL for you to send requests to. Learn more about webhooks.", "webhookUrlInfo.copy": "Copy", + "form.genericError": "Something went wrong. Please try again.", "installationForm.title": "Installation", "installationForm.fullNameFieldLabel": "Full name", "installationForm.emailFieldLabel": "Email", @@ -138,9 +139,9 @@ "installationForm.submit": "Create admin", "installationForm.validateEmail": "Email must be valid.", "installationForm.passwordsMustMatch": "Passwords must match.", + "installationForm.passwordMinLength": "Password must be at least 6 characters long.", "installationForm.mandatoryInput": "{inputName} is required.", "installationForm.success": "The admin account has been created, and thus, the installation has been completed. You can now log in here.", - "installationForm.error": "Something went wrong. Please try again.", "signupForm.title": "Sign up", "signupForm.fullNameFieldLabel": "Full name", "signupForm.emailFieldLabel": "Email", @@ -149,8 +150,8 @@ "signupForm.submit": "Sign up", "signupForm.validateEmail": "Email must be valid.", "signupForm.passwordsMustMatch": "Passwords must match.", + "signupForm.passwordMinLength": "Password must be at least 6 characters long.", "signupForm.mandatoryInput": "{inputName} is required.", - "signupForm.error": "Something went wrong. Please try again.", "loginForm.title": "Login", "loginForm.emailFieldLabel": "Email", "loginForm.passwordFieldLabel": "Password",