🛂(app-desk) add TeamActions component

TeamActions is a component that control the actions
that a member can performed on the team.
It contains a dropdown menu that contains the actions:
- Edit Team
- Remove Team
This commit is contained in:
Anthony LC
2024-03-26 16:21:18 +01:00
committed by Anthony LC
parent 73e58e274c
commit 6b2fb4169c
8 changed files with 215 additions and 16 deletions

View File

@@ -0,0 +1,78 @@
import { Button } from '@openfun/cunningham-react';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, DropButton, IconOptions, Text } from '@/components';
import { Role, Team } from '../types';
import { ModalRemoveTeam } from './ModalRemoveTeam';
import { ModalUpdateTeam } from './ModalUpdateTeam';
interface TeamActionsProps {
currentRole: Role;
team: Team;
}
export const TeamActions = ({ currentRole, team }: TeamActionsProps) => {
const { t } = useTranslation();
const [isModalUpdateOpen, setIsModalUpdateOpen] = useState(false);
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
const [isDropOpen, setIsDropOpen] = useState(false);
if (currentRole === Role.MEMBER) {
return null;
}
return (
<>
<DropButton
button={
<IconOptions
isOpen={isDropOpen}
aria-label={t('Open the team options')}
/>
}
onOpenChange={(isOpen) => setIsDropOpen(isOpen)}
isOpen={isDropOpen}
>
<Box>
<Button
onClick={() => {
setIsModalUpdateOpen(true);
setIsDropOpen(false);
}}
color="primary-text"
icon={<span className="material-icons">edit</span>}
>
<Text $theme="primary">{t('Update the team')}</Text>
</Button>
{currentRole === Role.OWNER && (
<Button
onClick={() => {
setIsModalRemoveOpen(true);
setIsDropOpen(false);
}}
color="primary-text"
icon={<span className="material-icons">delete</span>}
>
<Text $theme="primary">{t('Delete the team')}</Text>
</Button>
)}
</Box>
</DropButton>
{isModalUpdateOpen && (
<ModalUpdateTeam
onClose={() => setIsModalUpdateOpen(false)}
team={team}
/>
)}
{isModalRemoveOpen && (
<ModalRemoveTeam
onClose={() => setIsModalRemoveOpen(false)}
team={team}
/>
)}
</>
);
};

View File

