(frontend) add modal to delete a member

Add modal to delete a member.
This commit is contained in:
Anthony LC
2024-05-31 17:16:31 +02:00
committed by Anthony LC
parent cce0331b68
commit 197b16c5d0
6 changed files with 417 additions and 13 deletions

View File

@@ -86,15 +86,18 @@ export const addNewMember = async (
page: Page,
index: number,
role: 'Admin' | 'Owner' | 'Member',
fillText: string = 'test',
fillText: string = 'user',
) => {
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/);
await page.getByLabel('Open the document options').click();
await page.getByRole('button', { name: 'Add members' }).click();
const inputSearch = page.getByLabel(/Find a member to add to the document/);
// Select a new user
await inputSearch.fill(fillText);
@@ -102,23 +105,18 @@ export const addNewMember = async (
// Intercept response
const responseSearchUser = await responsePromiseSearchUser;
const users = (await responseSearchUser.json()).results as {
name: string;
email: string;
}[];
// Choose user
await page.getByRole('option', { name: users[index].name }).click();
await page.getByRole('option', { name: users[index].email }).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(page.getByText(`User added to the document.`)).toBeVisible();
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;
return users[index].email;
};

View File

@@ -0,0 +1,199 @@
import { expect, test } from '@playwright/test';
import { addNewMember, createPad, keyCloakSignIn } from './common';
test.beforeEach(async ({ page, browserName }) => {
await page.goto('/');
await keyCloakSignIn(page, browserName);
});
test.describe('Members Delete', () => {
test('it cannot delete himself when it is the last owner', async ({
page,
browserName,
}) => {
await createPad(page, 'member-delete-1', browserName, 1);
await page.getByLabel('Open the document options').click();
await page.getByRole('button', { name: 'Manage members' }).click();
const table = page.getByLabel('List members card').getByRole('table');
const cells = table.getByRole('row').nth(1).getByRole('cell');
await expect(cells.nth(0)).toHaveText(
new RegExp(`user@${browserName}.e2e`, 'i'),
);
await cells.nth(2).getByLabel('Member options').click();
await page.getByLabel('Open the modal to delete this member').click();
await expect(
page.getByText(
'You are the last owner, you cannot be removed from your document.',
),
).toBeVisible();
await expect(page.getByRole('button', { name: 'Validate' })).toBeDisabled();
});
test('it deletes himself when it is not the last owner', async ({
page,
browserName,
}) => {
await createPad(page, 'member-delete-2', browserName, 1);
await addNewMember(page, 0, 'Owner');
await page.getByLabel('Open the document options').click();
await page.getByRole('button', { name: 'Manage members' }).click();
const table = page.getByLabel('List members card').getByRole('table');
// find row where regexp match the name
const cells = table
.getByRole('row')
.filter({ hasText: new RegExp(`user@${browserName}.e2e`, 'i') })
.getByRole('cell');
await cells.nth(2).getByLabel('Member options').click();
await page.getByLabel('Open the modal to delete this member').click();
await page.getByRole('button', { name: 'Validate' }).click();
await expect(
page.getByText(`The member has been removed from the document`),
).toBeVisible();
await expect(
page.getByRole('button', { name: `Create a new document` }),
).toBeVisible();
});
test('it cannot delete owner member', async ({ page, browserName }) => {
await createPad(page, 'member-delete-3', browserName, 1);
const username = await addNewMember(page, 0, 'Owner');
await page.getByLabel('Open the document options').click();
await page.getByRole('button', { name: 'Manage members' }).click();
const table = page.getByLabel('List members card').getByRole('table');
// find row where regexp match the name
const cells = table
.getByRole('row')
.filter({ hasText: username })
.getByRole('cell');
await cells.getByLabel('Member options').click();
await page.getByLabel('Open the modal to delete this member').click();
await expect(
page.getByText(`You cannot remove other owner.`),
).toBeVisible();
await expect(page.getByRole('button', { name: 'Validate' })).toBeDisabled();
});
test('it deletes admin member', async ({ page, browserName }) => {
await createPad(page, 'member-delete-4', browserName, 1);
const username = await addNewMember(page, 0, 'Admin');
await page.getByLabel('Open the document options').click();
await page.getByRole('button', { name: 'Manage members' }).click();
const table = page.getByLabel('List members card').getByRole('table');
// find row where regexp match the name
const cells = table
.getByRole('row')
.filter({ hasText: username })
.getByRole('cell');
await cells.getByLabel('Member options').click();
await page.getByLabel('Open the modal to delete this member').click();
await page.getByRole('button', { name: 'Validate' }).click();
await expect(
page.getByText(`The member has been removed from the document`),
).toBeVisible();
await expect(table.getByText(username)).toBeHidden();
});
test('it cannot delete owner member when admin', async ({
page,
browserName,
}) => {
await createPad(page, 'member-delete-5', browserName, 1);
const username = await addNewMember(page, 0, 'Owner');
await page.getByLabel('Open the document options').click();
await page.getByRole('button', { name: 'Manage members' }).click();
const table = page.getByLabel('List members card').getByRole('table');
// find row where regexp match the name
const myCells = table
.getByRole('row')
.filter({ hasText: new RegExp(`user@${browserName}.e2e`, 'i') })
.getByRole('cell');
await myCells.getByLabel('Member options').click();
// Change role to Admin
await page.getByText('Update role').click();
const radioGroup = page.getByLabel('Radio buttons to update the roles');
await radioGroup.getByRole('radio', { name: 'Administrator' }).click();
await page.getByRole('button', { name: 'Validate' }).click();
const cells = table
.getByRole('row')
.filter({ hasText: username })
.getByRole('cell');
await expect(cells.getByLabel('Member options')).toBeHidden();
});
test('it deletes admin member when admin', async ({ page, browserName }) => {
await createPad(page, 'member-delete-6', browserName, 1);
// To not be the only owner
await addNewMember(page, 0, 'Owner');
const username = await addNewMember(page, 1, 'Admin');
await expect(
page.getByText(`User added to the document.`).last(),
).toBeHidden({
timeout: 5000,
});
await page.getByLabel('Open the document options').click();
await page.getByRole('button', { name: 'Manage members' }).click();
const table = page.getByLabel('List members card').getByRole('table');
// find row where regexp match the name
const myCells = table
.getByRole('row')
.filter({ hasText: new RegExp(`user@${browserName}.e2e`, 'i') })
.getByRole('cell');
await myCells.getByLabel('Member options').click();
// Change role to Admin
await page.getByText('Update role').click();
const radioGroup = page.getByLabel('Radio buttons to update the roles');
await radioGroup.getByRole('radio', { name: 'Administrator' }).click();
await page.getByRole('button', { name: 'Validate' }).click();
await expect(page.getByText(`The role has been updated`)).toBeVisible();
await expect(page.getByText(`The role has been updated`)).toBeHidden({
timeout: 5000,
});
const cells = table
.getByRole('row')
.filter({ hasText: new RegExp(username, 'i') })
.getByRole('cell');
await cells.nth(2).getByLabel('Member options').click();
await page.getByLabel('Open the modal to delete this member').click();
await page.getByRole('button', { name: 'Validate' }).click();
await expect(
page.getByText(`The member has been removed from the document`),
).toBeVisible();
await expect(table.getByText(username)).toBeHidden();
});
});

