diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index d37cb39f2..9ed1b01d7 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -150,7 +150,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - + - name: Set services env variables run: | make create-env-files @@ -175,7 +175,7 @@ jobs: with: path: src/frontend/apps/desk/out/ key: build-front-${{ github.run_id }} - + - name: Build and Start Docker Servers env: DOCKER_BUILDKIT: 1 @@ -183,7 +183,7 @@ jobs: run: | docker compose build --pull --build-arg BUILDKIT_INLINE_CACHE=1 make run - + - name: Apply DRF migrations run: | make migrate diff --git a/CHANGELOG.md b/CHANGELOG.md index 2185b025c..e5e3ae825 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to - ✨(api) add RELEASE version on config endpoint #459 - ✨(backend) manage roles on domain admin view - ✨(frontend) show version number in footer #369 +- ✨(frontend) add tabs inside #466 ### Changed diff --git a/src/frontend/apps/desk/next.config.js b/src/frontend/apps/desk/next.config.js index e2eb64f46..a428f4b1d 100644 --- a/src/frontend/apps/desk/next.config.js +++ b/src/frontend/apps/desk/next.config.js @@ -1,3 +1,5 @@ +const path = require('path'); + /** @type {import('next').NextConfig} */ const nextConfig = { output: 'export', @@ -5,6 +7,9 @@ const nextConfig = { images: { unoptimized: true, }, + sassOptions: { + includePaths: [path.join(__dirname, 'src')], + }, compiler: { // Enables the styled-components SWC transform styledComponents: true, diff --git a/src/frontend/apps/desk/package.json b/src/frontend/apps/desk/package.json index 71d513523..f94bdceb9 100644 --- a/src/frontend/apps/desk/package.json +++ b/src/frontend/apps/desk/package.json @@ -29,6 +29,7 @@ "react-hook-form": "7.53.0", "react-i18next": "15.0.2", "react-select": "5.8.1", + "sass": "1.80.3", "styled-components": "6.1.13", "zod": "3.23.8", "zustand": "4.5.5" diff --git a/src/frontend/apps/desk/src/components/tabs/CustomTabs.tsx b/src/frontend/apps/desk/src/components/tabs/CustomTabs.tsx new file mode 100644 index 000000000..03638a153 --- /dev/null +++ b/src/frontend/apps/desk/src/components/tabs/CustomTabs.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import { ReactNode } from 'react'; +import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components'; + +import { Box } from '@/components'; + +import style from './custom-tabs.module.scss'; + +type TabsOption = { + ariaLabel?: string; + label: string; + iconName?: string; + id?: string; + content: ReactNode; +}; + +type Props = { + tabs: TabsOption[]; +}; + +export const CustomTabs = ({ tabs }: Props) => { + return ( +
+ + + {tabs.map((tab) => { + const id = tab.id ?? tab.label; + return ( + + + {tab.iconName && ( + + )} + {tab.label} + + + ); + })} + + + {tabs.map((tab) => { + const id = tab.id ?? tab.label; + return ( + + {tab.content} + + ); + })} + +
+ ); +}; diff --git a/src/frontend/apps/desk/src/components/tabs/custom-tabs.module.scss b/src/frontend/apps/desk/src/components/tabs/custom-tabs.module.scss new file mode 100644 index 000000000..1fe7b9e98 --- /dev/null +++ b/src/frontend/apps/desk/src/components/tabs/custom-tabs.module.scss @@ -0,0 +1,63 @@ +.customTabsContainer { + :global { + .react-aria-TabList { + display: flex; + + &[data-orientation='horizontal'] { + .react-aria-Tab { + border-bottom: 2px solid var(--c--theme--colors--greyscale-500); + } + } + } + + .react-aria-Tab { + padding: 10px; + cursor: pointer; + outline: none; + position: relative; + color: var(--c--theme--colors--greyscale-700); + transition: color 200ms; + + --border-color: transparent; + + forced-color-adjust: none; + + &[data-hovered], + &[data-focused] { + color: var(--c--theme--colors--greyscale-900); + } + + &[data-selected] { + border-bottom: 2px solid var(--c--theme--colors--primary-600) !important; + color: var(--c--theme--colors--primary-600); + } + + &[data-disabled] { + color: var(--c--theme--colors--greyscale-500); + + &[data-selected] { + --border-color: var(--c--theme--colors--greyscale-200); + } + } + + &[data-focus-visible]::after { + content: ''; + position: absolute; + inset: 4px; + border-radius: 4px; + border: 1px solid var(--c--theme--colors--primary-600); + } + } + + .react-aria-TabPanel { + margin-top: 4px; + padding: 10px; + border-radius: 4px; + outline: none; + + &[data-focus-visible] { + outline: 2px solid var(--c--theme--colors--primary-600); + } + } + } +} diff --git a/src/frontend/apps/desk/src/features/mail-domains/access-management/components/AccessesContent.tsx b/src/frontend/apps/desk/src/features/mail-domains/access-management/components/AccessesContent.tsx index 3d7a59a5c..d02ae3396 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/access-management/components/AccessesContent.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/access-management/components/AccessesContent.tsx @@ -1,11 +1,6 @@ -import { Button } from '@openfun/cunningham-react'; -import { useRouter } from 'next/navigation'; import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { Box, Text } from '@/components'; import { AccessesGrid } from '@/features/mail-domains/access-management/components/AccessesGrid'; -import MailDomainsLogo from '@/features/mail-domains/assets/mail-domains-logo.svg'; import { MailDomain, Role } from '../../domains'; @@ -17,50 +12,6 @@ export const AccessesContent = ({ currentRole: Role; }) => ( <> - ); - -const TopBanner = ({ mailDomain }: { mailDomain: MailDomain }) => { - const router = useRouter(); - const { t } = useTranslation(); - - return ( - - - - - - - - - {mailDomain?.abilities?.manage_accesses && ( - - )} - - - - ); -}; diff --git a/src/frontend/apps/desk/src/features/mail-domains/access-management/components/AccessesGrid.tsx b/src/frontend/apps/desk/src/features/mail-domains/access-management/components/AccessesGrid.tsx index d2b2014b7..40f75ca2b 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/access-management/components/AccessesGrid.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/access-management/components/AccessesGrid.tsx @@ -106,8 +106,6 @@ export const AccessesGrid = ({ return ( { }); }); - it('renders the top banner and accesses grid correctly', () => { + it('renders the accesses grid correctly', () => { renderAccessesContent(); - expect(screen.getByText(mockMailDomain.name)).toBeInTheDocument(); - expect(screen.getByTestId('mail-domains-logo')).toBeInTheDocument(); expect(screen.getByText('Mock AccessesGrid')).toBeInTheDocument(); }); - - it('renders the "Manage mailboxes" button when the user has access', () => { - renderAccessesContent(); - - const manageMailboxesButton = screen.getByRole('button', { - name: /Manage example.com domain mailboxes/, - }); - - expect(manageMailboxesButton).toBeInTheDocument(); - - expect(AccessesGrid).toHaveBeenCalledWith( - { currentRole: Role.ADMIN, mailDomain: mockMailDomain }, - {}, // adding this empty object is necessary to load jest context and that AccessesGrid is a mock - ); - }); - - it('does not render the "Manage mailboxes" button if the user lacks manage_accesses ability', () => { - const mailDomainWithoutAccess = { - ...mockMailDomain, - abilities: { - ...mockMailDomain.abilities, - manage_accesses: false, - }, - }; - - renderAccessesContent(Role.ADMIN, mailDomainWithoutAccess); - - expect( - screen.queryByRole('button', { - name: /Manage mailboxes/i, - }), - ).not.toBeInTheDocument(); - }); - - it('navigates to the mailboxes management page when "Manage mailboxes" is clicked', async () => { - renderAccessesContent(); - - const manageMailboxesButton = screen.getByRole('button', { - name: /Manage example.com domain mailboxes/, - }); - - await userEvent.click(manageMailboxesButton); - - await waitFor(() => { - expect(mockRouterPush).toHaveBeenCalledWith(`/mail-domains/example-com/`); - }); - }); }); diff --git a/src/frontend/apps/desk/src/features/mail-domains/domains/components/MailDomainView.tsx b/src/frontend/apps/desk/src/features/mail-domains/domains/components/MailDomainView.tsx new file mode 100644 index 000000000..6d5da559b --- /dev/null +++ b/src/frontend/apps/desk/src/features/mail-domains/domains/components/MailDomainView.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Box, Text } from '@/components'; +import { CustomTabs } from '@/components/tabs/CustomTabs'; +import { AccessesContent } from '@/features/mail-domains/access-management'; +import MailDomainsLogo from '@/features/mail-domains/assets/mail-domains-logo.svg'; +import { MailDomain, Role } from '@/features/mail-domains/domains'; +import { MailDomainsContent } from '@/features/mail-domains/mailboxes'; + +type Props = { + mailDomain: MailDomain; +}; +export const MailDomainView = ({ mailDomain }: Props) => { + const { t } = useTranslation(); + const currentRole = mailDomain.abilities.delete + ? Role.OWNER + : mailDomain.abilities.manage_accesses + ? Role.ADMIN + : Role.VIEWER; + + const tabs = useMemo(() => { + return [ + { + ariaLabel: t('Go to mailbox management'), + id: 'mails', + iconName: 'mail', + label: t('Mailbox management'), + content: , + }, + { + ariaLabel: t('Go to accesses management'), + id: 'accesses', + iconName: 'people', + label: t('Access management'), + content: ( + + ), + }, + ]; + }, [t, currentRole, mailDomain]); + + return ( + + + + + + + + ); +}; diff --git a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/MailDomainsContent.tsx b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/MailDomainsContent.tsx index 71edf2175..d29ed467d 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/MailDomainsContent.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/MailDomainsContent.tsx @@ -9,14 +9,12 @@ 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'; @@ -99,12 +97,7 @@ export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) { showMailBoxCreationForm={setIsCreateMailboxFormVisible} /> - + {error && } void; }) => { - const router = useRouter(); const { t } = useTranslation(); return ( - + + + - - - - - - - - - - {mailDomain?.abilities?.manage_accesses && ( - - )} + {mailDomain?.abilities.post && (