🚸(frontend) improve mailbox creation validations and error handling
- replace known error causes returned by the API on unsuccessful mailbox creation requests by meaningful interpolated message shown above the form - strengthen form validation rules to be identical as those of the api endpoint to prevent emitting invalid requests - designate which form fields are mandatory for accessiblity - update texts for better ux writting, and their translations - fix css style input errors - update related e2e tests.
This commit is contained in:
committed by
Sebastien Nobour
parent
cda4373544
commit
32e6602dda
@@ -93,6 +93,16 @@
|
||||
0 0 2px;
|
||||
}
|
||||
|
||||
.c__input__wrapper--error.c__input__wrapper:focus-within {
|
||||
border-color: var(
|
||||
--c--components--forms-input--border--color-error-hover
|
||||
) !important;
|
||||
}
|
||||
|
||||
.c__input__wrapper--error.c__input__wrapper:focus-within label {
|
||||
color: var(--c--theme--colors--danger-600);
|
||||
}
|
||||
|
||||
.c__input__wrapper--error:not(.c__input__wrapper--disabled):hover label {
|
||||
color: var(--c--components--forms-input--border--color-error-hover);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) {
|
||||
{isCreateMailboxFormVisible && mailDomain ? (
|
||||
<CreateMailboxForm
|
||||
mailDomain={mailDomain}
|
||||
setIsFormVisible={setIsCreateMailboxFormVisible}
|
||||
closeModal={() => setIsCreateMailboxFormVisible(false)}
|
||||
/>
|
||||
) : null}
|
||||
<TopBanner
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
useForm,
|
||||
} from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Box, Text, TextErrors } from '@/components';
|
||||
@@ -26,24 +27,53 @@ import { MailDomain } from '../../types';
|
||||
|
||||
const FORM_ID: string = 'form-create-mailbox';
|
||||
|
||||
const createMailboxValidationSchema = z.object({
|
||||
first_name: z.string().min(1),
|
||||
last_name: z.string().min(1),
|
||||
local_part: z.string().min(1),
|
||||
secondary_email: z.string().min(1),
|
||||
});
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
.c__field__footer__top > .c__field__text {
|
||||
white-space: pre-line;
|
||||
}
|
||||
`;
|
||||
|
||||
export const CreateMailboxForm = ({
|
||||
mailDomain,
|
||||
setIsFormVisible,
|
||||
closeModal,
|
||||
}: {
|
||||
mailDomain: MailDomain;
|
||||
setIsFormVisible: (value: boolean) => void;
|
||||
closeModal: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToastProvider();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const messageInvalidMinChar = t('You must have minimum 1 character');
|
||||
|
||||
const createMailboxValidationSchema = z.object({
|
||||
first_name: z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
t('Please enter {{fieldName}}', { fieldName: 'your first name' }),
|
||||
),
|
||||
last_name: z
|
||||
.string()
|
||||
.min(1, t('Please enter {{fieldName}}', { fieldName: 'your last name' })),
|
||||
local_part: z
|
||||
.string()
|
||||
.regex(
|
||||
/^((?!@|\s)([a-zA-Z0-9.\-]))*$/,
|
||||
t(
|
||||
'It must not contain spaces, accents or special characters (except "." or "-"). E.g.: jean.dupont',
|
||||
),
|
||||
)
|
||||
.min(1, messageInvalidMinChar),
|
||||
secondary_email: z
|
||||
.string()
|
||||
.regex(
|
||||
/[^@\s]+@[^@\s]+\.[^@\s]+/,
|
||||
t('Please enter a valid email address.\nE.g. : jean.dupont@mail.fr'),
|
||||
)
|
||||
.min(1, messageInvalidMinChar),
|
||||
});
|
||||
|
||||
const methods = useForm<CreateMailboxParams>({
|
||||
delayError: 0,
|
||||
defaultValues: {
|
||||
@@ -57,19 +87,17 @@ export const CreateMailboxForm = ({
|
||||
resolver: zodResolver(createMailboxValidationSchema),
|
||||
});
|
||||
|
||||
const { mutate: createMailbox, ...queryState } = useCreateMailbox({
|
||||
const { mutate: createMailbox, error } = useCreateMailbox({
|
||||
mailDomainSlug: mailDomain.slug,
|
||||
onSuccess: () => {
|
||||
toast(t('Mailbox created!'), VariantType.SUCCESS, {
|
||||
duration: 4000,
|
||||
});
|
||||
|
||||
setIsFormVisible(false);
|
||||
closeModal();
|
||||
},
|
||||
});
|
||||
|
||||
const closeModal = () => setIsFormVisible(false);
|
||||
|
||||
const onSubmitCallback = (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
void methods.handleSubmit((data) =>
|
||||
@@ -77,6 +105,20 @@ export const CreateMailboxForm = ({
|
||||
)();
|
||||
};
|
||||
|
||||
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
|
||||
@@ -102,7 +144,7 @@ export const CreateMailboxForm = ({
|
||||
form={FORM_ID}
|
||||
disabled={methods.formState.isSubmitting}
|
||||
>
|
||||
{t('Submit')}
|
||||
{t('Create the mailbox')}
|
||||
</Button>
|
||||
}
|
||||
size={ModalSize.MEDIUM}
|
||||
@@ -113,15 +155,22 @@ export const CreateMailboxForm = ({
|
||||
color={colorsTokens()['primary-text']}
|
||||
title={t('Mailbox creation form')}
|
||||
/>
|
||||
<Text $size="h3" $margin="none" role="heading" aria-level={3}>
|
||||
<Text
|
||||
$size="h3"
|
||||
$margin="none"
|
||||
role="heading"
|
||||
aria-level={3}
|
||||
title={t('Create a mailbox')}
|
||||
>
|
||||
{t('Create a mailbox')}
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box $width="100%" $margin={{ top: 'large', bottom: 'xl' }}>
|
||||
{queryState.isError && (
|
||||
<TextErrors className="mb-s" causes={queryState.error.cause} />
|
||||
<GlobalStyle />
|
||||
<Box $width="100%" $margin={{ top: 'none', bottom: 'xl' }}>
|
||||
{!!causes?.length && (
|
||||
<TextErrors $margin={{ bottom: 'small' }} causes={causes} />
|
||||
)}
|
||||
{methods ? (
|
||||
<Form
|
||||
@@ -151,76 +200,39 @@ const Form = ({
|
||||
<form onSubmit={onSubmitCallback} id={FORM_ID}>
|
||||
<Box $direction="column" $width="100%" $gap="2rem" $margin="auto">
|
||||
<Box $margin={{ horizontal: 'none' }}>
|
||||
<Controller
|
||||
control={methods.control}
|
||||
<FieldMailBox
|
||||
name="first_name"
|
||||
render={({ fieldState }) => (
|
||||
<Input
|
||||
aria-invalid={!!fieldState.error}
|
||||
label={t('First name')}
|
||||
state={fieldState.error ? 'error' : 'default'}
|
||||
text={
|
||||
fieldState.error
|
||||
? t('Please enter your first name')
|
||||
: undefined
|
||||
}
|
||||
{...methods.register('first_name')}
|
||||
/>
|
||||
)}
|
||||
label={t('First name')}
|
||||
methods={methods}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box $margin={{ horizontal: 'none' }}>
|
||||
<Controller
|
||||
control={methods.control}
|
||||
<FieldMailBox
|
||||
name="last_name"
|
||||
render={({ fieldState }) => (
|
||||
<Input
|
||||
aria-invalid={!!fieldState.error}
|
||||
label={t('Last name')}
|
||||
state={fieldState.error ? 'error' : 'default'}
|
||||
text={
|
||||
fieldState.error
|
||||
? t('Please enter your last name')
|
||||
: undefined
|
||||
}
|
||||
{...methods.register('last_name')}
|
||||
/>
|
||||
)}
|
||||
label={t('Last name')}
|
||||
methods={methods}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box $margin={{ horizontal: 'none' }} $direction="row">
|
||||
<Box $width="65%">
|
||||
<Controller
|
||||
control={methods.control}
|
||||
<FieldMailBox
|
||||
name="local_part"
|
||||
render={({ fieldState }) => (
|
||||
<Input
|
||||
aria-invalid={!!fieldState.error}
|
||||
label={t('Main email address')}
|
||||
state={fieldState.error ? 'error' : 'default'}
|
||||
text={
|
||||
fieldState.error
|
||||
? t(
|
||||
'Please enter the first part of the email address, without including "@" in it',
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
{...methods.register('local_part')}
|
||||
/>
|
||||
label={t('Email address prefix')}
|
||||
methods={methods}
|
||||
text={t(
|
||||
'It must not contain spaces, accents or special characters (except "." or "-"). E.g.: jean.dupont',
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Text
|
||||
as="span"
|
||||
$theme="primary"
|
||||
$size="1rem"
|
||||
$display="inline-block"
|
||||
$css={`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
left: 1rem;
|
||||
top: 1rem;
|
||||
`}
|
||||
@@ -230,25 +242,41 @@ const Form = ({
|
||||
</Box>
|
||||
|
||||
<Box $margin={{ horizontal: 'none' }}>
|
||||
<Controller
|
||||
control={methods.control}
|
||||
<FieldMailBox
|
||||
name="secondary_email"
|
||||
render={({ fieldState }) => (
|
||||
<Input
|
||||
aria-invalid={!!fieldState.error}
|
||||
label={t('Secondary email address')}
|
||||
state={fieldState.error ? 'error' : 'default'}
|
||||
text={
|
||||
fieldState.error
|
||||
? t('Please enter your secondary email address')
|
||||
: undefined
|
||||
}
|
||||
{...methods.register('secondary_email')}
|
||||
/>
|
||||
)}
|
||||
label={t('Secondary email address')}
|
||||
methods={methods}
|
||||
text={t('E.g. : jean.dupont@mail.fr')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
interface FieldMailBoxProps {
|
||||
name: 'first_name' | 'last_name' | 'local_part' | 'secondary_email';
|
||||
label: string;
|
||||
methods: UseFormReturn<CreateMailboxParams>;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
const FieldMailBox = ({ name, label, methods, text }: FieldMailBoxProps) => {
|
||||
return (
|
||||
<Controller
|
||||
control={methods.control}
|
||||
name={name}
|
||||
render={({ fieldState }) => (
|
||||
<Input
|
||||
aria-invalid={!!fieldState.error}
|
||||
aria-required
|
||||
required
|
||||
label={label}
|
||||
state={fieldState.error ? 'error' : 'default'}
|
||||
text={fieldState?.error?.message ? fieldState.error.message : text}
|
||||
{...methods.register(name)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"Add team icon": "Icône ajout de groupe",
|
||||
"Address: National Agency for Territorial Cohesion - 20, avenue de Ségur TSA 10717 75 334 Paris Cedex 07 Paris": "Adresse : Agence Nationale de la Cohésion des Territoires - 20, avenue de Ségur TSA 10717 75 334 Paris Cedex 07",
|
||||
"Administration": "Administration",
|
||||
"All fields are mandatory.": "Tous les champs sont obligatoires.",
|
||||
"Are you sure you want to delete {{teamName}} team?": "Êtes-vous sûr de vouloir supprimer le groupe {{teamName}}?",
|
||||
"Are you sure you want to remove this member from the {{team}} group?": "Êtes-vous sûr de vouloir supprimer ce membre du groupe {{team}}?",
|
||||
"Back to home page": "Retour à l'accueil",
|
||||
@@ -37,6 +38,7 @@
|
||||
"Create a new group": "Créer un nouveau groupe",
|
||||
"Create a new team": "Créer un nouveau groupe",
|
||||
"Create new team card": "Carte créer un nouveau groupe",
|
||||
"Create the mailbox": "Créer la boîte mail",
|
||||
"Create the team": "Créer le groupe",
|
||||
"Create your first team by clicking on the \"Create a new team\" button.": "Créez votre premier groupe en cliquant sur le bouton \"Créer un nouveau groupe\".",
|
||||
"Created at": "Créé le",
|
||||
@@ -45,6 +47,8 @@
|
||||
"Delete the team": "Supprimer le groupe",
|
||||
"Deleting the {{teamName}} team": "Suppression du groupe {{teamName}}",
|
||||
"E-mail:": "E-mail:",
|
||||
"E.g. : jean.dupont@mail.fr": "Ex. : jean.dupont@mail.fr",
|
||||
"Email address prefix": "Préfixe de l'adresse mail",
|
||||
"Emails": "Emails",
|
||||
"Empty teams icon": "Icône de groupe vide",
|
||||
"Enter the new name of the selected team": "Entrez le nouveau nom du groupe sélectionné",
|
||||
@@ -61,6 +65,7 @@
|
||||
"Improvement and contact": "Amélioration et contact",
|
||||
"Invitation sent to {{email}}": "Invitation envoyée à {{email}}",
|
||||
"Invite new members to {{teamName}}": "Invitez de nouveaux membres à rejoindre {{teamName}}",
|
||||
"It must not contain spaces, accents or special characters (except \".\" or \"-\"). E.g.: jean.dupont": "Il ne doit pas contenir d'espaces, d'accents ou de caractères spéciaux (excepté \".\" ou \"-\"). Ex. : jean.dupont",
|
||||
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Il semble que la page que vous cherchez n'existe pas ou ne puisse pas être affichée correctement.",
|
||||
"It's true, you didn't have to click on a block that covers half the page to say you agree to the placement of cookies — even if you don't know what it means!": "C'est vrai, vous n'avez pas à cliquer sur un bloc qui couvre la moitié de la page pour dire que vous acceptez le placement de cookies — même si vous ne savez pas ce que cela signifie !",
|
||||
"La Régie (Suite Territoriale) is non-compliant with the RGAA. The site has not yet been audited.": "La Régie (Suite Territoriale) est non conforme avec le RGAA. Le site n’a encore pas été audité.",
|
||||
@@ -76,9 +81,7 @@
|
||||
"Mail Domains": "Domaines de messagerie",
|
||||
"Mail Domains icon": "Icône des domaines mail",
|
||||
"Mailbox created!": "Boîte mail créée !",
|
||||
"Mailbox creation form": "Formulaire de création de boîte mail",
|
||||
"Mailboxes list": "Liste des boîtes mail",
|
||||
"Main email address": "Adresse e-mail principale",
|
||||
"Marianne Logo": "Logo Marianne",
|
||||
"Member": "Membre",
|
||||
"Member icon": "Icône de membre",
|
||||
@@ -97,10 +100,8 @@
|
||||
"Ouch !": "Ouch !",
|
||||
"Owner": "Propriétaire",
|
||||
"Personal data and cookies": "Données personnelles et cookies",
|
||||
"Please enter the first part of the email address, without including \"@\" in it": "Veuillez entrer la première partie de l'adresse e-mail, sans y inclure \"@\"",
|
||||
"Please enter your first name": "Veuillez saisir votre prénom",
|
||||
"Please enter your last name": "Veuillez saisir votre nom",
|
||||
"Please enter your secondary email address": "Veuillez saisir votre adresse e-mail secondaire",
|
||||
"Please enter a valid email address.\nE.g. : jean.dupont@mail.fr": "Veuillez entrer une adresse e-mail valide.\nEx. : jean.dupont@mail.fr",
|
||||
"Please enter {{fieldName}}": "Veuillez entrer {{fieldName}}",
|
||||
"Publication Director": "Directeur de la publication",
|
||||
"Publisher": "Éditeur",
|
||||
"Radio buttons to update the roles": "Boutons radio pour mettre à jour les rôles",
|
||||
@@ -123,7 +124,6 @@
|
||||
"Sort the teams by creation date ascendent": "Trier les groupes par date de création ascendante",
|
||||
"Sort the teams by creation date descendent": "Trier les groupes par date de création descendante",
|
||||
"Stéphanie Schaer: Interministerial Digital Director (DINUM).": "Stéphanie Schaer: Directrice numérique interministériel (DINUM).",
|
||||
"Submit": "Valider",
|
||||
"Team name": "Nom du groupe",
|
||||
"Teams": "Équipes",
|
||||
"Teams icon": "Icône de groupe",
|
||||
@@ -135,6 +135,7 @@
|
||||
"The team in charge of the digital workspace \"La Suite numérique\" can be contacted directly at": "L'équipe responsable de l'espace de travail numérique \"La Suite numérique\" peut être contactée directement à l'adresse",
|
||||
"This accessibility statement applies to La Régie (Suite Territoriale)": "Cette déclaration d’accessibilité s’applique à La Régie (Suite Territoriale)",
|
||||
"This allows us to measure the number of visits and understand which pages are the most viewed.": "Cela nous permet de mesurer le nombre de visites et de comprendre quelles pages sont les plus consultées.",
|
||||
"This email prefix is already used.": "Ce préfixe d'email est déjà utilisé.",
|
||||
"This procedure is to be used in the following case: you have reported to the website \n manager an accessibility defect which prevents you from accessing content or one of the \n portal's services and you have not obtained a satisfactory response.": "Cette procédure est à utiliser dans le cas suivant : vous avez signalé au responsable du site internet un défaut d’accessibilité qui vous empêche d’accéder à un contenu ou à un des services du portail et vous n’avez pas obtenu de réponse satisfaisante.",
|
||||
"This site does not display a cookie consent banner, why?": "Ce site n'affiche pas de bannière de consentement des cookies, pourquoi?",
|
||||
"This site places a small text file (a \"cookie\") on your computer when you visit it.": "Ce site place un petit fichier texte (un « cookie ») sur votre ordinateur lorsque vous le visitez.",
|
||||
@@ -153,6 +154,7 @@
|
||||
"You can:": "Vous pouvez :",
|
||||
"You cannot remove other owner.": "Vous ne pouvez pas supprimer un autre propriétaire.",
|
||||
"You cannot update the role of other owner.": "Vous ne pouvez pas mettre à jour les rôles d'autre propriétaire.",
|
||||
"You must have minimum 1 character": "Vous devez entrer au moins 1 caractère",
|
||||
"accessibility-contact-defenseurdesdroits": "Contacter le délégué du<1>Défenseur des droits dans votre région</1>",
|
||||
"accessibility-form-defenseurdesdroits": "Écrire un message au<1>Défenseur des droits</1>",
|
||||
"icon group": "icône groupe",
|
||||
|
||||
Reference in New Issue
Block a user