(front) add modal update mailboxes

add modal update mailboxes - wip - waiting api update mailbox
This commit is contained in:
Eléonore Voisin
2025-06-11 16:09:34 +02:00
parent 1245c54c61
commit 03b2a37ba2
8 changed files with 364 additions and 18 deletions

View File

@@ -24,6 +24,7 @@ export const PanelActions = () => {
$css={`
& button {
padding: 0;
justify-content: start;
svg {
padding: 0.1rem;

View File

@@ -1,2 +1,3 @@
export * from './useCreateMailbox';
export * from './useMailboxes';
export * from './useUpdateMailbox';

View File

@@ -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);
}
},
});
};

View File

@@ -0,0 +1,243 @@
import { zodResolver } from '@hookform/resolvers/zod';
import {
Button,
Loader,
ModalSize,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
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/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',
});
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);
},
});
const onSubmitCallback = (event: React.FormEvent) => {
event.preventDefault();
if (!mailbox?.id) {
return;
}
void methods.handleSubmit((data) =>
updateMailbox({ ...data, mailDomainSlug: mailDomain.slug }),
)();
};
if (!mailbox) {
return null;
}
const steps = [
{
title: t('Set up 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}
/>
)}
/>
<Text $theme="greyscale" $variation="600">
{t(
'The person will receive an email at this address to set up their account.',
)}
</Text>
</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>
);
};

View File

@@ -1,3 +1,4 @@
export * from './ModalCreateMailbox';
export * from './ModalUpdateMailbox';
export * from './MailBoxesView';
export * from './panel';

View File

@@ -6,7 +6,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 { PAGE_SIZE } from '../../../conf';
@@ -28,13 +28,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,
@@ -63,11 +56,12 @@ export function MailBoxesListView({
}
return data.results.map((mailbox: MailDomainMailbox) => ({
email: `${mailbox.local_part}@${mailDomain.name}`,
name: `${mailbox.first_name} ${mailbox.last_name}`,
id: mailbox.id,
first_name: mailbox.first_name,
last_name: mailbox.last_name,
local_part: mailbox.local_part,
secondary_email: mailbox.secondary_email,
status: mailbox.status,
mailbox,
}));
}, [data?.results, mailDomain]);
@@ -79,7 +73,7 @@ export function MailBoxesListView({
return (
(mailboxes &&
mailboxes.filter((mailbox) =>
mailbox.email.toLowerCase().includes(lowerCaseSearch),
mailbox.local_part.toLowerCase().includes(lowerCaseSearch),
)) ||
[]
);
@@ -95,11 +89,11 @@ export function MailBoxesListView({
rows={filteredMailboxes}
columns={[
{
field: 'email',
field: 'local_part',
headerName: `${t('Address')}${filteredMailboxes.length}`,
},
{
field: 'name',
field: 'first_name',
headerName: t('User'),
enableSorting: true,
renderCell: ({ row }) => (
@@ -108,7 +102,7 @@ export function MailBoxesListView({
$theme="greyscale"
$css="text-transform: capitalize;"
>
{row.name}
{row.first_name} {row.last_name}
</Text>
),
},

View File

@@ -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();
@@ -83,6 +87,25 @@ export const PanelActions = ({ mailDomain, mailbox }: PanelActionsProps) => {
isOpen={isDropOpen}
>
<Box>
<Button
aria-label={t('Open the modal to update the role of this access')}
onClick={() => {
setIsDropOpen(false);
if (isEnabled) {
updateModal.open();
} else {
handleUpdateMailboxStatus();
}
}}
color="primary-text"
icon={
<span className="material-icons" aria-hidden="true">
settings
</span>
}
>
<Text $theme="primary">{t('Configure mailbox')}</Text>
</Button>
<Button
aria-label={t('Open the modal to update the role of this access')}
onClick={() => {
@@ -93,7 +116,6 @@ export const PanelActions = ({ mailDomain, mailbox }: PanelActionsProps) => {
handleUpdateMailboxStatus();
}
}}
fullWidth
color="primary-text"
icon={
<span className="material-icons" aria-hidden="true">
@@ -120,11 +142,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}

View File

@@ -14,3 +14,12 @@ export type MailDomainMailboxStatus =
| 'disabled'
| 'pending'
| 'failed';
export interface ViewMailbox {
id: string;
first_name: string;
last_name: string;
local_part: string;
secondary_email: string;
status: MailDomainMailboxStatus;
}