diff --git a/CHANGELOG.md b/CHANGELOG.md index 38910d9..0dc5ccd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to ## [Unreleased] +- ✨(front) add modal update mailboxes #954 + ### Added - ✨(api) update mailboxes #934 diff --git a/src/frontend/apps/desk/src/features/mail-domains/domains/components/panel/PanelActions.tsx b/src/frontend/apps/desk/src/features/mail-domains/domains/components/panel/PanelActions.tsx index 2edb32e..5706b88 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/domains/components/panel/PanelActions.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/domains/components/panel/PanelActions.tsx @@ -24,6 +24,7 @@ export const PanelActions = () => { $css={` & button { padding: 0; + justify-content: start; svg { padding: 0.1rem; diff --git a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/api/index.tsx b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/api/index.tsx index f394a92..60e5650 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/api/index.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/api/index.tsx @@ -1,2 +1,3 @@ export * from './useCreateMailbox'; export * from './useMailboxes'; +export * from './useUpdateMailbox'; diff --git a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/api/useUpdateMailbox.tsx b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/api/useUpdateMailbox.tsx new file mode 100644 index 0000000..d259f02 --- /dev/null +++ b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/api/useUpdateMailbox.tsx @@ -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 => { + 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; + +export const useUpdateMailbox = (options: UseUpdateMailboxParams) => { + const queryClient = useQueryClient(); + return useMutation({ + 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); + } + }, + }); +}; diff --git a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/ModalUpdateMailbox.tsx b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/ModalUpdateMailbox.tsx new file mode 100644 index 0000000..19964ac --- /dev/null +++ b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/ModalUpdateMailbox.tsx @@ -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([]); + 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: ( + + {!!errorCauses.length && } +
+ + + {t('Personal informations')} + + + {t('Update the user information.')} + + + + + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + + + + + + + {t('Email address')} + + + + + {mailbox.local_part}@{mailDomain.name} + + + +
+ ), + leftAction: ( + + ), + rightAction: ( + + ), + }, + ]; + + return ( + + ); +}; diff --git a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/index.ts b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/index.ts index 0e28135..bf9a2cc 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/index.ts +++ b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/index.ts @@ -1,3 +1,4 @@ export * from './ModalCreateMailbox'; +export * from './ModalUpdateMailbox'; export * from './MailBoxesView'; export * from './panel'; diff --git a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/panel/MailBoxesListView.tsx b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/panel/MailBoxesListView.tsx index b7417ca..4ff8d31 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/panel/MailBoxesListView.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/panel/MailBoxesListView.tsx @@ -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}`} ), }, diff --git a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/panel/PanelActions.tsx b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/panel/PanelActions.tsx index d0c2ccc..9be0495 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/panel/PanelActions.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/panel/PanelActions.tsx @@ -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) => { > + + { ).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(