🚚(frontend) reorganize mail domains feature

- separate feature into domain, mailboxes
as a member management feature is arriving in
the mail domains feature
- pages/mail-domains/[slug].tsx becomes
pages/mail-domains/[slug]/index.tsx
This commit is contained in:
daproclaima
2024-09-23 12:15:04 +02:00
committed by Anthony LC
parent aea15292ee
commit 2894b9a999
34 changed files with 382 additions and 62 deletions

View File

@@ -1 +0,0 @@
export * from './Panel';

View File

@@ -5,7 +5,7 @@ import React from 'react';
import { AppWrapper } from '@/tests/utils';
import { ModalAddMailDomain } from '../ModalAddMailDomain';
import { ModalAddMailDomain } from '../components';
const mockPush = jest.fn();
jest.mock('next/navigation', () => ({

View File

@@ -1,5 +1,3 @@
export * from './useMailDomains';
export * from './useMailDomain';
export * from './useCreateMailbox';
export * from './useMailboxes';
export * from './useAddMailDomain';

View File

@@ -1,7 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { MailDomain } from '@/features/mail-domains';
import { MailDomain } from '../types';
import { KEY_LIST_MAIL_DOMAIN } from './useMailDomains';

View File

@@ -25,7 +25,7 @@ export const getMailDomain = async ({
return response.json() as Promise<MailDomainResponse>;
};
const KEY_MAIL_DOMAIN = 'mail-domain';
export const KEY_MAIL_DOMAIN = 'mail-domain';
export function useMailDomain(
param: MailDomainParams,

View File

@@ -6,7 +6,8 @@ import {
} from '@tanstack/react-query';
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
import { MailDomain } from '@/features/mail-domains/types';
import { MailDomain } from '../types';
type MailDomainsResponse = APIList<MailDomain>;

View File

@@ -3,7 +3,8 @@ import { PropsWithChildren } from 'react';
import { Box } from '@/components';
import { MainLayout } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { Panel } from '@/features/mail-domains/components/panel';
import { Panel } from './panel';
export function MailDomainsLayout({ children }: PropsWithChildren) {
const { colorsTokens } = useCunninghamTheme();

View File

@@ -9,9 +9,9 @@ import { z } from 'zod';
import { parseAPIError } from '@/api/parseAPIError';
import { Box, Text, TextErrors } from '@/components';
import { Modal } from '@/components/Modal';
import { useAddMailDomain } from '@/features/mail-domains';
import { default as MailDomainsLogo } from '../assets/mail-domains-logo.svg';
import { default as MailDomainsLogo } from '../../assets/mail-domains-logo.svg';
import { useAddMailDomain } from '../api';
const FORM_ID = 'form-add-mail-domain';

View File

@@ -0,0 +1,3 @@
export * from './ModalAddMailDomain';
export * from './MailDomainsLayout';
export * from './panel';

View File

@@ -4,9 +4,11 @@ import { useTranslation } from 'react-i18next';
import { Box, Text } from '@/components';
import { InfiniteScroll } from '@/components/InfiniteScroll';
import { MailDomain } from '@/features/mail-domains';
import { useMailDomains } from '@/features/mail-domains/api/useMailDomains';
import { useMailDomainsStore } from '@/features/mail-domains/store/useMailDomainsStore';
import {
MailDomain,
useMailDomains,
useMailDomainsStore,
} from '@/features/mail-domains/domains';
import { PanelMailDomains } from './PanelItem';

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import IconOpenClose from '@/assets/icons/icon-open-close.svg';
import { Box, BoxButton, Text } from '@/components';
import { useConfigStore } from '@/core/';
import { useConfigStore } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { ItemList } from './ItemList';

View File

@@ -5,8 +5,10 @@ import IconAdd from '@/assets/icons/icon-add.svg';
import IconSort from '@/assets/icons/icon-sort.svg';
import { Box, BoxButton, StyledLink, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { EnumMailDomainsOrdering } from '@/features/mail-domains';
import { useMailDomainsStore } from '@/features/mail-domains/store/useMailDomainsStore';
import {
EnumMailDomainsOrdering,
useMailDomainsStore,
} from '@/features/mail-domains/domains';
export const PanelActions = () => {
const { t } = useTranslation();

View File

@@ -4,9 +4,10 @@ import { useTranslation } from 'react-i18next';
import { Box, StyledLink, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { MailDomain } from '@/features/mail-domains';
import IconMailDomains from '@/features/mail-domains/assets/icon-mail-domains.svg';
import { MailDomain } from '../../index';
interface MailDomainProps {
mailDomain: MailDomain;
}

View File

@@ -0,0 +1,4 @@
export * from './Panel';
export * from './ItemList';
export * from './PanelActions';
export * from './PanelItem';

View File

@@ -1,4 +1,4 @@
export * from './components';
export * from './types';
export * from './api';
export * from './store';
export * from './types';

View File

@@ -1,6 +1,6 @@
import { create } from 'zustand';
import { EnumMailDomainsOrdering } from '../api/useMailDomains';
import { EnumMailDomainsOrdering } from '@/features/mail-domains/domains/api';
interface MailDomainsStore {
ordering: EnumMailDomainsOrdering;

View File

@@ -1,5 +1,4 @@
import { UUID } from 'crypto';
export interface MailDomain {
id: UUID;
name: string;
@@ -17,10 +16,8 @@ export interface MailDomain {
};
}
export interface MailDomainMailbox {
id: UUID;
local_part: string;
first_name: string;
last_name: string;
secondary_email: string;
export enum Role {
ADMIN = 'administrator',
OWNER = 'owner',
VIEWER = 'viewer',
}

View File

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

View File

@@ -9,17 +9,18 @@ import {
VariantType,
usePagination,
} from '@openfun/cunningham-react';
import { useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Card, Text, TextErrors, TextStyled } from '@/components';
import { ModalCreateMailbox } from '@/features/mail-domains/mailboxes';
import { default as MailDomainsLogo } from '../../assets/mail-domains-logo.svg';
import { PAGE_SIZE } from '../../conf';
import { MailDomain } from '../../domains/types';
import { useMailboxes } from '../api/useMailboxes';
import { default as MailDomainsLogo } from '../assets/mail-domains-logo.svg';
import { PAGE_SIZE } from '../conf';
import { MailDomain, MailDomainMailbox } from '../types';
import { ModalCreateMailbox } from './ModalCreateMailbox';
import { MailDomainMailbox } from '../types';
export type ViewMailbox = {
name: string;
@@ -102,6 +103,7 @@ export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) {
$padding={{ bottom: 'small' }}
$margin={{ all: 'big', top: 'none' }}
$overflow="auto"
aria-label={t('Mailboxes list card')}
>
{error && <TextErrors causes={error.cause} />}
@@ -151,6 +153,7 @@ const TopBanner = ({
mailDomain: MailDomain;
showMailBoxCreationForm: (value: boolean) => void;
}) => {
const router = useRouter();
const { t } = useTranslation();
return (
@@ -165,7 +168,7 @@ const TopBanner = ({
$gap="2.25rem"
$justify="space-between"
>
<Box $direction="row" $margin="none" $gap="2.25rem">
<Box $direction="row" $margin="none" $gap="0.5rem">
<MailDomainsLogo aria-hidden="true" />
<Text $margin="none" as="h3" $size="h3">
{mailDomain?.name}
@@ -176,9 +179,22 @@ const TopBanner = ({
<Box $direction="row" $justify="space-between">
<AlertStatus status={mailDomain.status} />
</Box>
{mailDomain?.abilities.post && (
<Box $direction="row-reverse">
<Box $display="inline">
<Box $direction="row" $justify="flex-end">
<Box $display="flex" $direction="row" $gap="0.5rem">
{mailDomain?.abilities?.manage_accesses && (
<Button
color="tertiary"
aria-label={t('Manage {{name}} domain members', {
name: mailDomain?.name,
})}
onClick={() =>
router.push(`/mail-domains/${mailDomain.slug}/accesses/`)
}
>
{t('Manage accesses')}
</Button>
)}
{mailDomain?.abilities.post && (
<Button
aria-label={t('Create a mailbox in {{name}} domain', {
name: mailDomain?.name,
@@ -188,9 +204,9 @@ const TopBanner = ({
>
{t('Create a mailbox')}
</Button>
</Box>
)}
</Box>
)}
</Box>
</Box>
);
};
@@ -204,7 +220,7 @@ const AlertStatus = ({ status }: { status: MailDomain['status'] }) => {
return {
variant: VariantType.WARNING,
message: t(
'Your domain name is being validated. ' +
'Your domain name is being validated. ' +
'You will not be able to create mailboxes until your domain name has been validated by our team.',
),
};

View File

@@ -21,8 +21,8 @@ import { parseAPIError } from '@/api/parseAPIError';
import { Box, Text, TextErrors } from '@/components';
import { Modal } from '@/components/Modal';
import { MailDomain } from '../../domains/types';
import { CreateMailboxParams, useCreateMailbox } from '../api';
import { MailDomain } from '../types';
const FORM_ID: string = 'form-create-mailbox';

View File

@@ -0,0 +1,278 @@
import '@testing-library/jest-dom';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
import React from 'react';
import { AppWrapper } from '@/tests/utils';
import { MailDomain } from '../../../domains/types';
import { MailDomainsContent } from '../MailDomainsContent';
const mockMailDomain: MailDomain = {
id: '456ac6ca-0402-4615-8005-69bc1efde43f',
name: 'example.com',
slug: 'example-com',
status: 'enabled',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
const mockMailDomainAsViewer: MailDomain = {
id: '456ac6ca-0402-4615-8005-69bc1efde43f',
name: 'example.com',
slug: 'example-com',
status: 'enabled',
abilities: {
get: true,
patch: false,
put: false,
post: false,
delete: false,
manage_accesses: false,
},
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
const mockMailboxes = [
{
id: '1',
first_name: 'John',
last_name: 'Doe',
local_part: 'john.doe',
},
{
id: '2',
first_name: 'Jane',
last_name: 'Smith',
local_part: 'jane.smith',
},
];
const mockPush = jest.fn();
const mockedUseRouter = jest.fn().mockReturnValue({
push: mockPush,
});
jest.mock('next/navigation', () => ({
...jest.requireActual('next/navigation'),
useRouter: () => mockedUseRouter(),
}));
describe('MailDomainsContent', () => {
afterEach(() => {
fetchMock.restore();
});
it('renders with no mailboxes and displays empty placeholder', async () => {
fetchMock.get('end:/mail-domains/example-com/mailboxes/?page=1', {
count: 0,
results: [],
});
render(<MailDomainsContent mailDomain={mockMailDomain} />, {
wrapper: AppWrapper,
});
expect(screen.getByRole('status')).toBeInTheDocument();
expect(
await screen.findByText('No mail box was created with this mail domain.'),
).toBeInTheDocument();
});
it('renders mailboxes and displays them correctly', async () => {
fetchMock.get('end:/mail-domains/example-com/mailboxes/?page=1', {
count: 2,
results: mockMailboxes,
});
render(<MailDomainsContent mailDomain={mockMailDomain} />, {
wrapper: AppWrapper,
});
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
expect(screen.getByText('jane.smith@example.com')).toBeInTheDocument();
});
it('handles sorting by name and email', async () => {
const sortedByName = [...mockMailboxes].sort((a, b) =>
a.first_name.localeCompare(b.first_name),
);
const sortedByEmail = [...mockMailboxes].sort((a, b) =>
a.local_part.localeCompare(b.local_part),
);
fetchMock.get('end:/mail-domains/example-com/mailboxes/?page=1', {
count: 2,
results: mockMailboxes,
});
fetchMock.get(
'end:/mail-domains/example-com/mailboxes/?page=1&ordering=name',
{
count: 2,
results: sortedByName,
},
);
fetchMock.get(
'end:/mail-domains/example-com/mailboxes/?page=1&ordering=local_part',
{
count: 2,
results: sortedByEmail,
},
);
render(<MailDomainsContent mailDomain={mockMailDomain} />, {
wrapper: AppWrapper,
});
// Sorting by name
await waitFor(async () => {
await userEvent.click(screen.getByRole('button', { name: 'Names' }));
});
expect(fetchMock.lastUrl()).toContain(
'/mail-domains/example-com/mailboxes/?page=1&ordering=name',
);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
// Sorting by email
await waitFor(async () => {
await userEvent.click(screen.getByRole('button', { name: 'Emails' }));
});
expect(fetchMock.lastUrl()).toContain(
'/mail-domains/example-com/mailboxes/?page=1&ordering=local_part',
);
await waitFor(() => {
expect(screen.getByText('john.doe@example.com')).toBeInTheDocument();
});
});
it('opens the create mailbox modal when button is clicked by granted user', async () => {
fetchMock.get('end:/mail-domains/example-com/mailboxes/?page=1', {
count: 0,
results: [],
});
render(<MailDomainsContent mailDomain={mockMailDomain} />, {
wrapper: AppWrapper,
});
await waitFor(async () => {
await userEvent.click(screen.getByText('Create a mailbox'));
});
await waitFor(async () => {
expect(
await screen.findByTitle('Mailbox creation form'),
).toBeInTheDocument();
});
});
it('redirects to accesses management page when button is clicked by granted user', async () => {
fetchMock.get('end:/mail-domains/example-com/mailboxes/?page=1', {
count: 0,
results: [],
});
render(<MailDomainsContent mailDomain={mockMailDomain} />, {
wrapper: AppWrapper,
});
await waitFor(async () => {
await userEvent.click(screen.getByText('Manage accesses'));
});
expect(mockPush).toHaveBeenCalledWith(
'/mail-domains/example-com/accesses/',
);
});
it('displays the correct alert based on mail domain status', async () => {
fetchMock.get('end:/mail-domains/example-com/mailboxes/?page=1', {
count: 0,
results: [],
});
const statuses = [
{
status: 'pending',
regex: /Your domain name is being validated/,
},
{
status: 'disabled',
regex:
/This domain name is deactivated. No new mailboxes can be created/,
},
{
status: 'failed',
regex: /The domain name encounters an error/,
},
];
for (const { status, regex } of statuses) {
const updatedMailDomain = { ...mockMailDomain, status } as MailDomain;
render(<MailDomainsContent mailDomain={updatedMailDomain} />, {
wrapper: AppWrapper,
});
await waitFor(() => {
expect(screen.getByText(regex)).toBeInTheDocument();
});
}
});
it('handles API errors and displays the error message', async () => {
fetchMock.get('end:/mail-domains/example-com/mailboxes/?page=1', {
status: 500,
body: {
cause: 'An unexpected error occurred.',
},
});
render(<MailDomainsContent mailDomain={mockMailDomain} />, {
wrapper: AppWrapper,
});
expect(
await screen.findByText('An unexpected error occurred.'),
).toBeInTheDocument();
});
it('hides buttons to ungranted users', async () => {
fetchMock.get('end:/mail-domains/example-com/mailboxes/?page=1', {
count: 0,
results: [],
});
render(<MailDomainsContent mailDomain={mockMailDomainAsViewer} />, {
wrapper: AppWrapper,
});
await waitFor(() => {
expect(screen.queryByText('Manage accesses')).not.toBeInTheDocument();
});
await waitFor(() => {
expect(screen.queryByText('Create a mailbox')).not.toBeInTheDocument();
});
});
});

View File

@@ -4,7 +4,7 @@ import fetchMock from 'fetch-mock';
import { AppWrapper } from '@/tests/utils';
import { MailDomain } from '../../types';
import { MailDomain } from '../../../domains/types';
import { ModalCreateMailbox } from '../ModalCreateMailbox';
const mockMailDomain: MailDomain = {
@@ -64,9 +64,8 @@ describe('ModalCreateMailbox', () => {
fetchMock.restore();
});
it('renders the modal with all fields and buttons', () => {
it('renders all the elements', () => {
renderModalCreateMailbox();
const {
formTag,
inputFirstName,

View File

@@ -1,2 +1,2 @@
export * from './ModalCreateMailbox';
export * from './MailDomainsContent';
export * from './MailDomainsLayout';

View File

@@ -0,0 +1,3 @@
export * from './api';
export * from './components';
export * from './types';

View File

@@ -0,0 +1,9 @@
import { UUID } from 'crypto';
export interface MailDomainMailbox {
id: UUID;
local_part: string;
first_name: string;
last_name: string;
secondary_email: string;
}

View File

@@ -5,11 +5,14 @@ import { ReactElement } from 'react';
import { Box } from '@/components';
import { TextErrors } from '@/components/TextErrors';
import { MailDomainsContent, MailDomainsLayout } from '@/features/mail-domains';
import { useMailDomain } from '@/features/mail-domains/api/useMailDomain';
import {
MailDomainsLayout,
useMailDomain,
} from '@/features/mail-domains/domains';
import { MailDomainsContent } from '@/features/mail-domains/mailboxes';
import { NextPageWithLayout } from '@/types/next';
const Page: NextPageWithLayout = () => {
const MailboxesPage: NextPageWithLayout = () => {
const router = useRouter();
if (router?.query?.slug && typeof router.query.slug !== 'string') {
@@ -22,9 +25,9 @@ const Page: NextPageWithLayout = () => {
const {
data: mailDomain,
error: error,
error,
isError,
isLoading: isLoading,
isLoading,
} = useMailDomain({ slug: String(slug) });
if (error?.status === 404) {
@@ -47,8 +50,8 @@ const Page: NextPageWithLayout = () => {
}
};
Page.getLayout = function getLayout(page: ReactElement) {
MailboxesPage.getLayout = function getLayout(page: ReactElement) {
return <MailDomainsLayout>{page}</MailDomainsLayout>;
};
export default Page;
export default MailboxesPage;

View File

@@ -1,8 +1,10 @@
import React, { ReactElement } from 'react';
import { Box } from '@/components';
import { MailDomainsLayout } from '@/features/mail-domains';
import { ModalAddMailDomain } from '@/features/mail-domains/components/ModalAddMailDomain';
import {
MailDomainsLayout,
ModalAddMailDomain,
} from '@/features/mail-domains/domains';
import { NextPageWithLayout } from '@/types/next';
const Page: NextPageWithLayout = () => {

View File

@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import { Box } from '@/components';
import { MailDomainsLayout } from '@/features/mail-domains';
import { MailDomainsLayout } from '@/features/mail-domains/domains';
import { NextPageWithLayout } from '@/types/next';
const StyledButton = styled(Button)`

View File

@@ -1,8 +1,6 @@
import { Page, expect, test } from '@playwright/test';
import {
CreateMailboxParams,
MailDomain,
} from 'app-desk/src/features/mail-domains';
import { MailDomain } from 'app-desk/src/features/mail-domains/domains';
import { CreateMailboxParams } from 'app-desk/src/features/mail-domains/mailboxes';
import { keyCloakSignIn } from './common';
@@ -181,9 +179,10 @@ test.describe('Mail domain create mailbox', () => {
request.url().includes('/mail-domains/domainfr/mailboxes/') &&
request.method() === 'POST'
) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const payload: Omit<CreateMailboxParams, 'mailDomainId'> =
request.postDataJSON();
const payload = request.postDataJSON() as Omit<
CreateMailboxParams,
'mailDomainId'
>;
if (payload) {
isCreateMailboxRequestSentWithExpectedPayload =

View File

@@ -1,5 +1,5 @@
import { Page, expect, test } from '@playwright/test';
import { MailDomain } from 'app-desk/src/features/mail-domains';
import { MailDomain } from 'app-desk/src/features/mail-domains/domains';
import { keyCloakSignIn } from './common';

View File

@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
import { MailDomain } from 'app-desk/src/features/mail-domains';
import { MailDomain } from 'app-desk/src/features/mail-domains/domains';
import { keyCloakSignIn } from './common';