@@ -3,12 +3,13 @@ import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import IconGroup from '@/assets/icons/icon-group2.svg';
import { Box, BoxButton, Card, IconOptions, Text } from '@/components';
import { Box, Card, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Team } from '../api/types';
import { Role, Team } from '../types';
import { ModalUpdateTeam } from './ModalUpdateTeam';
import { TeamActions } from './TeamActions';
const format: DateTimeFormatOptions = {
month: '2-digit',
@@ -18,9 +19,10 @@ const format: DateTimeFormatOptions = {
interface TeamInfoProps {
team: Team;
currentRole: Role;
}
export const TeamInfo = ({ team }: TeamInfoProps) => {
export const TeamInfo = ({ team, currentRole }: TeamInfoProps) => {
const { t } = useTranslation();
const { colorsTokens } = useCunninghamTheme();
const { i18n } = useTranslation();
@@ -38,16 +40,7 @@ export const TeamInfo = ({ team }: TeamInfoProps) => {
<>
<Card className="m-b" style={{ paddingBottom: 0 }}>
<Box $css="align-self: flex-end;" className="m-t" $position="absolute">
<BoxButton
onClick={() => {
setIsModalUpdateOpen(true);
}}
>
<IconOptions
isOpen={isModalUpdateOpen}
aria-label={t('Open the team options modal')}
/>
</BoxButton>
<TeamActions currentRole={currentRole} team={team} />
</Box>
<Box className="m-b" $direction="row" $align="center" $gap="1.5rem">
<IconGroup

View File

@@ -22,6 +22,7 @@
"Create the team": "Créer le groupe",
"Create your first team by clicking on the \"Create a new team\" button.": "Créez votre premier groupe en cliquant sur le bouton \"Créer un nouveau groupe\".",
"Created at": "Créé le",
"Delete the team": "Supprimer le groupe",
"Desk": "Desk",
"Desk Logo": "Logo Desk",
"Emails": "Emails",
@@ -49,7 +50,7 @@
"Names": "Noms",
"New name...": "Nouveau nom...",
"Open the member options modal": "Ouvrir les options de membre dans la fenêtre modale",
"Open the team options modal": "Ouvrir les options de groupe dans la fenêtre modale",
"Open the team options": "Ouvrir les options de groupe dans la fenêtre modale",
"Owner": "Propriétaire",
"People": "People",
"People Description": "Description de People",
@@ -69,6 +70,7 @@
"The role has been updated": "Le rôle a bien été mis à jour",
"The team has been updated.": "Le groupe a été mise à jour.",
"Update team {{teamName}}": "Modification du groupe {{teamName}}",
"Update the team": "Mettre à jour le groupe",
"Update the role": "Mettre à jour ce rôle",
"Validate": "Valider",
"Validate the modification": "Valider la modification",

View File

@@ -56,7 +56,7 @@ const Team = ({ id }: TeamProps) => {
return (
<>
<TeamInfo team={team} />
<TeamInfo team={team} currentRole={currentRole} />
<MemberGrid team={team} currentRole={currentRole} />
</>
);

View File

@@ -46,3 +46,44 @@ export const createTeam = async (
return randomTeams;
};
export const addNewMember = async (
page: Page,
index: number,
role: 'Admin' | 'Owner' | 'Member',
fillText: string = 'test',
) => {
const responsePromiseSearchUser = page.waitForResponse(
(response) =>
response.url().includes(`/users/?q=${fillText}`) &&
response.status() === 200,
);
await page.getByLabel('Add members to the team').click();
const inputSearch = page.getByLabel(/Find a member to add to the team/);
// Select a new user
await inputSearch.fill(fillText);
// Intercept response
const responseSearchUser = await responsePromiseSearchUser;
const users = (await responseSearchUser.json()).results as {
name: string;
}[];
// Choose user
await page.getByRole('option', { name: users[index].name }).click();
// Choose a role
await page.getByRole('radio', { name: role }).click();
await page.getByRole('button', { name: 'Validate' }).click();
const table = page.getByLabel('List members card').getByRole('table');
await expect(table.getByText(users[index].name)).toBeVisible();
await expect(
page.getByText(`Member ${users[index].name} added to the team`),
).toBeVisible();
return users[index].name;
};

View File

@@ -44,7 +44,8 @@ test.describe('Team', () => {
test('it updates the team name', async ({ page, browserName }) => {
await createTeam(page, 'team-update-name', browserName, 1);
await page.getByLabel(`Open the team options modal`).click();
await page.getByLabel(`Open the team options`).click();
await page.getByRole('button', { name: `Update the team` }).click();
const teamName = randomName('new-team-update-name', browserName, 1)[0];
await page.getByText('New name...', { exact: true }).fill(teamName);

View File

@@ -0,0 +1,78 @@
import { expect, test } from '@playwright/test';
import { addNewMember, createTeam, keyCloakSignIn } from './common';
test.beforeEach(async ({ page, browserName }) => {
await page.goto('/');
await keyCloakSignIn(page, browserName);
});
test.describe('Teams Delete', () => {
test('it deletes the team when we are owner', async ({
page,
browserName,
}) => {
await createTeam(page, 'team-update-name', browserName, 1);
await page.getByLabel(`Open the team options`).click();
await page.getByRole('button', { name: `Delete the team` }).click();
await page.getByRole('button', { name: `Confirm deletion` }).click();
await expect(page.getByText(`The team has been removed.`)).toBeVisible();
await expect(
page.getByRole('button', { name: `Create a new team` }),
).toBeVisible();
});
test('it cannot delete the team when we are admin', async ({
page,
browserName,
}) => {
await createTeam(page, 'team-update-name', browserName, 1);
await addNewMember(page, 0, 'Owner');
// Change role to Admin
const table = page.getByLabel('List members card').getByRole('table');
const myCells = table
.getByRole('row')
.filter({ hasText: new RegExp(`E2E ${browserName}`, 'i') })
.getByRole('cell');
await myCells.nth(4).getByLabel('Member options').click();
await page.getByText('Update the role').click();
const radioGroup = page.getByLabel('Radio buttons to update the roles');
await radioGroup.getByRole('radio', { name: 'Admin' }).click();
await page.getByRole('button', { name: 'Validate' }).click();
// Delete the team button should be hidden
await page.getByLabel(`Open the team options`).click();
await expect(
page.getByRole('button', { name: `Delete the team` }),
).toBeHidden();
});
test('it cannot delete the team when we are member', async ({
page,
browserName,
}) => {
await createTeam(page, 'team-update-name', browserName, 1);
await addNewMember(page, 0, 'Owner');
// Change role to Admin
const table = page.getByLabel('List members card').getByRole('table');
const myCells = table
.getByRole('row')
.filter({ hasText: new RegExp(`E2E ${browserName}`, 'i') })
.getByRole('cell');
await myCells.nth(4).getByLabel('Member options').click();
await page.getByText('Update the role').click();
const radioGroup = page.getByLabel('Radio buttons to update the roles');
await radioGroup.getByRole('radio', { name: 'Member' }).click();
await page.getByRole('button', { name: 'Validate' }).click();
// Option button should be hidden
await expect(page.getByLabel(`Open the team options`)).toBeHidden();
});
});

View File

@@ -15,6 +15,12 @@ module.exports = {
rules: { ...common.globalRules, '@next/next/no-html-link-for-pages': 'off' },
overrides: [
...common.eslintTS,
{
files: ['**/*.ts'],
rules: {
'@typescript-eslint/no-unsafe-member-access': 'off',
},
},
{
files: ['*.spec.*', '*.test.*', '**/__mock__/**/*'],
extends: ['plugin:playwright/recommended'],