✨(app-desk) add new member to team
We integrate the endpoint to add a new member to the team with the multi select seach user. - If it is a unknown email, it will send an invitation, - If it is a known user, it will add it to the team.
This commit is contained in:
@@ -5,6 +5,7 @@ import { User } from '@/features/auth';
|
||||
import { Team } from '@/features/teams';
|
||||
|
||||
import { Invitation, Role } from '../types';
|
||||
import { OptionType } from '../typesSearchMembers';
|
||||
|
||||
interface CreateInvitationParams {
|
||||
email: User['email'];
|
||||
@@ -28,7 +29,10 @@ export const createInvitation = async ({
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
`Failed to create the invitation for ${email}`,
|
||||
await errorCauses(response, email),
|
||||
await errorCauses(response, {
|
||||
value: email,
|
||||
type: OptionType.INVITATION,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { User } from '@/features/auth';
|
||||
import { KEY_LIST_TEAM, KEY_TEAM, Team } from '@/features/teams';
|
||||
|
||||
import { Access, Role } from '../types';
|
||||
import { OptionType } from '../typesSearchMembers';
|
||||
|
||||
import { KEY_LIST_TEAM_ACCESSES } from '.';
|
||||
|
||||
@@ -32,7 +33,10 @@ export const createTeamAccess = async ({
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
`Failed to add ${name} in the team.`,
|
||||
await errorCauses(response),
|
||||
await errorCauses(response, {
|
||||
value: name,
|
||||
type: OptionType.NEW_MEMBER,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M21.34 26.04C20.9 26.02 20.46 26 20 26C15.16 26 10.64 27.34 6.78 29.64C5.02 30.68 4 32.64 4 34.7V40H22.52C20.94 37.74 20 34.98 20 32C20 29.86 20.5 27.86 21.34 26.04Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M20 24C24.4183 24 28 20.4183 28 16C28 11.5817 24.4183 8 20 8C15.5817 8 12 11.5817 12 16C12 20.4183 15.5817 24 20 24Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M33 24C28.032 24 24 28.032 24 33C24 37.968 28.032 42 33 42C37.968 42 42 37.968 42 33C42 28.032 37.968 24 33 24ZM36.6 33.9H33.9V36.6C33.9 37.095 33.495 37.5 33 37.5C32.505 37.5 32.1 37.095 32.1 36.6V33.9H29.4C28.905 33.9 28.5 33.495 28.5 33C28.5 32.505 28.905 32.1 29.4 32.1H32.1V29.4C32.1 28.905 32.505 28.5 33 28.5C33.495 28.5 33.9 28.905 33.9 29.4V32.1H36.6C37.095 32.1 37.5 32.505 37.5 33C37.5 33.495 37.095 33.9 36.6 33.9Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 925 B |
@@ -11,13 +11,22 @@ import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
import { APIError } from '@/api';
|
||||
import { Box, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Team } from '@/features/teams';
|
||||
|
||||
import { useCreateInvitation } from '../api';
|
||||
import { Invitation, Role } from '../types';
|
||||
import { useCreateInvitation, useCreateTeamAccess } from '../api';
|
||||
import IconAddMember from '../assets/add-member.svg';
|
||||
import { Role } from '../types';
|
||||
import {
|
||||
OptionInvitation,
|
||||
OptionNewMember,
|
||||
OptionSelect,
|
||||
OptionType,
|
||||
isOptionNewMember,
|
||||
} from '../typesSearchMembers';
|
||||
|
||||
import { ChooseRole } from './ChooseRole';
|
||||
import { OptionSelect, SearchMembers } from './SearchMembers';
|
||||
import { OptionsSelect, SearchMembers } from './SearchMembers';
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
.c__modal {
|
||||
@@ -25,6 +34,11 @@ const GlobalStyle = createGlobalStyle`
|
||||
}
|
||||
`;
|
||||
|
||||
type APIErrorMember = APIError<{
|
||||
value: string;
|
||||
type: OptionType;
|
||||
}>;
|
||||
|
||||
interface ModalAddMembersProps {
|
||||
currentRole: Role;
|
||||
onClose: () => void;
|
||||
@@ -36,49 +50,81 @@ export const ModalAddMembers = ({
|
||||
onClose,
|
||||
team,
|
||||
}: ModalAddMembersProps) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { t } = useTranslation();
|
||||
const [selectedMembers, setSelectedMembers] = useState<OptionSelect>([]);
|
||||
const [selectedMembers, setSelectedMembers] = useState<OptionsSelect>([]);
|
||||
const [selectedRole, setSelectedRole] = useState<Role>(Role.MEMBER);
|
||||
const { toast } = useToastProvider();
|
||||
const { mutateAsync: createInvitation } = useCreateInvitation();
|
||||
const { mutateAsync: createTeamAccess } = useCreateTeamAccess();
|
||||
|
||||
const handleValidate = async () => {
|
||||
const promisesMembers = selectedMembers.map((selectedMember) => {
|
||||
return createInvitation({
|
||||
email: selectedMember.value.email,
|
||||
role: selectedRole,
|
||||
teamId: team.id,
|
||||
});
|
||||
const switchActions = (selectedMembers: OptionsSelect) =>
|
||||
selectedMembers.map(async (selectedMember) => {
|
||||
switch (selectedMember.type) {
|
||||
case OptionType.INVITATION:
|
||||
await createInvitation({
|
||||
email: selectedMember.value.email,
|
||||
role: selectedRole,
|
||||
teamId: team.id,
|
||||
});
|
||||
break;
|
||||
|
||||
case OptionType.NEW_MEMBER:
|
||||
await createTeamAccess({
|
||||
name: selectedMember.value.name,
|
||||
role: selectedRole,
|
||||
teamId: team.id,
|
||||
userId: selectedMember.value.id,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return selectedMember;
|
||||
});
|
||||
|
||||
const promises = await Promise.allSettled<Invitation>(promisesMembers);
|
||||
const toastOptions = {
|
||||
duration: 4000,
|
||||
};
|
||||
|
||||
const onError = (dataError: APIErrorMember['data']) => {
|
||||
const messageError =
|
||||
dataError?.type === OptionType.INVITATION
|
||||
? t(`Failed to create the invitation for {{email}}`, {
|
||||
email: dataError?.value,
|
||||
})
|
||||
: t(`Failed to add {{name}} in the team`, {
|
||||
name: dataError?.value,
|
||||
});
|
||||
|
||||
toast(messageError, VariantType.ERROR, toastOptions);
|
||||
};
|
||||
|
||||
const onSuccess = (option: OptionSelect) => {
|
||||
const message = !isOptionNewMember(option)
|
||||
? t('Invitation sent to {{email}}', {
|
||||
email: option.value.email,
|
||||
})
|
||||
: t('Member {{name}} added to the team', {
|
||||
name: option.value.name,
|
||||
});
|
||||
|
||||
toast(message, VariantType.SUCCESS, toastOptions);
|
||||
};
|
||||
|
||||
const handleValidate = async () => {
|
||||
const settledPromises = await Promise.allSettled<
|
||||
OptionInvitation | OptionNewMember
|
||||
>(switchActions(selectedMembers));
|
||||
|
||||
onClose();
|
||||
promises.forEach((promise) => {
|
||||
switch (promise.status) {
|
||||
settledPromises.forEach((settledPromise) => {
|
||||
switch (settledPromise.status) {
|
||||
case 'rejected':
|
||||
const apiError = promise.reason as APIError<string>;
|
||||
toast(
|
||||
t(`Failed to create the invitation for {{email}}`, {
|
||||
email: apiError.data,
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
{
|
||||
duration: 4000,
|
||||
},
|
||||
);
|
||||
onError((settledPromise.reason as APIErrorMember).data);
|
||||
break;
|
||||
|
||||
case 'fulfilled':
|
||||
toast(
|
||||
t('Invitation sent to {{email}}', {
|
||||
email: promise.value.email,
|
||||
}),
|
||||
VariantType.SUCCESS,
|
||||
{
|
||||
duration: 4000,
|
||||
},
|
||||
);
|
||||
onSuccess(settledPromise.value);
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -106,7 +152,14 @@ export const ModalAddMembers = ({
|
||||
</Button>
|
||||
}
|
||||
size={ModalSize.MEDIUM}
|
||||
title={t('Add members to the team')}
|
||||
title={
|
||||
<Box $align="center" $gap="1rem">
|
||||
<IconAddMember width={48} color={colorsTokens()['primary-text']} />
|
||||
<Text $size="h3" className="m-0">
|
||||
{t('Add a member')}
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<GlobalStyle />
|
||||
<Box className="mb-xl mt-l">
|
||||
|
||||
@@ -3,20 +3,17 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Options } from 'react-select';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
|
||||
import { User } from '@/features/auth';
|
||||
import { Team } from '@/features/teams';
|
||||
import { isValidEmail } from '@/utils';
|
||||
|
||||
import { KEY_LIST_USER, useUsers } from '../api/useUsers';
|
||||
import { OptionSelect, OptionType } from '../typesSearchMembers';
|
||||
|
||||
export type OptionSelect = Options<{
|
||||
value: Partial<User> & { email: User['email'] };
|
||||
label: string;
|
||||
}>;
|
||||
export type OptionsSelect = Options<OptionSelect>;
|
||||
|
||||
interface SearchMembersProps {
|
||||
team: Team;
|
||||
setSelectedMembers: (value: OptionSelect) => void;
|
||||
setSelectedMembers: (value: OptionsSelect) => void;
|
||||
}
|
||||
|
||||
export const SearchMembers = ({
|
||||
@@ -26,7 +23,7 @@ export const SearchMembers = ({
|
||||
const { t } = useTranslation();
|
||||
const [input, setInput] = useState('');
|
||||
const [userQuery, setUserQuery] = useState('');
|
||||
const resolveOptionsRef = useRef<((value: OptionSelect) => void) | null>(
|
||||
const resolveOptionsRef = useRef<((value: OptionsSelect) => void) | null>(
|
||||
null,
|
||||
);
|
||||
const { data } = useUsers(
|
||||
@@ -44,9 +41,10 @@ export const SearchMembers = ({
|
||||
return;
|
||||
}
|
||||
|
||||
let users: OptionSelect = options.map((user) => ({
|
||||
let users: OptionsSelect = options.map((user) => ({
|
||||
value: user,
|
||||
label: user.name || '',
|
||||
label: user.name || user.email,
|
||||
type: OptionType.NEW_MEMBER,
|
||||
}));
|
||||
|
||||
if (userQuery && isValidEmail(userQuery)) {
|
||||
@@ -57,6 +55,7 @@ export const SearchMembers = ({
|
||||
{
|
||||
value: { email: userQuery },
|
||||
label: userQuery,
|
||||
type: OptionType.INVITATION,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -67,8 +66,8 @@ export const SearchMembers = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [options]);
|
||||
|
||||
const loadOptions = (): Promise<OptionSelect> => {
|
||||
return new Promise<OptionSelect>((resolve) => {
|
||||
const loadOptions = (): Promise<OptionsSelect> => {
|
||||
return new Promise<OptionsSelect>((resolve) => {
|
||||
resolveOptionsRef.current = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { User } from '@/features/auth';
|
||||
|
||||
export enum OptionType {
|
||||
INVITATION = 'invitation',
|
||||
NEW_MEMBER = 'new_member',
|
||||
}
|
||||
|
||||
export const isOptionNewMember = (
|
||||
data: OptionSelect,
|
||||
): data is OptionNewMember => {
|
||||
return 'id' in data.value;
|
||||
};
|
||||
|
||||
export interface OptionInvitation {
|
||||
value: { email: string };
|
||||
label: string;
|
||||
type: OptionType.INVITATION;
|
||||
}
|
||||
|
||||
export interface OptionNewMember {
|
||||
value: User;
|
||||
label: string;
|
||||
type: OptionType.NEW_MEMBER;
|
||||
}
|
||||
|
||||
export type OptionSelect = OptionNewMember | OptionInvitation;
|
||||
@@ -5,6 +5,7 @@
|
||||
"404 - Page not found": "404 - Page introuvable",
|
||||
"Access to the cells menu": "Accès au menu cellules",
|
||||
"Add": "Ajouter",
|
||||
"Add a member": "Ajouter un membre",
|
||||
"Add a team": "Ajouter un groupe",
|
||||
"Add members to the team": "Ajoutez des membres à votre groupe",
|
||||
"Add people to the “{{teamName}}“ group.": "Ajouter des personnes au groupe “{{teamName}}“.",
|
||||
@@ -27,6 +28,7 @@
|
||||
"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 add {{name}} in the team": "Échec de l'ajout de {{name}} dans le 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",
|
||||
@@ -41,6 +43,7 @@
|
||||
"Marianne Logo": "Logo Marianne",
|
||||
"Member": "Membre",
|
||||
"Member icon": "Icône de membre",
|
||||
"Member {{name}} added to the team": "Membre {{name}} ajouté au groupe",
|
||||
"Members of “{{teamName}}“": "Membres de “{{teamName}}“",
|
||||
"Name the team": "Nommer le groupe",
|
||||
"Names": "Noms",
|
||||
|
||||
@@ -25,7 +25,7 @@ test.describe('Members Create', () => {
|
||||
await expect(page.getByRole('button', { name: 'Cancel' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('it selects 2 users', async ({ page, browserName }) => {
|
||||
test('it selects 2 users and 1 invitation', async ({ page, browserName }) => {
|
||||
const responsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/users/?q=test') && response.status() === 200,
|
||||
@@ -38,53 +38,192 @@ test.describe('Members Create', () => {
|
||||
|
||||
const inputSearch = page.getByLabel(/Find a member to add to the team/);
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
await inputSearch.fill('test');
|
||||
// Select user 1
|
||||
await inputSearch.fill('test');
|
||||
|
||||
const response = await responsePromise;
|
||||
const users = (await response.json()).results as {
|
||||
name: string;
|
||||
}[];
|
||||
const response = await responsePromise;
|
||||
const users = (await response.json()).results as {
|
||||
name: string;
|
||||
}[];
|
||||
|
||||
await page.getByText(users[i].name).click();
|
||||
await page.getByRole('option', { name: users[0].name }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText(`${users[i].name}`, { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByLabel(`Remove ${users[i].name}`)).toBeVisible();
|
||||
}
|
||||
// Select user 2
|
||||
await inputSearch.fill('test1');
|
||||
await page.getByRole('option', { name: users[1].name }).click();
|
||||
|
||||
// Select email
|
||||
const email = randomName('test@test.fr', browserName, 1)[0];
|
||||
await inputSearch.fill(email);
|
||||
await page.getByRole('option', { name: email }).click();
|
||||
|
||||
// Check user 1 tag
|
||||
await expect(
|
||||
page.getByText(`${users[0].name}`, { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByLabel(`Remove ${users[0].name}`)).toBeVisible();
|
||||
|
||||
// Check user 2 tag
|
||||
await expect(
|
||||
page.getByText(`${users[1].name}`, { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByLabel(`Remove ${users[1].name}`)).toBeVisible();
|
||||
|
||||
// Check invitation tag
|
||||
await expect(page.getByText(email, { exact: true })).toBeVisible();
|
||||
await expect(page.getByLabel(`Remove ${email}`)).toBeVisible();
|
||||
|
||||
// Check roles are displayed
|
||||
await expect(page.getByText(/Choose a role/)).toBeVisible();
|
||||
await expect(page.getByRole('radio', { name: 'Member' })).toBeChecked();
|
||||
await expect(page.getByRole('radio', { name: 'Owner' })).toBeVisible();
|
||||
await expect(page.getByRole('radio', { name: 'Admin' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('it sends an invitation', async ({ page, browserName }) => {
|
||||
test('it sends a new invitation and adds a new member', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const responsePromiseSearchUser = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/users/?q=test') && response.status() === 200,
|
||||
);
|
||||
|
||||
await createTeam(page, 'member-invitation', browserName, 1);
|
||||
|
||||
await page.getByLabel('Add members to the team').click();
|
||||
|
||||
// Select a new email
|
||||
const inputSearch = page.getByLabel(/Find a member to add to the team/);
|
||||
|
||||
const email = randomName('test@test.fr', browserName, 1)[0];
|
||||
await inputSearch.fill(email);
|
||||
await page.getByRole('option', { name: email }).click();
|
||||
|
||||
// Select a new user
|
||||
await inputSearch.fill('test');
|
||||
const responseSearchUser = await responsePromiseSearchUser;
|
||||
const users = (await responseSearchUser.json()).results as {
|
||||
name: string;
|
||||
}[];
|
||||
await page.getByRole('option', { name: users[0].name }).click();
|
||||
|
||||
// Choose a role
|
||||
await page.getByRole('radio', { name: 'Admin' }).click();
|
||||
|
||||
const responsePromiseCreateInvitation = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/invitations/') && response.status() === 201,
|
||||
);
|
||||
const responsePromiseAddMember = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/accesses/') && response.status() === 201,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Validate' }).click();
|
||||
|
||||
// Check invitation sent
|
||||
await expect(page.getByText(`Invitation sent to ${email}`)).toBeVisible();
|
||||
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
||||
expect(responseCreateInvitation.ok()).toBeTruthy();
|
||||
|
||||
// Check member added
|
||||
await expect(
|
||||
page.getByText(`Member ${users[0].name} added to the team`),
|
||||
).toBeVisible();
|
||||
const responseAddMember = await responsePromiseAddMember;
|
||||
expect(responseAddMember.ok()).toBeTruthy();
|
||||
|
||||
const table = page.getByLabel('List members card').getByRole('table');
|
||||
await expect(table.getByText(users[0].name)).toBeVisible();
|
||||
await expect(table.getByText('Admin')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it try to add twice the same user', async ({ page, browserName }) => {
|
||||
const responsePromiseSearchUser = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/users/?q=test') && response.status() === 200,
|
||||
);
|
||||
|
||||
await createTeam(page, 'member-twice', 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');
|
||||
const responseSearchUser = await responsePromiseSearchUser;
|
||||
const users = (await responseSearchUser.json()).results as {
|
||||
name: string;
|
||||
}[];
|
||||
await page.getByRole('option', { name: users[0].name }).click();
|
||||
|
||||
// Choose a role
|
||||
await page.getByRole('radio', { name: 'Owner' }).click();
|
||||
|
||||
const responsePromiseAddMember = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/accesses/') && response.status() === 201,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Validate' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText(`Member ${users[0].name} added to the team`),
|
||||
).toBeVisible();
|
||||
const responseAddMember = await responsePromiseAddMember;
|
||||
expect(responseAddMember.ok()).toBeTruthy();
|
||||
|
||||
await page.getByLabel('Add members to the team').click();
|
||||
|
||||
await inputSearch.fill('test');
|
||||
await page.getByRole('option', { name: users[0].name }).click();
|
||||
await page.getByRole('radio', { name: 'Owner' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Validate' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText(`Failed to add ${users[0].name} in the team`),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('it try to add twice the same invitation', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await createTeam(page, 'invitation-twice', browserName, 1);
|
||||
|
||||
await page.getByLabel('Add members to the team').click();
|
||||
|
||||
const inputSearch = page.getByLabel(/Find a member to add to the team/);
|
||||
|
||||
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();
|
||||
|
||||
// Choose a role
|
||||
await page.getByRole('radio', { name: 'Owner' }).click();
|
||||
|
||||
const responsePromise = page.waitForResponse(
|
||||
const responsePromiseCreateInvitation = 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();
|
||||
// Check invitation sent
|
||||
await expect(page.getByText(`Invitation sent to ${email}`)).toBeVisible();
|
||||
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
||||
expect(responseCreateInvitation.ok()).toBeTruthy();
|
||||
|
||||
await page.getByLabel('Add members to the team').click();
|
||||
|
||||
await inputSearch.fill(email);
|
||||
await page.getByRole('option', { name: email }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Validate' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText(`Failed to create the invitation for ${email}`),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user