(app-desk) displays specific mail domain mailboxes

Fetches a mail domain by id and displays
its mailboxes as a list in a table.
Associated with e2e tests.
This commit is contained in:
daproclaima
2024-05-21 16:25:19 +02:00
committed by Sebastien Nobour
parent d4e0f74d30
commit 37d32888f5
7 changed files with 447 additions and 44 deletions

View File

@@ -0,0 +1,57 @@
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
import { MailDomainMailbox } from '../types';
export type MailDomainMailboxesParams = {
id: string;
page: number;
ordering?: string;
};
type MailDomainMailboxesResponse = APIList<MailDomainMailbox>;
export const getMailDomainMailboxes = async ({
id,
page,
ordering,
}: MailDomainMailboxesParams): Promise<MailDomainMailboxesResponse> => {
let url = `mail-domains/${id}/mailboxes/?page=${page}`;
if (ordering) {
url += '&ordering=' + ordering;
}
const response = await fetchAPI(url);
if (!response.ok) {
throw new APIError(
`Failed to get the mailboxes of mail domain ${id}`,
await errorCauses(response),
);
}
return response.json() as Promise<MailDomainMailboxesResponse>;
};
const KEY_LIST_MAILBOX = 'mailboxes';
export function useMailDomainMailboxes(
param: MailDomainMailboxesParams,
queryConfig?: UseQueryOptions<
MailDomainMailboxesResponse,
APIError,
MailDomainMailboxesResponse
>,
) {
return useQuery<
MailDomainMailboxesResponse,
APIError,
MailDomainMailboxesResponse
>({
queryKey: [KEY_LIST_MAILBOX, param],
queryFn: () => getMailDomainMailboxes(param),
...queryConfig,
});
}

View File

