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 50d49c0 commit 813497a
Show file tree
Hide file tree
Showing 12 changed files with 761 additions and 413 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ and this project adheres to
### Added

- 📈(monitoring) configure sentry monitoring #378
- 🥅(frontend) improve api error handling #355

### Fixed

- 🐛(dimail) improve handling of dimail errors on failed mailbox creation #377
- 🐛(dimail) improve handling of dimail errors on failed mailbox creation #377
- 🐛(frontend) fix mail domain addition submission #355

## [1.0.2] - 2024-08-30

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

export function useCreateMailDomain({
export const useAddMailDomain = ({
onSuccess,
onError,
}: {
onSuccess: (data: MailDomain) => void;
}) {
onError: (error: APIError) => void;
}) => {
const queryClient = useQueryClient();
return useMutation<MailDomain, APIError, string>({
mutationFn: createMailDomain,
mutationFn: addMailDomain,
onSuccess: (data) => {
void queryClient.invalidateQueries({
queryKey: [KEY_LIST_MAIL_DOMAIN],
});
onSuccess(data);
},
onError: (error) => {
onError(error);
},
});
}
};
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 @@ -7,90 +7,26 @@ import {
ModalSize,
} from '@openfun/cunningham-react';
import { useRouter } from 'next/navigation';
import React from 'react';
import {
Controller,
FormProvider,
UseFormReturn,
useForm,
} from 'react-hook-form';
import React, { useState } from 'react';
import { Controller, FormProvider, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';

import { APIError } from '@/api';
import { Box, StyledLink, Text, TextErrors } from '@/components';
import { useCreateMailDomain } from '@/features/mail-domains';
import { parseAPIError } from '@/api/parseAPIError';
import { Box, Text, TextErrors } from '@/components';
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();

const createMailDomainValidationSchema = z.object({
const [errorCauses, setErrorCauses] = useState<string[]>([]);

const addMailDomainValidationSchema = z.object({
name: z.string().min(1, t('Example: saint-laurent.fr')),
});

Expand All @@ -101,26 +37,62 @@ export const ModalAddMailDomain = () => {
},
mode: 'onChange',
reValidateMode: 'onChange',
resolver: zodResolver(createMailDomainValidationSchema),
resolver: zodResolver(addMailDomainValidationSchema),
});

const {
mutate: createMailDomain,
isPending,
error,
} = useCreateMailDomain({
const { mutate: addMailDomain, isPending } = useAddMailDomain({
onSuccess: (mailDomain) => {
router.push(`/mail-domains/${mailDomain.slug}`);
},
onError: (error) => {
const unhandledCauses = parseAPIError({
error,
errorParams: {
name: {
causes: [
'Mail domain with this name already exists.',
'Mail domain with this Slug already exists.',
],
handleError: () => {
if (methods.formState.errors.name) {
return;
}

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');
},
defaultMessage: 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((prevState) =>
unhandledCauses &&
JSON.stringify(unhandledCauses) !== JSON.stringify(prevState)
? unhandledCauses
: prevState,
);
},
});

const errorCauses = useAddMailDomainApiError({ error, methods });

const onSubmitCallback = (event: React.FormEvent) => {
event.preventDefault();

void methods.handleSubmit(({ name }) => {
void createMailDomain(name);
void addMailDomain(name);
})();
};

Expand All @@ -132,11 +104,14 @@ export const ModalAddMailDomain = () => {
<Modal
isOpen
leftActions={
<StyledLink href="/mail-domains">
<Button color="secondary" tabIndex={-1}>
{t('Cancel')}
</Button>
</StyledLink>
<Button
color="secondary"
fullWidth
onClick={() => router.push('/mail-domains/')}
disabled={methods.formState.isSubmitting}
>
{t('Cancel')}
</Button>
}
hideCloseButton
closeOnClickOutside
Expand All @@ -146,7 +121,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 +149,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
Loading

0 comments on commit 813497a

Please sign in to comment.