Skip to content

Commit

Permalink
🥅(frontend) improve error catching in forms
Browse files Browse the repository at this point in the history
- rename CreateMailboxForm into ModalCreateMailbox,
and useCreateMailDomain into useAddMailDomain
- use useAPIError hook in ModalCreateMailbox.tsx and ModalAddMailDomain
- update translations and tests (include removal of e2e test able
to be asserted by component tests)
  • Loading branch information
daproclaima committed Sep 5, 2024
1 parent 293637d commit 6fd0d92
Show file tree
Hide file tree
Showing 12 changed files with 684 additions and 399 deletions.
11 changes: 7 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ and this project adheres to

## [Unreleased]

### Fixed

🐛(frontend) fix mai domain addition submission #355

### Added
🥅(frontend) improve api error handling #355

## [1.0.2] - 2024-08-30

### Added
Expand All @@ -19,10 +26,6 @@ and this project adheres to

- 👽️(mailboxes) fix mailbox creation after dimail api improvement (#360)

### Fixed

- 🐛(frontend) user can submit form to add mail domain by pressing "Enter" key

## [1.0.1] - 2024-08-19

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import { MailDomain } from '@/features/mail-domains';

import { KEY_LIST_MAIL_DOMAIN } from './useMailDomains';

export const createMailDomain = async (name: string): Promise<MailDomain> => {
export interface AddMailDomainParams {
name: string;
}

export const createMailDomain = async (
name: AddMailDomainParams['name'],
): Promise<MailDomain> => {
const response = await fetchAPI(`mail-domains/`, {
method: 'POST',
body: JSON.stringify({
Expand All @@ -23,11 +29,11 @@ export const createMailDomain = async (name: string): Promise<MailDomain> => {
return response.json() as Promise<MailDomain>;
};

export function useCreateMailDomain({
export const useAddMailDomain = ({
onSuccess,
}: {
onSuccess: (data: MailDomain) => void;
}) {
}) => {
const queryClient = useQueryClient();
return useMutation<MailDomain, APIError, string>({
mutationFn: createMailDomain,
Expand All @@ -38,4 +44,4 @@ export function useCreateMailDomain({
onSuccess(data);
},
});
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export const createMailbox = async ({
});

if (!response.ok) {
// TODO: extend errorCauses to return the name of the invalid field names to highlight in the form?
throw new APIError(
'Failed to create the mailbox',
await errorCauses(response),
Expand All @@ -40,7 +39,7 @@ type UseCreateMailboxParams = { mailDomainSlug: string } & UseMutationOptions<
CreateMailboxParams
>;

export function useCreateMailbox(options: UseCreateMailboxParams) {
export const useCreateMailbox = (options: UseCreateMailboxParams) => {
const queryClient = useQueryClient();
return useMutation<void, APIError, CreateMailboxParams>({
mutationFn: createMailbox,
Expand All @@ -61,4 +60,4 @@ export function useCreateMailbox(options: UseCreateMailboxParams) {
}
},
});
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { default as MailDomainsLogo } from '../assets/mail-domains-logo.svg';
import { PAGE_SIZE } from '../conf';
import { MailDomain, MailDomainMailbox } from '../types';

import { CreateMailboxForm } from './forms/CreateMailboxForm';
import { ModalCreateMailbox } from './ModalCreateMailbox';

export type ViewMailbox = {
name: string;
Expand Down Expand Up @@ -87,7 +87,7 @@ export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) {
) : (
<>
{isCreateMailboxFormVisible && mailDomain ? (
<CreateMailboxForm
<ModalCreateMailbox
mailDomain={mailDomain}
closeModal={() => setIsCreateMailboxFormVisible(false)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,84 +8,18 @@ import {
} from '@openfun/cunningham-react';
import { useRouter } from 'next/navigation';
import React from 'react';
import {
Controller,
FormProvider,
UseFormReturn,
useForm,
} from 'react-hook-form';
import { Controller, FormProvider, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';

import { APIError } from '@/api';
import { useAPIError } from '@/api/useAPIError';
import { Box, StyledLink, Text, TextErrors } from '@/components';
import { useCreateMailDomain } from '@/features/mail-domains';
import { useAddMailDomain } from '@/features/mail-domains';

import { default as MailDomainsLogo } from '../assets/mail-domains-logo.svg';

const FORM_ID = 'form-add-mail-domain';

const useAddMailDomainApiError = ({
error,
methods,
}: {
error: APIError | null;
methods: UseFormReturn<{ name: string }> | null;
}): string[] | undefined => {
const [errorCauses, setErrorCauses] = React.useState<undefined | string[]>(
undefined,
);
const { t } = useTranslation();

React.useEffect(() => {
if (methods && t && error) {
let causes = undefined;

if (error.cause?.length) {
const parseCauses = (causes: string[]) =>
causes.reduce((arrayCauses, cause) => {
switch (cause) {
case 'Mail domain with this name already exists.':
case 'Mail domain with this Slug already exists.':
methods.setError('name', {
type: 'manual',
message: t(
'This mail domain is already used. Please, choose another one.',
),
});
break;
default:
arrayCauses.push(cause);
}

return arrayCauses;
}, [] as string[]);

causes = parseCauses(error.cause);
}

if (error.status === 500 || !error.cause) {
causes = [
t(
'Your request cannot be processed because the server is experiencing an error. If the problem ' +
'persists, please contact our support to resolve the issue: suiteterritoriale@anct.gouv.fr.',
),
];
}

setErrorCauses(causes);
}
}, [methods, t, error]);

React.useEffect(() => {
if (errorCauses && methods) {
methods.setFocus('name');
}
}, [methods, errorCauses]);

return errorCauses;
};

export const ModalAddMailDomain = () => {
const { t } = useTranslation();
const router = useRouter();
Expand All @@ -108,13 +42,37 @@ export const ModalAddMailDomain = () => {
mutate: createMailDomain,
isPending,
error,
} = useCreateMailDomain({
} = useAddMailDomain({
onSuccess: (mailDomain) => {
router.push(`/mail-domains/${mailDomain.slug}`);
},
});

const errorCauses = useAddMailDomainApiError({ error, methods });
const errorCauses = useAPIError({
error,
errorParams: {
name: {
causes: [
'Mail domain with this name already exists.',
'Mail domain with this Slug already exists.',
],
handleError: () => {
methods.setError('name', {
type: 'manual',
message: t(
'This mail domain is already used. Please, choose another one.',
),
});
methods.setFocus('name');
},
},
},
serverErrorParams: {
handleError: () => {
methods.setFocus('name');
},
},
});

const onSubmitCallback = (event: React.FormEvent) => {
event.preventDefault();
Expand Down Expand Up @@ -146,7 +104,11 @@ export const ModalAddMailDomain = () => {
<Button
type="submit"
form={FORM_ID}
disabled={!methods.watch('name') || isPending}
disabled={
methods.formState.isSubmitting ||
!methods.formState.isValid ||
isPending
}
>
{t('Add the domain')}
</Button>
Expand All @@ -170,7 +132,11 @@ export const ModalAddMailDomain = () => {
) : null}

<FormProvider {...methods}>
<form id={FORM_ID} onSubmit={onSubmitCallback}>
<form
id={FORM_ID}
onSubmit={onSubmitCallback}
title={t('Mail domain addition form')}
>
<Controller
control={methods.control}
name="name"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import { useTranslation } from 'react-i18next';
import { createGlobalStyle } from 'styled-components';
import { z } from 'zod';

import { useAPIError } from '@/api/useAPIError';
import { Box, Text, TextErrors } from '@/components';

import { CreateMailboxParams, useCreateMailbox } from '../../api';
import { MailDomain } from '../../types';
import { CreateMailboxParams, useCreateMailbox } from '../api';
import { MailDomain } from '../types';

const FORM_ID: string = 'form-create-mailbox';

Expand All @@ -32,7 +33,7 @@ const GlobalStyle = createGlobalStyle`
}
`;

export const CreateMailboxForm = ({
export const ModalCreateMailbox = ({
mailDomain,
closeModal,
}: {
Expand Down Expand Up @@ -77,7 +78,11 @@ export const CreateMailboxForm = ({
resolver: zodResolver(createMailboxValidationSchema),
});

const { mutate: createMailbox, error } = useCreateMailbox({
const {
mutate: createMailbox,
error,
isPending,
} = useCreateMailbox({
mailDomainSlug: mailDomain.slug,
onSuccess: () => {
toast(t('Mailbox created!'), VariantType.SUCCESS, {
Expand All @@ -88,27 +93,47 @@ export const CreateMailboxForm = ({
},
});

const errorCauses = useAPIError({
error,
errorParams: {
local_part: {
causes: ['Mailbox with this Local_part and Domain already exists.'],
handleError: () => {
methods.setError('local_part', {
type: 'manual',
message: t('This email prefix is already used.'),
});
methods.setFocus('local_part');
},
},
secret: {
causes: [
"Please configure your domain's secret before creating any mailbox.",
`Please check secret of the mail domain ${mailDomain.slug}`,
],
causeShown: t(
'The mail domain secret is misconfigured. Please, contact ' +
'our support team to solve the issue: suiteterritoriale@anct.gouv.fr',
),
handleError: () => {
methods.setFocus('first_name');
},
},
},
serverErrorParams: {
handleError: () => {
methods.setFocus('first_name');
},
},
});

const onSubmitCallback = (event: React.FormEvent) => {
event.preventDefault();
void methods.handleSubmit((data) =>
createMailbox({ ...data, mailDomainSlug: mailDomain.slug }),
)();
};

const causes = error?.cause?.filter((cause) => {
const isFound =
cause === 'Mailbox with this Local_part and Domain already exists.';

if (isFound) {
methods.setError('local_part', {
type: 'manual',
message: t('This email prefix is already used.'),
});
}

return !isFound;
});

return (
<FormProvider {...methods}>
<Modal
Expand All @@ -132,7 +157,11 @@ export const CreateMailboxForm = ({
fullWidth
type="submit"
form={FORM_ID}
disabled={methods.formState.isSubmitting}
disabled={
methods.formState.isSubmitting ||
!methods.formState.isValid ||
isPending
}
>
{t('Create the mailbox')}
</Button>
Expand All @@ -152,8 +181,12 @@ export const CreateMailboxForm = ({
>
<GlobalStyle />
<Box $width="100%" $margin={{ top: 'none', bottom: 'xl' }}>
{!!causes?.length && (
<TextErrors $margin={{ bottom: 'small' }} causes={causes} />
{!!errorCauses?.length && (
<TextErrors
$margin={{ bottom: 'small' }}
causes={errorCauses}
$textAlign="left"
/>
)}
<Text
$margin={{ horizontal: 'none', vertical: 'big' }}
Expand Down Expand Up @@ -188,7 +221,11 @@ const Form = ({
const { t } = useTranslation();

return (
<form onSubmit={onSubmitCallback} id={FORM_ID}>
<form
onSubmit={onSubmitCallback}
id={FORM_ID}
title={t('Mailbox creation form')}
>
<Box $direction="column" $width="100%" $gap="2rem" $margin="auto">
<Box $margin={{ horizontal: 'none' }}>
<FieldMailBox
Expand Down
Loading

0 comments on commit 6fd0d92

Please sign in to comment.