@@ -0,0 +1,12 @@
<svg width="43" height="32" viewBox="0 0 43 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2025_112)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.8936 30.6102L20.1012 15.999C21.179 15.1324 21.1277 14.1847 19.7786 13.2002L2.69288 0.792894C0.155858 -1.05085 -0.0055542 0.667942 -0.0055542 1.8582V29.8903C-0.0055542 31.3154 0.493135 31.7341 1.8936 30.6102Z" fill="#000091"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.3371 1.35888L13.6335 23.8743C12.8123 24.5353 12.8636 24.8658 13.6042 25.3507L19.4119 29.1781C21.157 30.324 22.3963 29.92 23.863 28.73L41.9092 14.0894C42.7889 13.3695 42.987 13.0243 42.9723 11.7975L42.8036 1.68206C42.7889 0.499369 42.437 0.469972 41.3371 1.35888Z" fill="#E1000F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M42.2978 16.4253L33.7623 23.1983C32.7063 24.0357 32.5891 24.0945 33.689 24.9467L42.019 31.3817C42.833 32.0136 42.9944 32.1678 42.9944 31.0659V16.7632C42.9944 16.0139 42.9284 15.926 42.2978 16.4253Z" fill="#E1000F"/>
</g>
<defs>
<clipPath id="clip0_2025_112">
<rect width="43" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,51 +1,123 @@
import { DataGrid } from '@openfun/cunningham-react';
import {
DataGrid,
Loader,
SortModel,
usePagination,
} from '@openfun/cunningham-react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Card } from '@/components';
import { Box, Card, Text, TextErrors } from '@/components';
import { MailDomain } from '@/features/mail-domains';
import { useMailDomainMailboxes } from '@/features/mail-domains/api/useMailDomainMailboxes';
import { PAGE_SIZE } from '@/features/mail-domains/conf';
export function MailDomainsContent() {
import { default as MailDomainsLogo } from '../assets/mail-domains-logo.svg';
export type ViewMailbox = { email: string; id: string };
// FIXME : ask Cunningham to export this type
type SortModelItem = {
field: string;
sort: 'asc' | 'desc' | null;
};
const defaultOrderingMapping: Record<string, string> = {
email: 'local_part',
};
/**
* Formats the sorting model based on a given mapping.
* @param {SortModelItem} sortModel The sorting model item containing field and sort direction.
* @param {Record<string, string>} mapping The mapping object to map field names.
* @returns {string} The formatted sorting string.
*/
function formatSortModel(
sortModel: SortModelItem,
mapping = defaultOrderingMapping,
) {
const { field, sort } = sortModel;
const orderingField = mapping[field] || field;
return sort === 'desc' ? `-${orderingField}` : orderingField;
}
export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) {
const [sortModel, setSortModel] = useState<SortModel>([]);
const { t } = useTranslation();
const pagination = usePagination({
defaultPage: 1,
pageSize: PAGE_SIZE,
});
const dataset = [
{
id: '1',
name: 'John Doe',
email: 'john@doe.com',
state: 'Active',
lastConnection: '2021-09-01',
},
{
id: '2',
name: 'Jane Doe',
email: 'jane@doe.com',
state: 'Inactive',
lastConnection: '2021-09-02',
},
];
const { page, pageSize, setPagesCount } = pagination;
const ordering = sortModel.length ? formatSortModel(sortModel[0]) : undefined;
return (
<Card $padding="small" $margin="large">
<DataGrid
columns={[
{
headerName: t('Names'),
field: 'name',
},
{
field: 'email',
headerName: t('Emails'),
},
{
field: 'state',
headerName: t('State'),
},
{
field: 'lastConnection',
headerName: t('Last Connecttion'),
},
]}
rows={dataset}
/>
</Card>
const { data, isLoading, error } = useMailDomainMailboxes({
id: mailDomain.id,
page,
ordering,
});
const viewMailboxes: ViewMailbox[] =
mailDomain && data?.results?.length
? data.results.map((mailbox) => ({
email: `${mailbox.local_part}@${mailDomain.name}`,
id: mailbox.id,
}))
: [];
useEffect(() => {
setPagesCount(data?.count ? Math.ceil(data.count / pageSize) : 0);
}, [data?.count, pageSize, setPagesCount]);
return isLoading ? (
<Box $align="center" $justify="center" $height="100%">
<Loader />
</Box>
) : (
<>
<TopBanner name={mailDomain.name} />
<Card
$padding={{ bottom: 'small' }}
$margin={{ all: 'big', top: 'none' }}
$overflow="auto"
>
{error && <TextErrors causes={error.cause} />}
<DataGrid
columns={[
{
field: 'email',
headerName: t('Emails'),
},
]}
rows={viewMailboxes}
isLoading={isLoading}
onSortModelChange={setSortModel}
sortModel={sortModel}
pagination={{
...pagination,
displayGoto: false,
}}
aria-label={t('Mailboxes list')}
/>
</Card>
</>
);
}
const TopBanner = ({ name }: { name: string }) => {
const { t } = useTranslation();
return (
<Box
$direction="row"
$align="center"
$margin={{ all: 'big', vertical: 'xbig' }}
$gap="2.25rem"
>
<MailDomainsLogo aria-label={t('Mail Domains icon')} />
<Text $margin="none" as="h3" $size="h3">
{name}
</Text>
</Box>
);
};

View File

@@ -51,7 +51,7 @@ export const PanelMailDomains = ({ mailDomain }: MailDomainProps) => {
>
<StyledLink
className="p-s pt-t pb-t"
href={`/mail-domains/${mailDomain.name}`}
href={`/mail-domains/${mailDomain.id}`}
>
<Box $align="center" $direction="row" $gap="0.5rem">
<IconMailDomains

View File

@@ -0,0 +1 @@
export const PAGE_SIZE = 20;

View File

@@ -6,3 +6,9 @@ export interface MailDomain {
created_at: string;
updated_at: string;
}
export interface MailDomainMailbox {
id: UUID;
local_part: string;
secondary_email: string;
}

View File

@@ -0,0 +1,255 @@
import { Page, expect, test } from '@playwright/test';
import { MailDomain } from 'app-desk/src/features/mail-domains';
import { keyCloakSignIn } from './common';
const currentDateIso = new Date().toISOString();
const mailDomainsFixtures: MailDomain[] = [
{
name: 'domain.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43f',
created_at: currentDateIso,
updated_at: currentDateIso,
},
{
name: 'mails.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43e',
created_at: currentDateIso,
updated_at: currentDateIso,
},
{
name: 'versailles.net',
id: '456ac6ca-0402-4615-8005-69bc1efde43g',
created_at: currentDateIso,
updated_at: currentDateIso,
},
{
name: 'paris.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43h',
created_at: currentDateIso,
updated_at: currentDateIso,
},
];
const mailDomainDomainFrFixture = mailDomainsFixtures[0];
const clickOnMailDomainsNavButton = async (page: Page): Promise<void> =>
await page.locator('menu').first().getByLabel(`Mail Domains button`).click();
test.describe('Mail domain', () => {
test.beforeEach(async ({ page, browserName }) => {
await page.goto('/');
await keyCloakSignIn(page, browserName);
});
test('redirects to 404 page when the mail domain requested does not exist', async ({
page,
}) => {
await page.route('**/api/v1.0/mail-domains/?page=*', async (route) => {
await route.fulfill({
json: {
count: 0,
next: null,
previous: null,
results: [],
},
});
});
await page.goto('/mail-domains/unknown-domain.fr');
await expect(
page.getByText(
'It seems that the page you are looking for does not exist or cannot be displayed correctly.',
),
).toBeVisible({
timeout: 15000,
});
});
test('checks all the elements are visible when domain exist but contains no mailboxes', async ({
page,
}) => {
const interceptApiCalls = async () => {
await page.route(
'**/api/v1.0/mail-domains/456ac6ca-0402-4615-8005-69bc1efde43f/mailboxes/?page=1',
async (route) => {
await route.fulfill({
json: {
count: 0,
next: null,
previous: null,
results: [],
},
});
},
);
await page.route(
'**/api/v1.0/mail-domains/456ac6ca-0402-4615-8005-69bc1efde43f**',
async (route) => {
await route.fulfill({
json: mailDomainDomainFrFixture,
});
},
);
await page.route('**/api/v1.0/mail-domains/?page=*', async (route) => {
await route.fulfill({
json: {
count: mailDomainsFixtures.length,
next: null,
previous: null,
results: mailDomainsFixtures,
},
});
});
};
await interceptApiCalls();
await clickOnMailDomainsNavButton(page);
await expect(page).toHaveURL(/mail-domains/);
await page.getByRole('listbox').first().getByText('domain.fr').click();
await expect(page).toHaveURL(
/mail-domains\/456ac6ca-0402-4615-8005-69bc1efde43f/,
);
await expect(
page.getByRole('heading', { name: /domain\.fr/ }).first(),
).toBeVisible();
await expect(page.getByText('This table is empty')).toBeVisible();
});
test('checks all the elements are visible when domain exists and contains 2 pages of mailboxes', async ({
page,
}) => {
const mailboxesFixtures = {
domainFr: {
page1: Array.from({ length: 20 }, (_, i) => ({
id: `456ac6ca-0402-4615-8005-69bc1efde${i}f`,
local_part: `local_part-${i}`,
secondary_email: `secondary_email-${i}`,
})),
page2: Array.from({ length: 2 }, (_, i) => ({
id: `456ac6ca-0402-4615-8005-69bc1efde${i}d`,
local_part: `local_part-${i}`,
secondary_email: `secondary_email-${i}`,
})),
},
};
const interceptApiCalls = async () => {
await page.route('**/api/v1.0/mail-domains/?page=*', async (route) => {
await route.fulfill({
json: {
count: mailDomainsFixtures.length,
next: null,
previous: null,
results: mailDomainsFixtures,
},
});
});
await page.route(
'**/api/v1.0/mail-domains/456ac6ca-0402-4615-8005-69bc1efde43f',
async (route) => {
await route.fulfill({
json: mailDomainDomainFrFixture,
});
},
);
await page.route(
'**/api/v1.0/mail-domains/456ac6ca-0402-4615-8005-69bc1efde43f/mailboxes/?page=1**',
async (route) => {
await route.fulfill({
json: {
count:
mailboxesFixtures.domainFr.page1.length +
mailboxesFixtures.domainFr.page2.length,
next: 'http://localhost:8071/api/v1.0/mail-domains/456ac6ca-0402-4615-8005-69bc1efde43f/mailboxes/?page=2',
previous: null,
results: mailboxesFixtures.domainFr.page1,
},
});
},
);
await page.route(
'**/api/v1.0/mail-domains/456ac6ca-0402-4615-8005-69bc1efde43f/mailboxes/?page=2**',
async (route) => {
await route.fulfill({
json: {
count:
mailboxesFixtures.domainFr.page1.length +
mailboxesFixtures.domainFr.page2.length,
next: null,
previous:
'http://localhost:8071/api/v1.0/mail-domains/456ac6ca-0402-4615-8005-69bc1efde43f/mailboxes/?page=1',
results: mailboxesFixtures.domainFr.page2,
},
});
},
);
};
await interceptApiCalls();
await clickOnMailDomainsNavButton(page);
await expect(page).toHaveURL(/mail-domains/);
await page.getByRole('listbox').first().getByText('domain.fr').click();
await expect(page).toHaveURL(
/mail-domains\/456ac6ca-0402-4615-8005-69bc1efde43f/,
);
await expect(
page.getByRole('heading', { name: 'domain.fr' }),
).toBeVisible();
await expect(
page.getByRole('button', { name: /Emails/ }).first(),
).toBeVisible();
await Promise.all(
mailboxesFixtures.domainFr.page1.map((mailbox) =>
expect(
page.getByText(
`${mailbox.local_part}@${mailDomainDomainFrFixture.name}`,
),
).toBeVisible(),
),
);
await expect(
page.locator('.c__pagination__list').getByRole('button', { name: '1' }),
).toBeVisible();
await expect(
page.locator('.c__pagination__list').getByText('navigate_next'),
).toBeVisible();
await page
.locator('.c__pagination__list')
.getByRole('button', { name: '2' })
.click();
await expect(
page.locator('.c__pagination__list').getByText('navigate_next'),
).toBeHidden();
await expect(
page.locator('.c__pagination__list').getByText('navigate_before'),
).toBeVisible();
await Promise.all(
mailboxesFixtures.domainFr.page2.map((mailbox) =>
expect(
page.getByText(
`${mailbox.local_part}@${mailDomainDomainFrFixture.name}`,
),
).toBeVisible(),
),
);
});
});