✨(app-desk) add panel for mail domains
Adds a panel based on teams' one. It fetches all mail-domains the connected user has relationships with and displays them as a list of links redirecting to /mail-domains/<my-domain-name>. Updates e2e tests accordingly.
This commit is contained in:
committed by
Sebastien Nobour
parent
3f1b446e8e
commit
9b198d0bab
@@ -0,0 +1 @@
|
||||
export * from './useMailDomains';
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
DefinedInitialDataInfiniteOptions,
|
||||
InfiniteData,
|
||||
QueryKey,
|
||||
useInfiniteQuery,
|
||||
} from '@tanstack/react-query';
|
||||
|
||||
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
|
||||
import { MailDomain } from '@/features/mail-domains/types';
|
||||
|
||||
type MailDomainsResponse = APIList<MailDomain>;
|
||||
|
||||
export enum EnumMailDomainsOrdering {
|
||||
BY_CREATED_AT = 'created_at',
|
||||
BY_CREATED_AT_DESC = '-created_at',
|
||||
}
|
||||
|
||||
export type MailDomainsParams = {
|
||||
ordering: EnumMailDomainsOrdering;
|
||||
};
|
||||
|
||||
type MailDomainsAPIParams = MailDomainsParams & {
|
||||
page: number;
|
||||
};
|
||||
|
||||
export const getMailDomains = async ({
|
||||
ordering,
|
||||
page,
|
||||
}: MailDomainsAPIParams): Promise<MailDomainsResponse> => {
|
||||
const orderingQuery = ordering ? `&ordering=${ordering}` : '';
|
||||
const response = await fetchAPI(`mail-domains/?page=${page}${orderingQuery}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
'Failed to get the mail domains',
|
||||
await errorCauses(response),
|
||||
);
|
||||
}
|
||||
|
||||
return response.json() as Promise<MailDomainsResponse>;
|
||||
};
|
||||
|
||||
export const KEY_LIST_MAIL_DOMAINS = 'mail-domains';
|
||||
|
||||
export function useMailDomains(
|
||||
param: MailDomainsParams,
|
||||
queryConfig?: DefinedInitialDataInfiniteOptions<
|
||||
MailDomainsResponse,
|
||||
APIError,
|
||||
InfiniteData<MailDomainsResponse>,
|
||||
QueryKey,
|
||||
number
|
||||
>,
|
||||
) {
|
||||
return useInfiniteQuery<
|
||||
MailDomainsResponse,
|
||||
APIError,
|
||||
InfiniteData<MailDomainsResponse>,
|
||||
QueryKey,
|
||||
number
|
||||
>({
|
||||
initialPageParam: 1,
|
||||
queryKey: [KEY_LIST_MAIL_DOMAINS, param],
|
||||
queryFn: ({ pageParam }) => getMailDomains({ ...param, page: pageParam }),
|
||||
getNextPageParam(lastPage, allPages) {
|
||||
return lastPage.next ? allPages.length + 1 : undefined;
|
||||
},
|
||||
...queryConfig,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<svg viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_879_8339)">
|
||||
<path
|
||||
d="M18 3C9.72 3 3 9.72 3 18C3 26.28 9.72 33 18 33H25.5V30H18C11.49 30 6 24.51 6 18C6 11.49 11.49 6 18 6C24.51 6 30 11.49 30 18V20.145C30 21.33 28.935 22.5 27.75 22.5C26.565 22.5 25.5 21.33 25.5 20.145V18C25.5 13.86 22.14 10.5 18 10.5C13.86 10.5 10.5 13.86 10.5 18C10.5 22.14 13.86 25.5 18 25.5C20.07 25.5 21.96 24.66 23.31 23.295C24.285 24.63 25.965 25.5 27.75 25.5C30.705 25.5 33 23.1 33 20.145V18C33 9.72 26.28 3 18 3ZM18 22.5C15.51 22.5 13.5 20.49 13.5 18C13.5 15.51 15.51 13.5 18 13.5C20.49 13.5 22.5 15.51 22.5 18C22.5 20.49 20.49 22.5 18 22.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_879_8339">
|
||||
<rect width="36" height="36" fill="currentColor" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 854 B |
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="11.5" transform="rotate(-180 12 12)" fill="white" stroke="currentColor"/>
|
||||
<path d="M14.1683 16.232C14.4803 15.92 14.4803 15.416 14.1683 15.104L11.0643 12L14.1683 8.896C14.4803 8.584 14.4803 8.08 14.1683 7.768C13.8563 7.456 13.3523 7.456 13.0403 7.768L9.36834 11.44C9.05634 11.752 9.05634 12.256 9.36834 12.568L13.0403 16.24C13.3443 16.544 13.8563 16.544 14.1683 16.232Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 500 B |
@@ -3,6 +3,7 @@ import { PropsWithChildren } from 'react';
|
||||
import { Box } from '@/components';
|
||||
import { MainLayout } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Panel } from '@/features/mail-domains/components/panel';
|
||||
|
||||
export function MailDomainsLayout({ children }: PropsWithChildren) {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
@@ -10,6 +11,7 @@ export function MailDomainsLayout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<MainLayout>
|
||||
<Box $height="inherit" $direction="row">
|
||||
<Panel />
|
||||
<Box
|
||||
$background={colorsTokens()['primary-bg']}
|
||||
$width="100%"
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
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 { PanelMailDomains } from './PanelItem';
|
||||
|
||||
interface PanelMailDomainsStateProps {
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
mailDomains?: MailDomain[];
|
||||
}
|
||||
|
||||
export const ItemList = () => {
|
||||
const { ordering } = useMailDomainsStore();
|
||||
const {
|
||||
data,
|
||||
isError,
|
||||
isLoading,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useMailDomains({ ordering });
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const mailDomains = useMemo(() => {
|
||||
return data?.pages.reduce((acc, page) => {
|
||||
return acc.concat(page.results);
|
||||
}, [] as MailDomain[]);
|
||||
}, [data?.pages]);
|
||||
|
||||
return (
|
||||
<Box $css="overflow-y: auto; overflow-x: hidden;" ref={containerRef}>
|
||||
<InfiniteScroll
|
||||
hasMore={hasNextPage}
|
||||
isLoading={isFetchingNextPage}
|
||||
next={() => {
|
||||
void fetchNextPage();
|
||||
}}
|
||||
scrollContainer={containerRef.current}
|
||||
as="ul"
|
||||
$margin={{ top: 'none' }}
|
||||
$padding={{ all: 'none' }}
|
||||
role="listbox"
|
||||
>
|
||||
<ItemListState
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
mailDomains={mailDomains}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ItemListState = ({
|
||||
isLoading,
|
||||
isError,
|
||||
mailDomains,
|
||||
}: PanelMailDomainsStateProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Box $justify="center" $margin={{ bottom: 'big' }}>
|
||||
<Text $theme="danger" $align="center" $textAlign="center">
|
||||
{t('Something wrong happened, please refresh the page.')}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box $align="center" $margin={{ all: 'large' }}>
|
||||
<Loader aria-label={t('mail domains list loading')} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!mailDomains?.length) {
|
||||
return (
|
||||
<Box $justify="center" $margin={{ all: 'small' }}>
|
||||
<Text
|
||||
as="p"
|
||||
$margin={{ vertical: 'none' }}
|
||||
$theme="greyscale"
|
||||
$variation="500"
|
||||
>
|
||||
{t(`0 mail domain to display.`)}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return mailDomains.map((mailDomain) => (
|
||||
<PanelMailDomains mailDomain={mailDomain} key={mailDomain.id} />
|
||||
));
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, BoxButton, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import IconOpenClose from '../../assets/icon-open-close.svg';
|
||||
|
||||
import { ItemList } from './ItemList';
|
||||
|
||||
export const Panel = () => {
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const closedOverridingStyles = !isOpen && {
|
||||
$width: '0',
|
||||
$maxWidth: '0',
|
||||
$minWidth: '0',
|
||||
};
|
||||
|
||||
const transition = 'all 0.5s ease-in-out';
|
||||
|
||||
return (
|
||||
<Box
|
||||
$width="100%"
|
||||
$maxWidth="20rem"
|
||||
$minWidth="14rem"
|
||||
$css={`
|
||||
position: relative;
|
||||
border-right: 1px solid ${colorsTokens()['primary-300']};
|
||||
transition: ${transition};
|
||||
`}
|
||||
$height="inherit"
|
||||
aria-label="mail domains panel"
|
||||
{...closedOverridingStyles}
|
||||
>
|
||||
<BoxButton
|
||||
aria-label={
|
||||
isOpen
|
||||
? t(`Close the mail domains panel`)
|
||||
: t(`Open the mail domains panel`)
|
||||
}
|
||||
$color={colorsTokens()['primary-600']}
|
||||
$css={`
|
||||
position: absolute;
|
||||
right: -1.2rem;
|
||||
top: 1.03rem;
|
||||
transform: rotate(${isOpen ? '0' : '180'}deg);
|
||||
transition: ${transition};
|
||||
`}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<IconOpenClose width={24} height={24} />
|
||||
</BoxButton>
|
||||
<Box
|
||||
$css={`
|
||||
overflow: hidden;
|
||||
opacity: ${isOpen ? '1' : '0'};
|
||||
transition: ${transition};
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
$padding={{ all: 'small', right: 'large' }}
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
$css={`
|
||||
border-bottom: 1px solid ${colorsTokens()['primary-300']};
|
||||
`}
|
||||
>
|
||||
<Text $weight="bold" $size="1.25rem">
|
||||
{t('Mail Domains')}
|
||||
</Text>
|
||||
</Box>
|
||||
<ItemList />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
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';
|
||||
|
||||
interface MailDomainProps {
|
||||
mailDomain: MailDomain;
|
||||
}
|
||||
|
||||
export const PanelMailDomains = ({ mailDomain }: MailDomainProps) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
query: { name },
|
||||
} = useRouter();
|
||||
|
||||
const isActive = mailDomain.id === name;
|
||||
|
||||
const activeStyle = `
|
||||
border-right: 4px solid ${colorsTokens()['primary-600']};
|
||||
background: ${colorsTokens()['primary-400']};
|
||||
span{
|
||||
color: ${colorsTokens()['primary-text']};
|
||||
}
|
||||
`;
|
||||
|
||||
const hoverStyle = `
|
||||
&:hover{
|
||||
border-right: 4px solid ${colorsTokens()['primary-400']};
|
||||
background: ${colorsTokens()['primary-300']};
|
||||
|
||||
span{
|
||||
color: ${colorsTokens()['primary-text']};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<Box
|
||||
$margin={{ all: 'none' }}
|
||||
as="li"
|
||||
$css={`
|
||||
transition: all 0.2s ease-in;
|
||||
border-right: 4px solid transparent;
|
||||
${isActive ? activeStyle : hoverStyle}
|
||||
`}
|
||||
>
|
||||
<StyledLink
|
||||
className="p-s pt-t pb-t"
|
||||
href={`/mail-domains/${mailDomain.name}`}
|
||||
>
|
||||
<Box $align="center" $direction="row" $gap="0.5rem">
|
||||
<IconMailDomains
|
||||
aria-label={t(`Mail Domains icon`)}
|
||||
color={colorsTokens()['primary-500']}
|
||||
className="p-t"
|
||||
width="52"
|
||||
style={{
|
||||
borderRadius: '10px',
|
||||
flexShrink: 0,
|
||||
background: '#fff',
|
||||
border: `1px solid ${colorsTokens()['primary-300']}`,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
$weight="bold"
|
||||
$color={colorsTokens()['greyscale-600']}
|
||||
$css={`
|
||||
min-width: 14rem;
|
||||
`}
|
||||
>
|
||||
{mailDomain.name}
|
||||
</Text>
|
||||
</Box>
|
||||
</StyledLink>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Panel';
|
||||
@@ -1 +1,3 @@
|
||||
export * from './components/';
|
||||
export * from './types';
|
||||
export * from './api';
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { EnumMailDomainsOrdering } from '../api/useMailDomains';
|
||||
|
||||
interface MailDomainsStore {
|
||||
ordering: EnumMailDomainsOrdering;
|
||||
}
|
||||
|
||||
export const useMailDomainsStore = create<MailDomainsStore>(() => ({
|
||||
ordering: EnumMailDomainsOrdering.BY_CREATED_AT_DESC,
|
||||
}));
|
||||
@@ -0,0 +1,8 @@
|
||||
import { UUID } from 'crypto';
|
||||
|
||||
export interface MailDomain {
|
||||
id: UUID;
|
||||
name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -1,16 +1,93 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { MailDomain } from 'app-desk/src/features/mail-domains';
|
||||
|
||||
import { keyCloakSignIn } from './common';
|
||||
|
||||
test.beforeEach(async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
});
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
test.describe('Mails', () => {
|
||||
test('checks all the elements are visible', async ({ page }) => {
|
||||
await page.locator('menu').first().getByLabel(`Mails button`).click();
|
||||
await expect(page.getByText('john@doe.com')).toBeVisible();
|
||||
await expect(page.getByText('jane@doe.com')).toBeVisible();
|
||||
test.describe('Mail domain', () => {
|
||||
test.describe('checks all the elements are visible', () => {
|
||||
test.beforeEach(async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
});
|
||||
|
||||
test('when no mail domain exists', 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
|
||||
.locator('menu')
|
||||
.first()
|
||||
.getByLabel(`Mail Domains button`)
|
||||
.click();
|
||||
await expect(page).toHaveURL(/mail-domains/);
|
||||
await expect(
|
||||
page.getByLabel('mail domains panel', { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText('0 mail domain to display.')).toBeVisible();
|
||||
});
|
||||
|
||||
test('when 4 mail domains exist', async ({ page }) => {
|
||||
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
|
||||
.locator('menu')
|
||||
.first()
|
||||
.getByLabel(`Mail Domains button`)
|
||||
.click();
|
||||
await expect(page).toHaveURL(/mail-domains/);
|
||||
await expect(
|
||||
page.getByLabel('mail domains panel', { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText('0 mail domain to display.')).toHaveCount(0);
|
||||
await expect(page.getByText('domain.fr')).toBeVisible();
|
||||
await expect(page.getByText('mails.fr')).toBeVisible();
|
||||
await expect(page.getByText('versailles.net')).toBeVisible();
|
||||
await expect(page.getByText('paris.fr')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,10 +16,10 @@ test.describe('Menu', () => {
|
||||
expectedText: 'Create a new team',
|
||||
},
|
||||
{
|
||||
name: 'Mails',
|
||||
name: 'Mail Domains',
|
||||
isDefault: false,
|
||||
expectedUrl: '/mails',
|
||||
expectedText: 'Emails',
|
||||
expectedUrl: '/mail-domains',
|
||||
expectedText: 'Mail Domains',
|
||||
},
|
||||
];
|
||||
for (const { name, isDefault, expectedUrl, expectedText } of menuItems) {
|
||||
|
||||
Reference in New Issue
Block a user