✨(front) add modal update mailboxes (#954)
* ✨(front) add modal update mailboxes add modal update mailboxes * ♿️(labels) improve aria-labels on domain panel's buttons improve descriptions on aria-label on domain panel's buttons --------- Co-authored-by: Marie PUPO JEAMMET <marie.pupojeammet@numerique.gouv.fr>
This commit is contained in:
@@ -8,6 +8,8 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- ✨(front) add modal update mailboxes #954
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(api) update mailboxes #934
|
||||
|
||||
@@ -24,6 +24,7 @@ export const PanelActions = () => {
|
||||
$css={`
|
||||
& button {
|
||||
padding: 0;
|
||||
justify-content: start;
|
||||
|
||||
svg {
|
||||
padding: 0.1rem;
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './useCreateMailbox';
|
||||
export * from './useMailboxes';
|
||||
export * from './useUpdateMailbox';
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
UseMutationOptions,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
|
||||
import { KEY_LIST_MAILBOX } from './useMailboxes';
|
||||
|
||||
export interface UpdateMailboxParams {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
secondary_email: string;
|
||||
mailDomainSlug: string;
|
||||
}
|
||||
|
||||
export const updateMailbox = async ({
|
||||
mailDomainSlug,
|
||||
mailboxId,
|
||||
...data
|
||||
}: UpdateMailboxParams & { mailboxId: string }): Promise<void> => {
|
||||
const response = await fetchAPI(
|
||||
`mail-domains/${mailDomainSlug}/mailboxes/${mailboxId}/`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await errorCauses(response);
|
||||
console.log('Error data:', errorData);
|
||||
throw new APIError('Failed to update the mailbox', {
|
||||
status: errorData.status,
|
||||
cause: errorData.cause as string[],
|
||||
data: errorData.data,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
type UseUpdateMailboxParams = {
|
||||
mailDomainSlug: string;
|
||||
mailboxId: string;
|
||||
} & UseMutationOptions<void, APIError, UpdateMailboxParams>;
|
||||
|
||||
export const useUpdateMailbox = (options: UseUpdateMailboxParams) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, APIError, UpdateMailboxParams>({
|
||||
mutationFn: (data) =>
|
||||
updateMailbox({ ...data, mailboxId: options.mailboxId }),
|
||||
onSuccess: (data, variables, context) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [
|
||||
KEY_LIST_MAILBOX,
|
||||
{ mailDomainSlug: variables.mailDomainSlug },
|
||||
],
|
||||
});
|
||||
if (options?.onSuccess) {
|
||||
options.onSuccess(data, variables, context);
|
||||
}
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
if (options?.onError) {
|
||||
options.onError(error, variables, context);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,248 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
Button,
|
||||
Loader,
|
||||
ModalSize,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { APIError } from '@/api/APIError';
|
||||
import { parseAPIError } from '@/api/parseAPIError';
|
||||
import {
|
||||
Box,
|
||||
HorizontalSeparator,
|
||||
Input,
|
||||
Text,
|
||||
TextErrors,
|
||||
} from '@/components';
|
||||
import { CustomModal } from '@/components/modal/CustomModal';
|
||||
|
||||
import { MailDomain } from '../../domains/types';
|
||||
import { useUpdateMailbox } from '../api/useUpdateMailbox';
|
||||
import { ViewMailbox } from '../types';
|
||||
|
||||
const FORM_ID = 'form-update-mailbox';
|
||||
|
||||
interface ModalUpdateMailboxProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
mailDomain: MailDomain;
|
||||
mailbox: ViewMailbox;
|
||||
}
|
||||
|
||||
export const ModalUpdateMailbox = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
mailDomain,
|
||||
mailbox,
|
||||
}: ModalUpdateMailboxProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToastProvider();
|
||||
const [errorCauses, setErrorCauses] = useState<string[]>([]);
|
||||
const [step] = useState(0);
|
||||
|
||||
const updateMailboxValidationSchema = z.object({
|
||||
first_name: z.string().min(1, t('Please enter your first name')),
|
||||
last_name: z.string().min(1, t('Please enter your last name')),
|
||||
secondary_email: z.string().email(t('Please enter a valid email address')),
|
||||
});
|
||||
|
||||
const methods = useForm({
|
||||
resolver: zodResolver(updateMailboxValidationSchema),
|
||||
defaultValues: {
|
||||
first_name: mailbox?.first_name || '',
|
||||
last_name: mailbox?.last_name || '',
|
||||
secondary_email: mailbox?.secondary_email || '',
|
||||
},
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (mailbox && isOpen) {
|
||||
methods.reset({
|
||||
first_name: mailbox.first_name || '',
|
||||
last_name: mailbox.last_name || '',
|
||||
secondary_email: mailbox.secondary_email || '',
|
||||
});
|
||||
}
|
||||
}, [mailbox, isOpen, methods]);
|
||||
|
||||
const { mutate: updateMailbox, isPending } = useUpdateMailbox({
|
||||
mailDomainSlug: mailDomain.slug,
|
||||
mailboxId: mailbox?.id || '',
|
||||
onSuccess: () => {
|
||||
toast(t('Mailbox updated!'), VariantType.SUCCESS, { duration: 4000 });
|
||||
onClose();
|
||||
},
|
||||
onError: (error: APIError) => {
|
||||
const causes =
|
||||
parseAPIError({
|
||||
error,
|
||||
errorParams: [
|
||||
[
|
||||
['Invalid format'],
|
||||
t('Invalid format for the email address.'),
|
||||
undefined,
|
||||
],
|
||||
],
|
||||
serverErrorParams: [
|
||||
t(
|
||||
'An error occurred while updating the mailbox. Please try again.',
|
||||
),
|
||||
undefined,
|
||||
],
|
||||
}) || [];
|
||||
if (causes.length > 0) {
|
||||
causes.forEach((cause) => {
|
||||
toast(cause, VariantType.ERROR, { duration: 4000 });
|
||||
});
|
||||
} else {
|
||||
toast(t('Mailbox update failed!'), VariantType.ERROR, {
|
||||
duration: 4000,
|
||||
});
|
||||
}
|
||||
setErrorCauses(causes);
|
||||
},
|
||||
});
|
||||
|
||||
if (!mailbox) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onSubmitCallback = (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!mailbox?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
void methods.handleSubmit((data) =>
|
||||
updateMailbox({ ...data, mailDomainSlug: mailDomain.slug }),
|
||||
)();
|
||||
};
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: t('Update account'),
|
||||
content: (
|
||||
<FormProvider {...methods}>
|
||||
{!!errorCauses.length && <TextErrors causes={errorCauses} />}
|
||||
<form id={FORM_ID} onSubmit={onSubmitCallback}>
|
||||
<Box $padding={{ top: 'sm', horizontal: 'md' }} $gap="4px">
|
||||
<Text $size="md" $weight="bold">
|
||||
{t('Personal informations')}
|
||||
</Text>
|
||||
<Text $theme="greyscale" $variation="600">
|
||||
{t('Update the user information.')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box $padding={{ horizontal: 'md' }}>
|
||||
<Box $margin={{ top: 'base' }}>
|
||||
<Controller
|
||||
name="first_name"
|
||||
control={methods.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
label={t('First name')}
|
||||
placeholder={t('First name')}
|
||||
required
|
||||
error={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
<Box $margin={{ top: 'base' }}>
|
||||
<Controller
|
||||
name="last_name"
|
||||
control={methods.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
label={t('Last name')}
|
||||
placeholder={t('Last name')}
|
||||
required
|
||||
error={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
<Box $margin={{ top: 'base' }}>
|
||||
<Controller
|
||||
name="secondary_email"
|
||||
control={methods.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
label={t('Personal email address')}
|
||||
placeholder={t('john.appleseed@free.fr')}
|
||||
required
|
||||
error={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<HorizontalSeparator $withPadding={true} />
|
||||
|
||||
<Box $padding={{ top: 'base', horizontal: 'md' }}>
|
||||
<Text $size="md" $weight="bold">
|
||||
{t('Email address')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box $padding="md">
|
||||
<Text>
|
||||
{mailbox.local_part}@{mailDomain.name}
|
||||
</Text>
|
||||
</Box>
|
||||
</form>
|
||||
</FormProvider>
|
||||
),
|
||||
leftAction: (
|
||||
<Button color="secondary" onClick={onClose}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
),
|
||||
rightAction: (
|
||||
<Button
|
||||
type="submit"
|
||||
form={FORM_ID}
|
||||
disabled={!methods.formState.isValid || isPending}
|
||||
>
|
||||
{t('Update')}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div id="modal-update-mailbox">
|
||||
<CustomModal
|
||||
isOpen={isOpen}
|
||||
hideCloseButton
|
||||
step={step}
|
||||
totalSteps={steps.length}
|
||||
leftActions={steps[step].leftAction}
|
||||
rightActions={steps[step].rightAction}
|
||||
size={ModalSize.MEDIUM}
|
||||
title={steps[step].title}
|
||||
onClose={onClose}
|
||||
closeOnEsc
|
||||
closeOnClickOutside
|
||||
>
|
||||
{steps[step].content}
|
||||
{isPending && (
|
||||
<Box $align="center">
|
||||
<Loader />
|
||||
</Box>
|
||||
)}
|
||||
</CustomModal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './ModalCreateMailbox';
|
||||
export * from './ModalUpdateMailbox';
|
||||
export * from './MailBoxesView';
|
||||
export * from './panel';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Box, Tag, Text, TextErrors } from '@/components';
|
||||
import { MailDomain } from '@/features/mail-domains/domains';
|
||||
import {
|
||||
MailDomainMailbox,
|
||||
MailDomainMailboxStatus,
|
||||
ViewMailbox,
|
||||
} from '@/features/mail-domains/mailboxes/types';
|
||||
|
||||
import { useMailboxesInfinite } from '../../api/useMailboxesInfinite';
|
||||
@@ -35,13 +35,6 @@ function formatSortModel(sortModel: SortModelItem) {
|
||||
return sortModel.sort === 'desc' ? `-${sortModel.field}` : sortModel.field;
|
||||
}
|
||||
|
||||
export type ViewMailbox = {
|
||||
name: string;
|
||||
id: string;
|
||||
email: string;
|
||||
status: MailDomainMailboxStatus;
|
||||
};
|
||||
|
||||
export function MailBoxesListView({
|
||||
mailDomain,
|
||||
querySearch,
|
||||
@@ -76,9 +69,13 @@ export function MailBoxesListView({
|
||||
}
|
||||
return data.pages.flatMap((page) =>
|
||||
page.results.map((mailbox: MailDomainMailbox) => ({
|
||||
email: `${mailbox.local_part}@${mailDomain.name}`,
|
||||
name: `${mailbox.first_name} ${mailbox.last_name}`,
|
||||
id: mailbox.id,
|
||||
email: `${mailbox.local_part}@${mailDomain.name}`,
|
||||
first_name: mailbox.first_name,
|
||||
last_name: mailbox.last_name,
|
||||
name: `${mailbox.first_name} ${mailbox.last_name}`,
|
||||
local_part: mailbox.local_part,
|
||||
secondary_email: mailbox.secondary_email,
|
||||
status: mailbox.status,
|
||||
mailbox,
|
||||
})),
|
||||
@@ -86,12 +83,15 @@ export function MailBoxesListView({
|
||||
}, [data, mailDomain]);
|
||||
|
||||
const filteredMailboxes = useMemo(() => {
|
||||
if (!querySearch) {
|
||||
if (typeof querySearch !== 'string' || !querySearch) {
|
||||
return mailboxes;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
const lowerCaseSearch = querySearch.toLowerCase();
|
||||
return mailboxes.filter((mailbox) =>
|
||||
mailbox.email.toLowerCase().includes(lowerCaseSearch),
|
||||
return mailboxes.filter(
|
||||
(mailbox) =>
|
||||
typeof mailbox.email === 'string' &&
|
||||
mailbox.email.toLowerCase().includes(lowerCaseSearch),
|
||||
);
|
||||
}, [querySearch, mailboxes]);
|
||||
|
||||
@@ -145,7 +145,7 @@ export function MailBoxesListView({
|
||||
$theme="greyscale"
|
||||
$css="text-transform: capitalize;"
|
||||
>
|
||||
{row.name}
|
||||
{`${row.first_name} ${row.last_name}`}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -11,7 +11,10 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, DropButton, IconOptions, Text } from '@/components';
|
||||
import { MailDomain } from '@/features/mail-domains/domains';
|
||||
import { ViewMailbox } from '@/features/mail-domains/mailboxes';
|
||||
import {
|
||||
ModalUpdateMailbox,
|
||||
ViewMailbox,
|
||||
} from '@/features/mail-domains/mailboxes';
|
||||
|
||||
import {
|
||||
useResetPassword,
|
||||
@@ -28,6 +31,7 @@ export const PanelActions = ({ mailDomain, mailbox }: PanelActionsProps) => {
|
||||
const [isDropOpen, setIsDropOpen] = useState(false);
|
||||
const isEnabled = mailbox.status === 'enabled';
|
||||
const disableModal = useModal();
|
||||
const updateModal = useModal();
|
||||
const { toast } = useToastProvider();
|
||||
|
||||
const { mutate: updateMailboxStatus } = useUpdateMailboxStatus();
|
||||
@@ -84,7 +88,7 @@ export const PanelActions = ({ mailDomain, mailbox }: PanelActionsProps) => {
|
||||
>
|
||||
<Box>
|
||||
<Button
|
||||
aria-label={t('Open the modal to update the role of this access')}
|
||||
aria-label={t('Open a modal to enable or disable mailbox')}
|
||||
onClick={() => {
|
||||
setIsDropOpen(false);
|
||||
if (isEnabled) {
|
||||
@@ -93,7 +97,6 @@ export const PanelActions = ({ mailDomain, mailbox }: PanelActionsProps) => {
|
||||
handleUpdateMailboxStatus();
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
color="primary-text"
|
||||
icon={
|
||||
<span className="material-icons" aria-hidden="true">
|
||||
@@ -106,7 +109,29 @@ export const PanelActions = ({ mailDomain, mailbox }: PanelActionsProps) => {
|
||||
</Text>
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('Open the modal to update the role of this access')}
|
||||
aria-label={t('Open the modal to update mailbox attributes')}
|
||||
onClick={() => {
|
||||
setIsDropOpen(false);
|
||||
if (isEnabled) {
|
||||
updateModal.open();
|
||||
} else {
|
||||
handleUpdateMailboxStatus();
|
||||
}
|
||||
}}
|
||||
color="primary-text"
|
||||
disabled={!isEnabled}
|
||||
icon={
|
||||
<span className="material-icons" aria-hidden="true">
|
||||
{isEnabled ? 'settings' : 'block'}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Text $theme={isEnabled ? 'primary' : 'greyscale'}>
|
||||
{t('Configure mailbox')}
|
||||
</Text>
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('Reset password for this mailbox')}
|
||||
onClick={() => {
|
||||
setIsDropOpen(false);
|
||||
handleResetMailboxPassword();
|
||||
@@ -120,11 +145,17 @@ export const PanelActions = ({ mailDomain, mailbox }: PanelActionsProps) => {
|
||||
}
|
||||
>
|
||||
<Text $theme={isEnabled ? 'primary' : 'greyscale'}>
|
||||
{isEnabled ? t('Reset password') : t('Reset password')}
|
||||
{t('Reset password')}
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</DropButton>
|
||||
<ModalUpdateMailbox
|
||||
isOpen={updateModal.isOpen}
|
||||
onClose={updateModal.close}
|
||||
mailDomain={mailDomain}
|
||||
mailbox={mailbox}
|
||||
/>
|
||||
<Modal
|
||||
isOpen={disableModal.isOpen}
|
||||
onClose={disableModal.close}
|
||||
|
||||
@@ -14,3 +14,13 @@ export type MailDomainMailboxStatus =
|
||||
| 'disabled'
|
||||
| 'pending'
|
||||
| 'failed';
|
||||
|
||||
export interface ViewMailbox {
|
||||
id: string;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
local_part: string;
|
||||
secondary_email: string;
|
||||
status: MailDomainMailboxStatus;
|
||||
}
|
||||
|
||||
@@ -251,7 +251,9 @@ test.describe('Mail domain', () => {
|
||||
).toBeVisible();
|
||||
|
||||
// Click disable in modal
|
||||
await page.getByRole('button', { name: 'Disable' }).click();
|
||||
await page
|
||||
.getByRole('button', { name: 'Disable', exact: true })
|
||||
.click();
|
||||
|
||||
// Verify mailbox status shows as disabled
|
||||
await expect(
|
||||
|
||||
Reference in New Issue
Block a user