View File

@@ -0,0 +1,64 @@
import {
UseMutationOptions,
useMutation,
useQueryClient,
} from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { KEY_LIST_PAD, KEY_PAD } from '@/features/pads/pad-management';
import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses';
interface DeleteDocAccessProps {
docId: string;
accessId: string;
}
export const deleteDocAccess = async ({
docId,
accessId,
}: DeleteDocAccessProps): Promise<void> => {
const response = await fetchAPI(`documents/${docId}/accesses/${accessId}/`, {
method: 'DELETE',
});
if (!response.ok) {
throw new APIError(
'Failed to delete the member',
await errorCauses(response),
);
}
};
type UseDeleteDocAccessOptions = UseMutationOptions<
void,
APIError,
DeleteDocAccessProps
>;
export const useDeleteDocAccess = (options?: UseDeleteDocAccessOptions) => {
const queryClient = useQueryClient();
return useMutation<void, APIError, DeleteDocAccessProps>({
mutationFn: deleteDocAccess,
...options,
onSuccess: (data, variables, context) => {
void queryClient.invalidateQueries({
queryKey: [KEY_LIST_DOC_ACCESSES],
});
void queryClient.invalidateQueries({
queryKey: [KEY_PAD],
});
void queryClient.invalidateQueries({
queryKey: [KEY_LIST_PAD],
});
if (options?.onSuccess) {
options.onSuccess(data, variables, context);
}
},
onError: (error, variables, context) => {
if (options?.onError) {
options.onError(error, variables, context);
}
},
});
};

View File

