Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"zimbra_account_account_add": "Créer un compte",
"zimbra_account_account_order": "Commander un compte",
"zimbra_account_account_migrate": "Migrer un compte",
"zimbra_account_delete_all": "Supprimer les comptes ({{ count }})",
"zimbra_account_tooltip_need_domain": "Veuillez d'abord configurer un domaine.",
"zimbra_account_tooltip_need_slot": "Aucun compte disponible, veuillez en commander.",
"zimbra_account_cancel_modal_content": "Le compte email <strong>{{ email }}</strong> et toutes ses données seront définitivement supprimées à la fin de la période d'engagement du service.",
Expand All @@ -19,6 +20,9 @@
"zimbra_slot_modal_renew_date": "Fin de l'engagement: {{ renewDate }}",
"zimbra_account_delete_modal_content_step1": "Êtes-vous sûr(e) de vouloir supprimer le compte email <strong>{{ email }}</strong> ?",
"zimbra_account_delete_modal_content_step2": "Êtes-vous vraiment sûr(e) de vouloir supprimer ce compte email <strong>{{ email }}</strong> ?",
"zimbra_account_delete_all_modal_content_step1": "Êtes-vous sûr(e) de vouloir supprimer les comptes suivants ?",
"zimbra_account_delete_all_modal_content_step2": "Êtes-vous vraiment sûr(e) de vouloir supprimer ces comptes email suivants ?",
"zimbra_account_delete_all_confirm_label": "Saisissez <strong>{{ label }}</strong> pour confirmer la suppression :",
"zimbra_account_delete_modal_warn_message": "Attention cette action est irréversible et entraîne la suppression des données.",
"zimbra_account_upgrade_cta_back": "Retour vers mes comptes emails",
"zimbra_account_upgrade_new_offer": "Nous lançons en version bêta notre <Link href=\"{{ zimbra_emails_link }}\">nouvelle offre Zimbra Pro</Link>.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"add_email_account": "Créer un compte email",
"email_account_settings": "Paramètres du compte",
"delete_email_account": "Supprimer le compte",
"delete_email_accounts": "Supprimer les comptes",
"order_zimbra_accounts": "Commander des comptes Zimbra",
"select_slot": "Sélectionner une offre",
"form_at_least_one_digit": "Doit contenir au minimum un chiffre",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,15 @@ import {
} from '@/tracking.constants';
import { IAM_ACTIONS } from '@/utils/iamAction.constants';

