Skip to content

Commit

Permalink
✨(frontend) add tabs for mail domain page
Browse files Browse the repository at this point in the history
Currently, it is complicated to understand the navigation between mailbox
management and role management for an email domain.
This is why we add tabs with explicit naming
  • Loading branch information
PanchoutNathan committed Oct 23, 2024
1 parent 30229e1 commit cc81368
Show file tree
Hide file tree
Showing 16 changed files with 360 additions and 183 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/people.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set services env variables
run: |
make create-env-files
Expand All @@ -175,15 +175,15 @@ jobs:
with:
path: src/frontend/apps/desk/out/
key: build-front-${{ github.run_id }}

- name: Build and Start Docker Servers
env:
DOCKER_BUILDKIT: 1
COMPOSE_DOCKER_CLI_BUILD: 1
run: |
docker compose build --pull --build-arg BUILDKIT_INLINE_CACHE=1
make run
- name: Apply DRF migrations
run: |
make migrate
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions src/frontend/apps/desk/next.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
const path = require('path');

/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
trailingSlash: true,
images: {
unoptimized: true,
},
sassOptions: {
includePaths: [path.join(__dirname, 'src')],
},
compiler: {
// Enables the styled-components SWC transform
styledComponents: true,
Expand Down
1 change: 1 addition & 0 deletions src/frontend/apps/desk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
54 changes: 54 additions & 0 deletions src/frontend/apps/desk/src/components/tabs/CustomTabs.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={style.customTabsContainer}>
<Tabs>
<TabList>
{tabs.map((tab) => {
const id = tab.id ?? tab.label;
return (
<Tab key={id} aria-label={tab.ariaLabel} id={id}>
<Box $direction="row" $align="center" $gap="5px">
{tab.iconName && (
<span className="material-icons" aria-hidden="true">
{tab.iconName}
</span>
)}
{tab.label}
</Box>
</Tab>
);
})}
</TabList>

{tabs.map((tab) => {
const id = tab.id ?? tab.label;
return (
<TabPanel key={id} id={id}>
{tab.content}
</TabPanel>
);
})}
</Tabs>
</div>
);
};
63 changes: 63 additions & 0 deletions src/frontend/apps/desk/src/components/tabs/custom-tabs.module.scss
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -17,50 +12,6 @@ export const AccessesContent = ({
currentRole: Role;
}) => (
<>
<TopBanner mailDomain={mailDomain} />
<AccessesGrid mailDomain={mailDomain} currentRole={currentRole} />
</>
);

const TopBanner = ({ mailDomain }: { mailDomain: MailDomain }) => {
const router = useRouter();
const { t } = useTranslation();

return (
<Box
$direction="column"
$margin={{ all: 'big', bottom: 'tiny' }}
$gap="1rem"
>
<Box
$direction="row"
$align="center"
$gap="2.25rem"
$justify="space-between"
>
<Box $direction="row" $margin="none" $gap="0.5rem">
<MailDomainsLogo aria-hidden="true" />
<Text $margin="none" as="h3" $size="h3">
{mailDomain?.name}
</Text>
</Box>
</Box>

<Box $direction="row" $justify="flex-end">
<Box $display="flex" $direction="row" $gap="8rem">
{mailDomain?.abilities?.manage_accesses && (
<Button
color="tertiary"
aria-label={t('Manage {{name}} domain mailboxes', {
name: mailDomain?.name,
})}
onClick={() => router.push(`/mail-domains/${mailDomain.slug}/`)}
>
{t('Manage mailboxes')}
</Button>
)}
</Box>
</Box>
</Box>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,6 @@ export const AccessesGrid = ({

return (
<Card
$padding={{ bottom: 'small' }}
$margin={{ all: 'big', top: 'none' }}
$overflow="auto"
$css={`
& .c__pagination__goto {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import { useRouter } from 'next/navigation';

import { AccessesGrid } from '@/features/mail-domains/access-management';
import { AppWrapper } from '@/tests/utils';

import { MailDomain, Role } from '../../../domains';
Expand Down Expand Up @@ -61,58 +59,9 @@ describe('AccessesContent', () => {
});
});

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/`);
});
});
});
Original file line number Diff line number Diff line change
@@ -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: <MailDomainsContent mailDomain={mailDomain} />,
},
{
ariaLabel: t('Go to accesses management'),
id: 'accesses',
iconName: 'people',
label: t('Access management'),
content: (
<AccessesContent mailDomain={mailDomain} currentRole={currentRole} />
),
},
];
}, [t, currentRole, mailDomain]);

return (
<Box $padding="big">
<Box
$width="100%"
$direction="row"
$align="center"
$gap="2.25rem"
$justify="center"
>
<Box
$direction="row"
$justify="center"
$margin={{ bottom: 'big' }}
$gap="0.5rem"
>
<MailDomainsLogo aria-hidden="true" />
<Text $margin="none" as="h3" $size="h3">
{mailDomain?.name}
</Text>
</Box>
</Box>
<CustomTabs tabs={tabs} />
</Box>
);
};
Loading

0 comments on commit cc81368

Please sign in to comment.