diff --git a/src/frontend/apps/desk/src/features/members/components/ModalAddMembers.tsx b/src/frontend/apps/desk/src/features/members/components/ModalAddMembers.tsx index 90c2984..1a353f9 100644 --- a/src/frontend/apps/desk/src/features/members/components/ModalAddMembers.tsx +++ b/src/frontend/apps/desk/src/features/members/components/ModalAddMembers.tsx @@ -1,12 +1,20 @@ -import { Button, Modal, ModalSize } from '@openfun/cunningham-react'; +import { + Button, + Modal, + ModalSize, + VariantType, + useToastProvider, +} from '@openfun/cunningham-react'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { createGlobalStyle } from 'styled-components'; +import { APIError } from '@/api'; import { Box, Text } from '@/components'; import { Team } from '@/features/teams'; -import { Role } from '../types'; +import { useCreateInvitation } from '../api'; +import { Invitation, Role } from '../types'; import { ChooseRole } from './ChooseRole'; import { OptionSelect, SearchMembers } from './SearchMembers'; @@ -30,7 +38,51 @@ export const ModalAddMembers = ({ }: ModalAddMembersProps) => { const { t } = useTranslation(); const [selectedMembers, setSelectedMembers] = useState([]); - const [, setSelectedRole] = useState(Role.MEMBER); + const [selectedRole, setSelectedRole] = useState(Role.MEMBER); + const { toast } = useToastProvider(); + const { mutateAsync: createInvitation } = useCreateInvitation(); + + const handleValidate = async () => { + const promisesMembers = selectedMembers.map((selectedMember) => { + return createInvitation({ + email: selectedMember.value.email, + role: selectedRole, + teamId: team.id, + }); + }); + + const promises = await Promise.allSettled(promisesMembers); + + onClose(); + promises.forEach((promise) => { + switch (promise.status) { + case 'rejected': + const apiError = promise.reason as APIError; + toast( + t(`Failed to create the invitation for {{email}}`, { + email: apiError.data, + }), + VariantType.ERROR, + { + duration: 4000, + }, + ); + break; + + case 'fulfilled': + toast( + t('Invitation sent to {{email}}', { + email: promise.value.email, + }), + VariantType.SUCCESS, + { + duration: 4000, + }, + ); + break; + } + }); + }; return ( {}} + onClick={() => void handleValidate()} > {t('Validate')} diff --git a/src/frontend/apps/desk/src/i18n/translations.json b/src/frontend/apps/desk/src/i18n/translations.json index 9e77fd4..cbd421a 100644 --- a/src/frontend/apps/desk/src/i18n/translations.json +++ b/src/frontend/apps/desk/src/i18n/translations.json @@ -27,10 +27,12 @@ "Empty teams icon": "Icône de groupe vide", "Enter the new name of the selected team": "Entrez le nouveau nom du groupe sélectionné", "Enter the new team name": "Entrez le nouveau nom de groupe", + "Failed to create the invitation for {{email}}": "Échec de la création de l'invitation pour {{email}}", "Favorite": "Favoris", "Find a member to add to the team": "Trouver un membre à ajouter au groupe", "Freedom Equality Fraternity Logo": "Logo Liberté Égalité Fraternité", "Groups": "Groupes", + "Invitation sent to {{email}}": "Invitation envoyée à {{email}}", "Invite new members to {{teamName}}": "Invitez de nouveaux membres à rejoindre {{teamName}}", "Language": "Langue", "Language Icon": "Icône de langue", diff --git a/src/frontend/apps/e2e/__tests__/app-desk/common.ts b/src/frontend/apps/e2e/__tests__/app-desk/common.ts index 08203d0..d231814 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/common.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/common.ts @@ -18,13 +18,9 @@ export const keyCloakSignIn = async (page: Page, browserName: string) => { } }; -export const randomTeamsName = ( - teamName: string, - browserName: string, - length: number, -) => +export const randomName = (name: string, browserName: string, length: number) => Array.from({ length }, (_el, index) => { - return `${teamName}-${browserName}-${Math.floor(Math.random() * 10000)}-${index}`; + return `${browserName}-${Math.floor(Math.random() * 10000)}-${index}-${name}`; }); export const createTeam = async ( @@ -36,7 +32,7 @@ export const createTeam = async ( const panel = page.getByLabel('Teams panel').first(); const buttonCreate = page.getByRole('button', { name: 'Create the team' }); - const randomTeams = randomTeamsName(teamName, browserName, length); + const randomTeams = randomName(teamName, browserName, length); for (let i = 0; i < randomTeams.length; i++) { await panel.getByRole('button', { name: 'Add a team' }).click(); diff --git a/src/frontend/apps/e2e/__tests__/app-desk/member-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/member-create.spec.ts index e0323c3..d992db1 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/member-create.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/member-create.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -import { createTeam, keyCloakSignIn } from './common'; +import { createTeam, keyCloakSignIn, randomName } from './common'; test.beforeEach(async ({ page, browserName }) => { await page.goto('/'); @@ -60,16 +60,31 @@ test.describe('Members Create', () => { await expect(page.getByRole('radio', { name: 'Admin' })).toBeVisible(); }); - test('it selects non existing email', async ({ page, browserName }) => { - await createTeam(page, 'member-modal-search-user', browserName, 1); + test('it sends an invitation', async ({ page, browserName }) => { + await createTeam(page, 'member-invitation', browserName, 1); await page.getByLabel('Add members to the team').click(); const inputSearch = page.getByLabel(/Find a member to add to the team/); - await inputSearch.fill('test@test.fr'); - await page.getByRole('option', { name: 'test@test.fr' }).click(); - await expect(page.getByText('test@test.fr', { exact: true })).toBeVisible(); - await expect(page.getByLabel(`Remove test@test.fr`)).toBeVisible(); + const email = randomName('test@test.fr', browserName, 1)[0]; + await inputSearch.fill(email); + await page.getByRole('option', { name: email }).click(); + + await expect(page.getByText(email, { exact: true })).toBeVisible(); + await expect(page.getByLabel(`Remove ${email}`)).toBeVisible(); + + await page.getByRole('radio', { name: 'Owner' }).click(); + + const responsePromise = page.waitForResponse( + (response) => + response.url().includes('/invitations/') && response.status() === 201, + ); + + await page.getByRole('button', { name: 'Validate' }).click(); + await expect(page.getByText(`Invitation sent to ${email}`)).toBeVisible(); + + const response = await responsePromise; + expect(response.ok()).toBeTruthy(); }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-desk/team.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/team.spec.ts index fd3370f..6d287e9 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/team.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/team.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -import { createTeam, keyCloakSignIn, randomTeamsName } from './common'; +import { createTeam, keyCloakSignIn, randomName } from './common'; test.beforeEach(async ({ page, browserName }) => { await page.goto('/'); @@ -46,7 +46,7 @@ test.describe('Team', () => { await page.getByLabel(`Open the team options modal`).click(); - const teamName = randomTeamsName('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