🛂(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:
@@ -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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,12 +3,13 @@ import React, { useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import IconGroup from '@/assets/icons/icon-group2.svg';
|
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 { useCunninghamTheme } from '@/cunningham';
|
||||||
|
|
||||||
import { Team } from '../api/types';
|
import { Role, Team } from '../types';
|
||||||
|
|
||||||
import { ModalUpdateTeam } from './ModalUpdateTeam';
|
import { ModalUpdateTeam } from './ModalUpdateTeam';
|
||||||
|
import { TeamActions } from './TeamActions';
|
||||||
|
|
||||||
const format: DateTimeFormatOptions = {
|
const format: DateTimeFormatOptions = {
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
@@ -18,9 +19,10 @@ const format: DateTimeFormatOptions = {
|
|||||||
|
|
||||||
interface TeamInfoProps {
|
interface TeamInfoProps {
|
||||||
team: Team;
|
team: Team;
|
||||||
|
currentRole: Role;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TeamInfo = ({ team }: TeamInfoProps) => {
|
export const TeamInfo = ({ team, currentRole }: TeamInfoProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { colorsTokens } = useCunninghamTheme();
|
const { colorsTokens } = useCunninghamTheme();
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
@@ -38,16 +40,7 @@ export const TeamInfo = ({ team }: TeamInfoProps) => {
|
|||||||
<>
|
<>
|
||||||
<Card className="m-b" style={{ paddingBottom: 0 }}>
|
<Card className="m-b" style={{ paddingBottom: 0 }}>
|
||||||
<Box $css="align-self: flex-end;" className="m-t" $position="absolute">
|
<Box $css="align-self: flex-end;" className="m-t" $position="absolute">
|
||||||
<BoxButton
|
<TeamActions currentRole={currentRole} team={team} />
|
||||||
onClick={() => {
|
|
||||||
setIsModalUpdateOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconOptions
|
|
||||||
isOpen={isModalUpdateOpen}
|
|
||||||
aria-label={t('Open the team options modal')}
|
|
||||||
/>
|
|
||||||
</BoxButton>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box className="m-b" $direction="row" $align="center" $gap="1.5rem">
|
<Box className="m-b" $direction="row" $align="center" $gap="1.5rem">
|
||||||
<IconGroup
|
<IconGroup
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"Create the team": "Créer le groupe",
|
"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\".",
|
"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",
|
"Created at": "Créé le",
|
||||||
|
"Delete the team": "Supprimer le groupe",
|
||||||
"Desk": "Desk",
|
"Desk": "Desk",
|
||||||
"Desk Logo": "Logo Desk",
|
"Desk Logo": "Logo Desk",
|
||||||
"Emails": "Emails",
|
"Emails": "Emails",
|
||||||
@@ -49,7 +50,7 @@
|
|||||||
"Names": "Noms",
|
"Names": "Noms",
|
||||||
"New name...": "Nouveau nom...",
|
"New name...": "Nouveau nom...",
|
||||||
"Open the member options modal": "Ouvrir les options de membre dans la fenêtre modale",
|
"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",
|
"Owner": "Propriétaire",
|
||||||
"People": "People",
|
"People": "People",
|
||||||
"People Description": "Description de People",
|
"People Description": "Description de People",
|
||||||
@@ -69,6 +70,7 @@
|
|||||||
"The role has been updated": "Le rôle a bien été mis à jour",
|
"The role has been updated": "Le rôle a bien été mis à jour",
|
||||||
"The team has been updated.": "Le groupe a été mise à jour.",
|
"The team has been updated.": "Le groupe a été mise à jour.",
|
||||||
"Update team {{teamName}}": "Modification du groupe {{teamName}}",
|
"Update team {{teamName}}": "Modification du groupe {{teamName}}",
|
||||||
|
"Update the team": "Mettre à jour le groupe",
|
||||||
"Update the role": "Mettre à jour ce rôle",
|
"Update the role": "Mettre à jour ce rôle",
|
||||||
"Validate": "Valider",
|
"Validate": "Valider",
|
||||||
"Validate the modification": "Valider la modification",
|
"Validate the modification": "Valider la modification",
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const Team = ({ id }: TeamProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TeamInfo team={team} />
|
<TeamInfo team={team} currentRole={currentRole} />
|
||||||
<MemberGrid team={team} currentRole={currentRole} />
|
<MemberGrid team={team} currentRole={currentRole} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,3 +46,44 @@ export const createTeam = async (
|
|||||||
|
|
||||||
return randomTeams;
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ test.describe('Team', () => {
|
|||||||
test('it updates the team name', async ({ page, browserName }) => {
|
test('it updates the team name', async ({ page, browserName }) => {
|
||||||
await createTeam(page, 'team-update-name', browserName, 1);
|
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];
|
const teamName = randomName('new-team-update-name', browserName, 1)[0];
|
||||||
await page.getByText('New name...', { exact: true }).fill(teamName);
|
await page.getByText('New name...', { exact: true }).fill(teamName);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,6 +15,12 @@ module.exports = {
|
|||||||
rules: { ...common.globalRules, '@next/next/no-html-link-for-pages': 'off' },
|
rules: { ...common.globalRules, '@next/next/no-html-link-for-pages': 'off' },
|
||||||
overrides: [
|
overrides: [
|
||||||
...common.eslintTS,
|
...common.eslintTS,
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
files: ['*.spec.*', '*.test.*', '**/__mock__/**/*'],
|
files: ['*.spec.*', '*.test.*', '**/__mock__/**/*'],
|
||||||
extends: ['plugin:playwright/recommended'],
|
extends: ['plugin:playwright/recommended'],
|
||||||
|
|||||||
Reference in New Issue
Block a user