✨(frontend) add tabs for mail domain page (#466)
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
This commit is contained in:
6
.github/workflows/people.yml
vendored
6
.github/workflows/people.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
54
src/frontend/apps/desk/src/components/tabs/CustomTabs.tsx
Normal file
54
src/frontend/apps/desk/src/components/tabs/CustomTabs.tsx
Normal file
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}) => (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -106,8 +106,6 @@ export const AccessesGrid = ({
|
||||
|
||||
return (
|
||||
<Card
|
||||
$padding={{ bottom: 'small' }}
|
||||
$margin={{ all: 'big', top: 'none' }}
|
||||
$overflow="auto"
|
||||
$css={`
|
||||
& .c__pagination__goto {
|
||||
|
||||
@@ -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';
|
||||
@@ -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/`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
<Card
|
||||
$padding={{ bottom: 'small' }}
|
||||
$margin={{ all: 'big', top: 'none' }}
|
||||
$overflow="auto"
|
||||
aria-label={t('Mailboxes list card')}
|
||||
>
|
||||
<Card $overflow="auto" aria-label={t('Mailboxes list card')}>
|
||||
{error && <TextErrors causes={error.cause} />}
|
||||
|
||||
<DataGrid
|
||||
@@ -153,47 +146,19 @@ const TopBanner = ({
|
||||
mailDomain: MailDomain;
|
||||
showMailBoxCreationForm: (value: boolean) => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box
|
||||
$direction="column"
|
||||
$margin={{ all: 'big', bottom: 'tiny' }}
|
||||
$gap="1rem"
|
||||
>
|
||||
<Box $direction="column" $gap="1rem">
|
||||
<AlertStatus status={mailDomain.status} />
|
||||
|
||||
<Box
|
||||
$direction="row"
|
||||
$justify="flex-end"
|
||||
$margin={{ bottom: 'small' }}
|
||||
$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="space-between">
|
||||
<AlertStatus status={mailDomain.status} />
|
||||
</Box>
|
||||
<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>
|
||||
)}
|
||||
<Box $display="flex" $direction="row">
|
||||
{mailDomain?.abilities.post && (
|
||||
<Button
|
||||
aria-label={t('Create a mailbox in {{name}} domain', {
|
||||
|
||||
@@ -187,25 +187,6 @@ describe('MailDomainsContent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"de": { "translation": {} },
|
||||
"en": {
|
||||
"translation": {
|
||||
"{{count}} member_many": "{{count}} members",
|
||||
@@ -11,6 +10,7 @@
|
||||
"translation": {
|
||||
"0 group to display.": "0 groupe à afficher.",
|
||||
"Access icon": "Icône d'accès",
|
||||
"Access management": "Gestion des rôles",
|
||||
"Accesses list card": "Carte de la liste des accès",
|
||||
"Accessibility statement": "Déclaration d'accessibilité",
|
||||
"Accessibility: non-compliant": "Accessibilité : non conforme",
|
||||
@@ -68,6 +68,8 @@
|
||||
"First name": "Prénom",
|
||||
"Freedom Equality Fraternity Logo": "Logo Liberté Égalité Fraternité",
|
||||
"French Interministerial Directorate for Digital Affairs (DINUM), 20 avenue de Ségur 75007 Paris.": "Direction interministérielle des affaires numériques (DINUM), 20 avenue de Segur 75007 Paris.",
|
||||
"Go to accesses management": "Aller à la gestion des rôles",
|
||||
"Go to mailbox management": "Aller à la gestion des mails",
|
||||
"Group details": "Détails du groupe",
|
||||
"Group members": "Membres du groupe",
|
||||
"Groups": "Groupes",
|
||||
@@ -94,12 +96,9 @@
|
||||
"Mail domains panel": "Panel des domaines de messagerie",
|
||||
"Mailbox created!": "Boîte mail créée !",
|
||||
"Mailbox creation form": "Formulaire de création de boite mail",
|
||||
"Mailbox management": "Gestion des boîtes mails",
|
||||
"Mailboxes list": "Liste des boîtes mail",
|
||||
"Mailboxes list card": "Carte liste des boîtes mails",
|
||||
"Manage accesses": "Gérer les accès",
|
||||
"Manage mailboxes": "Gérer les boîtes mails",
|
||||
"Manage {{name}} domain mailboxes": "Gérer les boîtes mails du domaine {{name}}",
|
||||
"Manage {{name}} domain members": "Gérer les membres du domaine {{name}}",
|
||||
"Marianne Logo": "Logo Marianne",
|
||||
"Member": "Membre",
|
||||
"Member icon": "Icône de membre",
|
||||
@@ -155,6 +154,7 @@
|
||||
"The National Agency for Territorial Cohesion undertakes to make its\n service accessible, in accordance with article 47 of law no. 2005-102\n of February 11, 2005.": "L'Agence Nationale de la Cohésion des Territoires s’engage à rendre son service accessible, conformément à l’article 47 de la loi n° 2005-102 du 11 février 2005.",
|
||||
"The access has been removed from the domain": "L'accès a été supprimé du domaine",
|
||||
"The domain name encounters an error. Please contact our support team to solve the problem:": "Le nom de domaine rencontre une erreur. Veuillez contacter notre support pour résoudre le problème :",
|
||||
"The mail domain secret is misconfigured. Please, contact our support team to solve the issue: suiteterritoriale@anct.gouv.fr": "Le secret du domaine de messagerie est mal configuré. Veuillez contacter notre support pour résoudre le problème : suiteterritoriale@anct.gouv.fr",
|
||||
"The member has been removed from the team": "Le membre a été supprimé de votre groupe",
|
||||
"The role has been updated": "Le rôle a bien été mis à jour",
|
||||
"The team has been removed.": "Le groupe a été supprimé.",
|
||||
@@ -177,11 +177,11 @@
|
||||
"Update the team": "Mettre à jour le groupe",
|
||||
"Validate": "Valider",
|
||||
"Validate the modification": "Valider la modification",
|
||||
"Version: {{release}}": "Version : {{release}}",
|
||||
"Viewer": "Lecteur",
|
||||
"We simply comply with the law, which states that certain audience measurement tools, properly configured to respect privacy, are exempt from prior authorization.": "Nous nous conformons simplement à la loi, qui stipule que certains outils de mesure d’audience, correctement configurés pour respecter la vie privée, sont exemptés de toute autorisation préalable.",
|
||||
"You are the last owner, you cannot be removed from your domain.": "Vous êtes le dernier propriétaire, vous ne pouvez pas être retiré de votre domaine.",
|
||||
"You are the last owner, you cannot be removed from your team.": "Vous êtes le dernier propriétaire, vous ne pouvez pas être retiré de votre groupe.",
|
||||
"You are the sole owner of this domain. Make another member the domain owner, before you can change your own role.": "Vous êtes le seul propriétaire de ce domaine. Faites d'un autre membre le propriétaire du domaine avant de modifier votre rôle.",
|
||||
"You are the sole owner of this group. Make another member the group owner, before you can change your own role.": "Vous êtes l’unique propriétaire de ce groupe. Désignez un autre membre comme propriétaire du groupe, avant de pouvoir modifier votre propre rôle.",
|
||||
"You can oppose the tracking of your browsing on this website.": "Vous pouvez vous opposer au suivi de votre navigation sur ce site.",
|
||||
"You can:": "Vous pouvez :",
|
||||
@@ -190,7 +190,6 @@
|
||||
"You must have minimum 1 character": "Vous devez entrer au moins 1 caractère",
|
||||
"Your domain name is being validated. You will not be able to create mailboxes until your domain name has been validated by our team.": "Votre nom de domaine est en cours de validation. Vous ne pourrez créer de boîtes mail que lorsque votre nom de domaine sera validé par notre équipe.",
|
||||
"Your request cannot be processed because the server is experiencing an error. If the problem persists, please contact our support to resolve the issue: suiteterritoriale@anct.gouv.fr": "Votre demande ne peut pas être traitée car le serveur rencontre une erreur. Si le problème persiste, veuillez contacter notre support pour résoudre le problème : suiteterritoriale@anct.gouv.fr",
|
||||
"Your request to create a mailbox cannot be completed due to incorrect settings on our server. Please contact our support team to resolve the problem: suiteterritoriale@anct.gouv.fr": "Votre demande de création de boîte mail ne peut pas être complétée en raison de paramètres incorrects sur notre serveur. Veuillez contacter notre équipe support pour résoudre le problème : suiteterritoriale@anct.gouv.fr",
|
||||
"[disabled]": "[désactivé]",
|
||||
"[enabled]": "[actif]",
|
||||
"[failed]": "[erroné]",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { useRouter as useNavigate } from 'next/navigation';
|
||||
import { useRouter } from 'next/router';
|
||||
import { ReactElement } from 'react';
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { TextErrors } from '@/components/TextErrors';
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
MailDomainsLayout,
|
||||
useMailDomain,
|
||||
} from '@/features/mail-domains/domains';
|
||||
import { MailDomainsContent } from '@/features/mail-domains/mailboxes';
|
||||
import { MailDomainView } from '@/features/mail-domains/domains/components/MailDomainView';
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
const MailboxesPage: NextPageWithLayout = () => {
|
||||
@@ -45,9 +45,13 @@ const MailboxesPage: NextPageWithLayout = () => {
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
return mailDomain ? <MailDomainsContent mailDomain={mailDomain} /> : null;
|
||||
}
|
||||
|
||||
if (!mailDomain) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <MailDomainView mailDomain={mailDomain} />;
|
||||
};
|
||||
|
||||
MailboxesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
|
||||
@@ -211,6 +211,19 @@ test.describe('Mail domain', () => {
|
||||
},
|
||||
];
|
||||
|
||||
test('checks if all tabs are visible', async ({ page }) => {
|
||||
await interceptCommonApiCalls(page, mailDomainsFixtures);
|
||||
|
||||
await clickOnMailDomainsNavButton(page);
|
||||
|
||||
await assertMailDomainUpperElementsAreVisible(page);
|
||||
|
||||
await expect(
|
||||
page.getByLabel('Go to accesses management'),
|
||||
).toBeVisible();
|
||||
await expect(page.getByLabel('Go to mailbox management')).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks all the elements are visible when domain exist but contains no mailboxes', async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -1880,6 +1880,89 @@
|
||||
figlet "1.7.0"
|
||||
ts-node "10.9.2"
|
||||
|
||||
"@parcel/watcher-android-arm64@2.4.1":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz#c2c19a3c442313ff007d2d7a9c2c1dd3e1c9ca84"
|
||||
integrity sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==
|
||||
|
||||
"@parcel/watcher-darwin-arm64@2.4.1":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz#c817c7a3b4f3a79c1535bfe54a1c2818d9ffdc34"
|
||||
integrity sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==
|
||||
|
||||
"@parcel/watcher-darwin-x64@2.4.1":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz#1a3f69d9323eae4f1c61a5f480a59c478d2cb020"
|
||||
integrity sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==
|
||||
|
||||
"@parcel/watcher-freebsd-x64@2.4.1":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz#0d67fef1609f90ba6a8a662bc76a55fc93706fc8"
|
||||
integrity sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==
|
||||
|
||||
"@parcel/watcher-linux-arm-glibc@2.4.1":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz#ce5b340da5829b8e546bd00f752ae5292e1c702d"
|
||||
integrity sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==
|
||||
|
||||
"@parcel/watcher-linux-arm64-glibc@2.4.1":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz#6d7c00dde6d40608f9554e73998db11b2b1ff7c7"
|
||||
integrity sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==
|
||||
|
||||
"@parcel/watcher-linux-arm64-musl@2.4.1":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz#bd39bc71015f08a4a31a47cd89c236b9d6a7f635"
|
||||
integrity sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==
|
||||
|
||||
"@parcel/watcher-linux-x64-glibc@2.4.1":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz#0ce29966b082fb6cdd3de44f2f74057eef2c9e39"
|
||||
integrity sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==
|
||||
|
||||
"@parcel/watcher-linux-x64-musl@2.4.1":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz#d2ebbf60e407170bb647cd6e447f4f2bab19ad16"
|
||||
integrity sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==
|
||||
|
||||
"@parcel/watcher-win32-arm64@2.4.1":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz#eb4deef37e80f0b5e2f215dd6d7a6d40a85f8adc"
|
||||
integrity sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==
|
||||
|
||||
"@parcel/watcher-win32-ia32@2.4.1":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz#94fbd4b497be39fd5c8c71ba05436927842c9df7"
|
||||
integrity sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==
|
||||
|
||||
"@parcel/watcher-win32-x64@2.4.1":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz#4bf920912f67cae5f2d264f58df81abfea68dadf"
|
||||
integrity sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==
|
||||
|
||||
"@parcel/watcher@^2.4.1":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.4.1.tgz#a50275151a1bb110879c6123589dba90c19f1bf8"
|
||||
integrity sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==
|
||||
dependencies:
|
||||
detect-libc "^1.0.3"
|
||||
is-glob "^4.0.3"
|
||||
micromatch "^4.0.5"
|
||||
node-addon-api "^7.0.0"
|
||||
optionalDependencies:
|
||||
"@parcel/watcher-android-arm64" "2.4.1"
|
||||
"@parcel/watcher-darwin-arm64" "2.4.1"
|
||||
"@parcel/watcher-darwin-x64" "2.4.1"
|
||||
"@parcel/watcher-freebsd-x64" "2.4.1"
|
||||
"@parcel/watcher-linux-arm-glibc" "2.4.1"
|
||||
"@parcel/watcher-linux-arm64-glibc" "2.4.1"
|
||||
"@parcel/watcher-linux-arm64-musl" "2.4.1"
|
||||
"@parcel/watcher-linux-x64-glibc" "2.4.1"
|
||||
"@parcel/watcher-linux-x64-musl" "2.4.1"
|
||||
"@parcel/watcher-win32-arm64" "2.4.1"
|
||||
"@parcel/watcher-win32-ia32" "2.4.1"
|
||||
"@parcel/watcher-win32-x64" "2.4.1"
|
||||
|
||||
"@pkgjs/parseargs@^0.11.0":
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
@@ -3409,7 +3492,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
|
||||
integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
|
||||
|
||||
"@types/node@*", "@types/node@20.16.10":
|
||||
"@types/node@*":
|
||||
version "20.16.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.16.10.tgz#0cc3fdd3daf114a4776f54ba19726a01c907ef71"
|
||||
integrity sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==
|
||||
@@ -3426,7 +3509,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6"
|
||||
integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==
|
||||
|
||||
"@types/react-dom@*", "@types/react-dom@18.3.0":
|
||||
"@types/react-dom@*":
|
||||
version "18.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0"
|
||||
integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==
|
||||
@@ -4363,6 +4446,13 @@ cheerio@^1.0.0:
|
||||
undici "^6.19.5"
|
||||
whatwg-mimetype "^4.0.0"
|
||||
|
||||
chokidar@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.1.tgz#4a6dff66798fb0f72a94f616abbd7e1a19f31d41"
|
||||
integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==
|
||||
dependencies:
|
||||
readdirp "^4.0.1"
|
||||
|
||||
chromatic@11.7.1:
|
||||
version "11.7.1"
|
||||
resolved "https://registry.yarnpkg.com/chromatic/-/chromatic-11.7.1.tgz#9de59dd9d0e2a847627bccd959f05881335b524e"
|
||||
@@ -4797,6 +4887,11 @@ dequal@^2.0.2, dequal@^2.0.3:
|
||||
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
||||
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
|
||||
|
||||
detect-libc@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
||||
integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
|
||||
|
||||
detect-newline@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
|
||||
@@ -6171,6 +6266,11 @@ ignore@^5.2.0, ignore@^5.3.1, ignore@^5.3.2:
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
||||
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
|
||||
|
||||
immutable@^4.0.0:
|
||||
version "4.3.7"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381"
|
||||
integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==
|
||||
|
||||
import-fresh@^3.2.1, import-fresh@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
||||
@@ -7278,7 +7378,7 @@ merge2@^1.3.0, merge2@^1.4.1:
|
||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||
|
||||
micromatch@^4.0.4, micromatch@^4.0.8:
|
||||
micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
|
||||
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
|
||||
@@ -7400,6 +7500,11 @@ no-case@^3.0.4:
|
||||
lower-case "^2.0.2"
|
||||
tslib "^2.0.3"
|
||||
|
||||
node-addon-api@^7.0.0:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
|
||||
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
|
||||
|
||||
node-fetch@2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||
@@ -8154,6 +8259,11 @@ readable-stream@~2.3.6:
|
||||
string_decoder "~1.1.1"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
readdirp@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a"
|
||||
integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==
|
||||
|
||||
redent@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
|
||||
@@ -8375,6 +8485,16 @@ safe-regex-test@^1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
sass@^1.80.3:
|
||||
version "1.80.3"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.80.3.tgz#3f63dd527647d2b3de35f36acb971bda80517423"
|
||||
integrity sha512-ptDWyVmDMVielpz/oWy3YP3nfs7LpJTHIJZboMVs8GEC9eUmtZTZhMHlTW98wY4aEorDfjN38+Wr/XjskFWcfA==
|
||||
dependencies:
|
||||
"@parcel/watcher" "^2.4.1"
|
||||
chokidar "^4.0.0"
|
||||
immutable "^4.0.0"
|
||||
source-map-js ">=0.6.2 <2.0.0"
|
||||
|
||||
saxes@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5"
|
||||
@@ -8492,6 +8612,11 @@ sort-keys@^5.0.0:
|
||||
dependencies:
|
||||
is-plain-obj "^4.0.0"
|
||||
|
||||
"source-map-js@>=0.6.2 <2.0.0":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
|
||||
source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
|
||||
@@ -9134,7 +9259,7 @@ typed-array-length@^1.0.6:
|
||||
is-typed-array "^1.1.13"
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
typescript@*, typescript@5.6.2, typescript@^5.0.4:
|
||||
typescript@*, typescript@^5.0.4:
|
||||
version "5.6.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0"
|
||||
integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==
|
||||
|
||||
Reference in New Issue
Block a user