✨(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:
committed by
Sebastien Nobour
parent
d4e0f74d30
commit
37d32888f5
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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 |
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
1
src/frontend/apps/desk/src/features/mail-domains/conf.ts
Normal file
1
src/frontend/apps/desk/src/features/mail-domains/conf.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const PAGE_SIZE = 20;
|
||||
@@ -6,3 +6,9 @@ export interface MailDomain {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface MailDomainMailbox {
|
||||
id: UUID;
|
||||
local_part: string;
|
||||
secondary_email: string;
|
||||
}
|
||||
|
||||
255
src/frontend/apps/e2e/__tests__/app-desk/mail-domain.spec.ts
Normal file
255
src/frontend/apps/e2e/__tests__/app-desk/mail-domain.spec.ts
Normal 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(),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user