diff --git a/src/frontend/apps/desk/src/features/mail-domains/api/index.tsx b/src/frontend/apps/desk/src/features/mail-domains/api/index.tsx new file mode 100644 index 0000000..2fb66f0 --- /dev/null +++ b/src/frontend/apps/desk/src/features/mail-domains/api/index.tsx @@ -0,0 +1 @@ +export * from './useMailDomains'; diff --git a/src/frontend/apps/desk/src/features/mail-domains/api/useMailDomains.tsx b/src/frontend/apps/desk/src/features/mail-domains/api/useMailDomains.tsx new file mode 100644 index 0000000..a9216a2 --- /dev/null +++ b/src/frontend/apps/desk/src/features/mail-domains/api/useMailDomains.tsx @@ -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; + +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 => { + 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; +}; + +export const KEY_LIST_MAIL_DOMAINS = 'mail-domains'; + +export function useMailDomains( + param: MailDomainsParams, + queryConfig?: DefinedInitialDataInfiniteOptions< + MailDomainsResponse, + APIError, + InfiniteData, + QueryKey, + number + >, +) { + return useInfiniteQuery< + MailDomainsResponse, + APIError, + InfiniteData, + 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, + }); +} diff --git a/src/frontend/apps/desk/src/features/mail-domains/assets/icon-mail-domains.svg b/src/frontend/apps/desk/src/features/mail-domains/assets/icon-mail-domains.svg new file mode 100644 index 0000000..b946454 --- /dev/null +++ b/src/frontend/apps/desk/src/features/mail-domains/assets/icon-mail-domains.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/src/frontend/apps/desk/src/features/mail-domains/assets/icon-open-close.svg b/src/frontend/apps/desk/src/features/mail-domains/assets/icon-open-close.svg new file mode 100644 index 0000000..c87764a --- /dev/null +++ b/src/frontend/apps/desk/src/features/mail-domains/assets/icon-open-close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/frontend/apps/desk/src/features/mail-domains/components/MailDomainsLayout.tsx b/src/frontend/apps/desk/src/features/mail-domains/components/MailDomainsLayout.tsx index ff880ff..7d1e14a 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/components/MailDomainsLayout.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/components/MailDomainsLayout.tsx @@ -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 ( + { + const { ordering } = useMailDomainsStore(); + const { + data, + isError, + isLoading, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useMailDomains({ ordering }); + const containerRef = useRef(null); + const mailDomains = useMemo(() => { + return data?.pages.reduce((acc, page) => { + return acc.concat(page.results); + }, [] as MailDomain[]); + }, [data?.pages]); + + return ( + + { + void fetchNextPage(); + }} + scrollContainer={containerRef.current} + as="ul" + $margin={{ top: 'none' }} + $padding={{ all: 'none' }} + role="listbox" + > + + + + ); +}; + +const ItemListState = ({ + isLoading, + isError, + mailDomains, +}: PanelMailDomainsStateProps) => { + const { t } = useTranslation(); + + if (isError) { + return ( + + + {t('Something wrong happened, please refresh the page.')} + + + ); + } + + if (isLoading) { + return ( + + + + ); + } + + if (!mailDomains?.length) { + return ( + + + {t(`0 mail domain to display.`)} + + + ); + } + + return mailDomains.map((mailDomain) => ( + + )); +}; diff --git a/src/frontend/apps/desk/src/features/mail-domains/components/panel/Panel.tsx b/src/frontend/apps/desk/src/features/mail-domains/components/panel/Panel.tsx new file mode 100644 index 0000000..a2c85f4 --- /dev/null +++ b/src/frontend/apps/desk/src/features/mail-domains/components/panel/Panel.tsx @@ -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 ( + + setIsOpen(!isOpen)} + > + + + + + + {t('Mail Domains')} + + + + + + ); +}; diff --git a/src/frontend/apps/desk/src/features/mail-domains/components/panel/PanelItem.tsx b/src/frontend/apps/desk/src/features/mail-domains/components/panel/PanelItem.tsx new file mode 100644 index 0000000..ea81dd4 --- /dev/null +++ b/src/frontend/apps/desk/src/features/mail-domains/components/panel/PanelItem.tsx @@ -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 ( + + + + + + {mailDomain.name} + + + + + ); +}; diff --git a/src/frontend/apps/desk/src/features/mail-domains/components/panel/index.ts b/src/frontend/apps/desk/src/features/mail-domains/components/panel/index.ts new file mode 100644 index 0000000..8960d84 --- /dev/null +++ b/src/frontend/apps/desk/src/features/mail-domains/components/panel/index.ts @@ -0,0 +1 @@ +export * from './Panel'; diff --git a/src/frontend/apps/desk/src/features/mail-domains/index.tsx b/src/frontend/apps/desk/src/features/mail-domains/index.tsx index ef6330a..40f5848 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/index.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/index.tsx @@ -1 +1,3 @@ export * from './components/'; +export * from './types'; +export * from './api'; diff --git a/src/frontend/apps/desk/src/features/mail-domains/store/useMailDomainsStore.tsx b/src/frontend/apps/desk/src/features/mail-domains/store/useMailDomainsStore.tsx new file mode 100644 index 0000000..fd18c0e --- /dev/null +++ b/src/frontend/apps/desk/src/features/mail-domains/store/useMailDomainsStore.tsx @@ -0,0 +1,11 @@ +import { create } from 'zustand'; + +import { EnumMailDomainsOrdering } from '../api/useMailDomains'; + +interface MailDomainsStore { + ordering: EnumMailDomainsOrdering; +} + +export const useMailDomainsStore = create(() => ({ + ordering: EnumMailDomainsOrdering.BY_CREATED_AT_DESC, +})); diff --git a/src/frontend/apps/desk/src/features/mail-domains/types.tsx b/src/frontend/apps/desk/src/features/mail-domains/types.tsx new file mode 100644 index 0000000..8eb443a --- /dev/null +++ b/src/frontend/apps/desk/src/features/mail-domains/types.tsx @@ -0,0 +1,8 @@ +import { UUID } from 'crypto'; + +export interface MailDomain { + id: UUID; + name: string; + created_at: string; + updated_at: string; +} diff --git a/src/frontend/apps/e2e/__tests__/app-desk/mail-domains.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/mail-domains.spec.ts index 761ece4..47ae596 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/mail-domains.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/mail-domains.spec.ts @@ -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(); + }); }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-desk/menu.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/menu.spec.ts index b617756..cd07a5c 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/menu.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/menu.spec.ts @@ -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) {