export const DatagridTopbar = () => {
import { EmailAccountItem } from './EmailAccounts.types';

export type DatagridTopbarProps = {
selectedRows?: EmailAccountItem[];
};

export const DatagridTopbar: React.FC<DatagridTopbarProps> = ({
selectedRows,
}: DatagridTopbarProps) => {
const { t } = useTranslation(['accounts', 'common']);
const { trackClick } = useOvhTracking();
const navigate = useNavigate();
Expand All @@ -36,6 +44,7 @@ export const DatagridTopbar = () => {

const hrefAddEmailAccount = useGenerateUrl('./add', 'path');
const hrefOrderEmailAccount = useGenerateUrl('./order', 'path');
const hrefDeleteSelectedEmailAccounts = useGenerateUrl('./delete_all', 'path');

const { data: domains, isLoading: isLoadingDomains } = useDomains();

Expand Down Expand Up @@ -76,6 +85,16 @@ export const DatagridTopbar = () => {
});
window.open(GUIDES_LIST.ovh_mail_migrator.url.DEFAULT, '_blank');
};
const handleSelectEmailAccounts = () => {
navigate(hrefDeleteSelectedEmailAccounts, {
state: {
selectedEmailAccounts: selectedRows.map((row) => ({
id: row?.id,
email: row?.email,
})),
},
});
};

return (
<div className="flex gap-6">
Expand Down Expand Up @@ -125,6 +144,17 @@ export const DatagridTopbar = () => {
iconAlignment={ODS_BUTTON_ICON_ALIGNMENT.right}
icon={ODS_ICON_NAME.externalLink}
/>
{!!selectedRows?.length && (
<ManagerButton
id="ovh-mail-delete-selected-btn"
color={ODS_BUTTON_COLOR.critical}
size={ODS_BUTTON_SIZE.sm}
onClick={handleSelectEmailAccounts}
label={t('zimbra_account_delete_all', { count: selectedRows.length })}
iconAlignment={ODS_BUTTON_ICON_ALIGNMENT.right}
icon={ODS_ICON_NAME.trash}
/>
)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';

import { useLocation } from 'react-router-dom';

import { useTranslation } from 'react-i18next';

Expand All @@ -21,9 +23,13 @@ import { EmailAccountItem } from './EmailAccounts.types';

export const EmailAccountsDatagrid = () => {
const { t } = useTranslation(['accounts', 'common', NAMESPACES.STATUS]);
const [rowSelection, setRowSelection] = useState({});
const [selectedRows, setSelectedRows] = useState([]);
const [hasADeletingAccount, setHasADeletingAccount] = useState(false);
const isOverridedPage = useOverridePage();
const { formatBytes } = useBytes();
const location = useLocation();
const { clearSelectedEmailAccounts } = location.state || {};

const [searchInput, setSearchInput, debouncedSearchInput, setDebouncedSearchInput] =
useDebouncedValue('');
Expand All @@ -48,6 +54,13 @@ export const EmailAccountsDatagrid = () => {
enabled: !isOverridedPage,
});

useEffect(() => {
if (clearSelectedEmailAccounts) {
setRowSelection({});
setSelectedRows([]);
}
}, [clearSelectedEmailAccounts]);

/* This is necessary to enable back the "Create account" button when your
* slots are full and you delete an account and the account goes
* from "DELETING" state to actually being deleted, because we invalidate
Expand Down Expand Up @@ -84,6 +97,11 @@ export const EmailAccountsDatagrid = () => {
);
}, [accounts, services]);

const isRowSelectable = useCallback(
(item: EmailAccountItem) => item.status === ResourceStatus.READY,
[],
);

const columns: DatagridColumn<EmailAccountItem>[] = useMemo(
() => [
{
Expand Down Expand Up @@ -132,12 +150,18 @@ export const EmailAccountsDatagrid = () => {

return (
<Datagrid
topbar={<DatagridTopbar />}
topbar={<DatagridTopbar selectedRows={selectedRows} />}
search={{
searchInput,
setSearchInput,
onSearch: (search) => setDebouncedSearchInput(search),
}}
rowSelection={{
rowSelection,
setRowSelection,
enableRowSelection: ({ original: item }) => isRowSelectable(item),
onRowSelectionChange: setSelectedRows,
}}
columns={columns.map((column) => ({
...column,
label: t(column.label),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import React, { useState } from 'react';

import { useLocation, useNavigate, useParams } from 'react-router-dom';

import { useMutation } from '@tanstack/react-query';
import { Trans, useTranslation } from 'react-i18next';

import { ODS_INPUT_TYPE, ODS_MODAL_COLOR, ODS_TEXT_PRESET } from '@ovhcloud/ods-components';
import { OdsFormField, OdsInput, OdsText } from '@ovhcloud/ods-components/react';

import { NAMESPACES } from '@ovh-ux/manager-common-translations';
import { ApiError } from '@ovh-ux/manager-core-api';
import { Modal, useNotifications } from '@ovh-ux/manager-react-components';
import {
ButtonType,
PageLocation,
PageType,
useOvhTracking,
} from '@ovh-ux/manager-react-shell-client';

import { deleteZimbraPlatformAccount, getZimbraPlatformListQueryKey } from '@/data/api';
import { useGenerateUrl } from '@/hooks';
import queryClient from '@/queryClient';
import { CANCEL, CONFIRM, DELETE_EMAIL_ACCOUNT } from '@/tracking.constants';

export const DeleteAllEmailAccountModal = () => {
const { trackClick, trackPage } = useOvhTracking();
const { platformId } = useParams();
const { t } = useTranslation(['accounts', 'common', NAMESPACES.ACTIONS]);
const { addError, addSuccess } = useNotifications();
const navigate = useNavigate();
const location = useLocation();
const [isConfirmed, seIsConfirmed] = useState<boolean>(false);
const { selectedEmailAccounts }: { selectedEmailAccounts: Array<{ id: string; email: string }> } =
location.state || {};

const goBackUrl = useGenerateUrl('..', 'path');
const onClose = (clear: boolean) =>
navigate(goBackUrl, { state: { clearSelectedEmailAccounts: clear } });

const [step, setStep] = useState(1);

const { mutate: deleteAllEmailAccount, isPending: isSending } = useMutation({
mutationFn: async () => {
await Promise.all(
selectedEmailAccounts.map((account) => deleteZimbraPlatformAccount(platformId, account.id)),
);
},
onSuccess: () => {
trackPage({
pageType: PageType.bannerSuccess,
pageName: DELETE_EMAIL_ACCOUNT,
});
addSuccess(
<OdsText preset={ODS_TEXT_PRESET.paragraph}>{t('common:delete_success_message')}</OdsText>,
true,
);
},
onError: (error: ApiError) => {
trackPage({
pageType: PageType.bannerError,
pageName: DELETE_EMAIL_ACCOUNT,
});
addError(
<OdsText preset={ODS_TEXT_PRESET.paragraph}>
{t('common:delete_error_message', {
error: error?.response?.data?.message,
})}
</OdsText>,
true,
);
},
onSettled: async () => {
await queryClient.invalidateQueries({
queryKey: getZimbraPlatformListQueryKey(),
});

onClose(true);
},
});

const handleDeleteClick = () => {
trackClick({
location: PageLocation.popup,
buttonType: ButtonType.button,
actionType: 'action',
actions: [DELETE_EMAIL_ACCOUNT, CONFIRM],
});
deleteAllEmailAccount();
};

const handleCancelClick = () => {
trackClick({
location: PageLocation.popup,
buttonType: ButtonType.button,
actionType: 'action',
actions: [DELETE_EMAIL_ACCOUNT, CANCEL],
});
onClose(false);
};

return (
<Modal
heading={t('common:delete_email_accounts')}
type={ODS_MODAL_COLOR.critical}
onDismiss={() => onClose(false)}
isOpen
primaryLabel={t(`${NAMESPACES.ACTIONS}:delete`)}
isPrimaryButtonLoading={step === 1 ? false : isSending}
isPrimaryButtonDisabled={step === 2 && !isConfirmed}
onPrimaryButtonClick={step === 1 ? () => setStep(2) : handleDeleteClick}
primaryButtonTestId="primary-btn"
secondaryLabel={t(`${NAMESPACES.ACTIONS}:cancel`)}
onSecondaryButtonClick={handleCancelClick}
>
<>
{step === 1 && (
<div className="flex flex-col">
<OdsText preset={ODS_TEXT_PRESET.paragraph} data-testid="text-step-1" className="mb-4">
{t('zimbra_account_delete_all_modal_content_step1')}
</OdsText>
{selectedEmailAccounts?.map((account) => (
<OdsText key={account.id} preset={ODS_TEXT_PRESET.paragraph} className="font-bold">
{account.email}
</OdsText>
))}
</div>
)}

{step === 2 && (
<div className="flex flex-col gap-6 select-none">
<OdsText preset={ODS_TEXT_PRESET.paragraph} data-testid="text-step-2" className="mb-4">
{t('zimbra_account_delete_all_modal_content_step2')}
</OdsText>
<OdsFormField className="w-full">
<label htmlFor="confirmation-delete" slot="label">
<Trans
t={t}
i18nKey={'zimbra_account_delete_all_confirm_label'}
values={{
label: t(`${NAMESPACES.ACTIONS}:delete`),
}}
/>
</label>
<OdsInput
type={ODS_INPUT_TYPE.text}
data-testid="input-delete-confirm"
name="confirmation-delete"
id="confirmation-delete"
onPaste={(e) => e.preventDefault()}
onOdsChange={(e) =>
seIsConfirmed(
String(e.target.value).toLocaleLowerCase() ===
t(`${NAMESPACES.ACTIONS}:delete`).toLowerCase(),
)
}
/>
</OdsFormField>
</div>
)}
</>
</Modal>
);
};

export default DeleteAllEmailAccountModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react';

import { useLocation } from 'react-router-dom';

import '@testing-library/jest-dom';
import 'element-internals-polyfill';
import { describe, expect, vi } from 'vitest';

import { deleteZimbraPlatformAccount } from '@/data/api';
import commonTranslation from '@/public/translations/common/Messages_fr_FR.json';
import { act, fireEvent, render, waitFor } from '@/utils/test.provider';
import { OdsHTMLElement } from '@/utils/test.utils';

import DeleteAllEmailAccountModal from './DeleteAll.modal';

vi.mocked(useLocation).mockReturnValue({
state: {
selectedEmailAccounts: [{ id: '1', email: '[email protected]' }],
},
key: '',
pathname: '',
search: '',
hash: '',
});

describe('Email Accounts delete modal', () => {
it('check if it is displayed', async () => {
const { findByText } = render(<DeleteAllEmailAccountModal />);
expect(await findByText(commonTranslation.delete_email_accounts)).toBeVisible();
});

it('check transition from step 1 to step 2 and delete button is disabled', () => {
const { getByTestId } = render(<DeleteAllEmailAccountModal />);
expect(getByTestId('text-step-1')).toBeVisible();
act(() => {
fireEvent.click(getByTestId('primary-btn'));
});

expect(getByTestId('text-step-2')).toBeVisible();
expect(getByTestId('primary-btn')).toHaveAttribute('is-disabled', 'true');
});

it('check transition from step 1 to step 2 and delete', async () => {
const { getByTestId } = render(<DeleteAllEmailAccountModal />);
expect(getByTestId('text-step-1')).toBeVisible();
act(() => {
fireEvent.click(getByTestId('primary-btn'));
});

expect(getByTestId('text-step-2')).toBeVisible();

const confirmInput = getByTestId('input-delete-confirm') as OdsHTMLElement;
expect(confirmInput).toBeVisible();

act(() => {
fireEvent.change(confirmInput, { target: { value: 'delete' } });
confirmInput.odsChange.emit({
name: 'confirmation-delete',
value: 'delete',
});
});
waitFor(() => {
expect(getByTestId('primary-btn')).toHaveAttribute('is-disabled', 'false');
});
// eslint-disable-next-line @typescript-eslint/require-await
await act(async () => {
fireEvent.click(getByTestId('primary-btn'));
});
expect(deleteZimbraPlatformAccount).toHaveBeenCalledOnce();
});
});
Loading
Loading