@@ -0,0 +1,8 @@
<svg viewBox="0 0 48 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.85735 7.9065L0 3.05123L3.05331 0L41.593 38.5397L38.5418 41.591L28.4121 31.4612C27.3276 31.7198 26.2034 31.8554 25.0584 31.8554C22.0135 31.8554 19.1145 30.896 16.7161 29.2275V33.941H2.11688V25.5986C2.11688 23.3045 3.99392 21.4274 6.28807 21.4274H13.108C14.5262 21.4274 15.7984 22.1991 16.6118 23.3462C18.6369 26.2055 21.7569 27.5528 24.6246 27.6759L21.953 25.0021C20.5035 24.4223 19.2334 23.4421 18.322 22.1574C17.0706 20.4055 15.2144 19.4044 13.2123 19.3627C13.5397 18.8037 14.0819 18.2886 14.7765 17.8256L13.5522 16.6013C12.4281 18.2573 10.5302 19.3418 8.37367 19.3418C4.91158 19.3418 2.11688 16.5471 2.11688 13.085C2.11688 10.9285 3.20139 9.03063 4.85735 7.9065ZM24.1804 15.1915C24.4786 15.1769 24.7727 15.1706 25.0584 15.1706C29.3965 15.1706 35.403 16.7974 36.9046 19.3418C34.9025 19.3835 33.0463 20.3846 31.7949 22.1365C31.7031 22.2679 31.6072 22.3951 31.5071 22.5203L24.1804 15.1915ZM32.9962 24.0094C33.3174 23.634 33.4863 23.3754 33.5051 23.3462C34.1933 22.3659 35.403 21.4274 37.0089 21.4274H43.8288C46.123 21.4274 48 23.3045 48 25.5986V33.941H42.9299L32.9962 24.0094ZM41.7432 19.3418C38.2811 19.3418 35.4864 16.5471 35.4864 13.085C35.4864 9.62294 38.2811 6.82824 41.7432 6.82824C45.2053 6.82824 48 9.62294 48 13.085C48 16.5471 45.2053 19.3418 41.7432 19.3418ZM25.0584 13.085C21.5964 13.085 18.8017 10.2903 18.8017 6.82824C18.8017 3.36615 21.5964 0.571453 25.0584 0.571453C28.5205 0.571453 31.3152 3.36615 31.3152 6.82824C31.3152 10.2903 28.5205 13.085 25.0584 13.085Z"
fill="currentColor"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -65,7 +65,7 @@ export const MemberAction = ({
color="primary-text"
icon={<span className="material-icons">delete</span>}
>
<Text $theme="primary">{t('Remove from group')}</Text>
{t('Remove from group')}
</Button>
</Box>
</DropButton>

View File

@@ -0,0 +1,135 @@
import {
Button,
Modal,
ModalSize,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
import { t } from 'i18next';
import { useRouter } from 'next/navigation';
import IconUser from '@/assets/icons/icon-user.svg';
import { Box, Text, TextErrors } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Access, Pad, Role } from '@/features/pads/pad-management';
import { useDeleteDocAccess } from '../api';
import IconRemoveMember from '../assets/icon-remove-member.svg';
import { useWhoAmI } from '../hooks/useWhoAmI';
interface ModalDeleteProps {
access: Access;
currentRole: Role;
onClose: () => void;
doc: Pad;
}
export const ModalDelete = ({ access, onClose, doc }: ModalDeleteProps) => {
const { toast } = useToastProvider();
const { colorsTokens } = useCunninghamTheme();
const router = useRouter();
const { isMyself, isLastOwner, isOtherOwner } = useWhoAmI(access);
const isNotAllowed = isOtherOwner || isLastOwner;
const {
mutate: removeDocAccess,
error: errorUpdate,
isError: isErrorUpdate,
} = useDeleteDocAccess({
onSuccess: () => {
toast(
t('The member has been removed from the document'),
VariantType.SUCCESS,
{
duration: 4000,
},
);
// If we remove ourselves, we redirect to the home page
// because we are no longer part of the team
isMyself ? router.push('/') : onClose();
},
});
return (
<Modal
isOpen
closeOnClickOutside
hideCloseButton
leftActions={
<Button color="secondary" fullWidth onClick={() => onClose()}>
{t('Cancel')}
</Button>
}
onClose={onClose}
rightActions={
<Button
color="primary"
fullWidth
onClick={() => {
removeDocAccess({
docId: doc.id,
accessId: access.id,
});
}}
disabled={isNotAllowed}
>
{t('Validate')}
</Button>
}
size={ModalSize.MEDIUM}
title={
<Box $align="center" $gap="1rem">
<IconRemoveMember width={48} color={colorsTokens()['primary-text']} />
<Text $size="h3" $margin="none">
{t('Remove the member')}
</Text>
</Box>
}
>
<Box aria-label={t('Radio buttons to update the roles')}>
<Text>
{t('Are you sure you want to remove this member from the document?')}
</Text>
{isErrorUpdate && (
<TextErrors
$margin={{ bottom: 'small' }}
causes={errorUpdate.cause}
/>
)}
{(isLastOwner || isOtherOwner) && (
<Text
$theme="warning"
$direction="row"
$align="center"
$gap="0.5rem"
$margin="tiny"
$justify="center"
>
<span className="material-icons">warning</span>
{isLastOwner &&
t(
'You are the last owner, you cannot be removed from your document.',
)}
{isOtherOwner && t('You cannot remove other owner.')}
</Text>
)}
<Text
as="p"
$padding="big"
$direction="row"
$gap="0.5rem"
$background={colorsTokens()['primary-150']}
$theme="primary"
>
<IconUser width={20} height={20} />
<Text>{access.user.email}</Text>
</Text>
</Box>
</Modal>
);
};