(app-desk) integrate modal to update roles

Integrate the design and functionality
for updating a member's role.
Managed use cases:
 - when the user is an admin
 - when the user is the last owner
 - when the user want to update other orner
This commit is contained in:
Anthony LC
2024-03-08 15:18:19 +01:00
committed by Anthony LC
parent 0648c2e8d3
commit e15c7cb2f4
6 changed files with 483 additions and 2 deletions

View File

@@ -434,3 +434,10 @@ input:-webkit-autofill:focus {
--c--components--button--danger--background--color-disabled
);
}
/**
* Modal
*/
.c__modal__backdrop {
z-index: 1000;
}

View File

@@ -0,0 +1,286 @@
import '@testing-library/jest-dom';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
import { useAuthStore } from '@/features/auth';
import { AppWrapper } from '@/tests/utils';
import { Access, Role } from '../api';
import { ModalRole } from '../components/Member/ModalRole';
const toast = jest.fn();
jest.mock('@openfun/cunningham-react', () => ({
...jest.requireActual('@openfun/cunningham-react'),
useToastProvider: () => ({
toast,
}),
}));
HTMLDialogElement.prototype.showModal = jest.fn(function mock(
this: HTMLDialogElement,
) {
this.open = true;
});
const access: Access = {
id: '789',
role: Role.ADMIN,
user: {
id: '11',
name: 'username1',
email: 'user1@test.com',
},
abilities: {
set_role_to: [Role.MEMBER, Role.ADMIN],
} as any,
};
describe('ModalRole', () => {
afterEach(() => {
fetchMock.restore();
});
it('checks the cancel button', async () => {
const onClose = jest.fn();
render(
<ModalRole
access={access}
currentRole={Role.ADMIN}
onClose={onClose}
teamId="123"
/>,
{
wrapper: AppWrapper,
},
);
await userEvent.click(
screen.getByRole('button', {
name: 'Cancel',
}),
);
expect(onClose).toHaveBeenCalled();
});
it('updates the role successfully', async () => {
fetchMock.patchOnce(`/api/teams/123/accesses/789/`, {
status: 200,
ok: true,
});
const onClose = jest.fn();
render(
<ModalRole
access={access}
currentRole={Role.OWNER}
onClose={onClose}
teamId="123"
/>,
{ wrapper: AppWrapper },
);
expect(
screen.getByRole('radio', {
name: 'Admin',
}),
).toBeChecked();
await userEvent.click(
screen.getByRole('radio', {
name: 'Member',
}),
);
await userEvent.click(
screen.getByRole('button', {
name: 'Validate',
}),
);
await waitFor(() => {
expect(toast).toHaveBeenCalledWith(
'The role has been updated',
'success',
{
duration: 4000,
},
);
});
expect(fetchMock.lastUrl()).toBe(`/api/teams/123/accesses/789/`);
expect(onClose).toHaveBeenCalled();
});
it('fails to update the role', async () => {
fetchMock.patchOnce(`/api/teams/123/accesses/789/`, {
status: 500,
body: {
detail: 'The server is totally broken',
},
});
render(
<ModalRole
access={access}
currentRole={Role.OWNER}
onClose={jest.fn()}
teamId="123"
/>,
{ wrapper: AppWrapper },
);
await userEvent.click(
screen.getByRole('radio', {
name: 'Member',
}),
);
await userEvent.click(
screen.getByRole('button', {
name: 'Validate',
}),
);
expect(
await screen.findByText('The server is totally broken'),
).toBeInTheDocument();
});
it('checks the render when last owner', () => {
useAuthStore.setState({
userData: access.user,
});
const access2: Access = {
...access,
role: Role.OWNER,
abilities: {
set_role_to: [],
} as any,
};
render(
<ModalRole
access={access2}
currentRole={Role.OWNER}
onClose={jest.fn()}
teamId="123"
/>,
{ wrapper: AppWrapper },
);
expect(
screen.getByText('You are the last owner, you cannot change your role.'),
).toBeInTheDocument();
expect(
screen.getByRole('radio', {
name: 'Admin',
}),
).toBeDisabled();
expect(
screen.getByRole('radio', {
name: 'Owner',
}),
).toBeDisabled();
expect(
screen.getByRole('radio', {
name: 'Member',
}),
).toBeDisabled();
expect(
screen.getByRole('button', {
name: 'Validate',
}),
).toBeDisabled();
});
it('checks the render when it is another owner', () => {
useAuthStore.setState({
userData: {
id: '12',
name: 'username2',
email: 'username2@test.com',
},
});
const access2: Access = {
...access,
role: Role.OWNER,
};
render(
<ModalRole
access={access2}
currentRole={Role.OWNER}
onClose={jest.fn()}
teamId="123"
/>,
{ wrapper: AppWrapper },
);
expect(
screen.getByText('You cannot update the role of other owner.'),
).toBeInTheDocument();
expect(
screen.getByRole('radio', {
name: 'Admin',
}),
).toBeDisabled();
expect(
screen.getByRole('radio', {
name: 'Owner',
}),
).toBeDisabled();
expect(
screen.getByRole('radio', {
name: 'Member',
}),
).toBeDisabled();
expect(
screen.getByRole('button', {
name: 'Validate',
}),
).toBeDisabled();
});
it('checks the render when current user is admin', () => {
render(
<ModalRole
access={access}
currentRole={Role.ADMIN}
onClose={jest.fn()}
teamId="123"
/>,
{ wrapper: AppWrapper },
);
expect(
screen.getByRole('radio', {
name: 'Member',
}),
).toBeEnabled();
expect(
screen.getByRole('radio', {
name: 'Admin',
}),
).toBeEnabled();
expect(
screen.getByRole('radio', {
name: 'Owner',
}),
).toBeDisabled();
});
});

View File

@@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next';
import { DropButton, Text } from '@/components';
import { Access, Role } from '@/features/teams/api';
import { ModalRole } from './ModalRole';
interface MemberActionProps {
access: Access;
currentRole: Role;
@@ -17,6 +19,7 @@ export const MemberAction = ({
teamId,
}: MemberActionProps) => {
const { t } = useTranslation();
const [isModalRoleOpen, setIsModalRoleOpen] = useState(false);
const [isDropOpen, setIsDropOpen] = useState(false);
if (
@@ -46,6 +49,7 @@ export const MemberAction = ({
>
<Button
onClick={() => {
setIsModalRoleOpen(true);
setIsDropOpen(false);
}}
color="primary-text"
@@ -54,6 +58,14 @@ export const MemberAction = ({
<Text $theme="primary">{t('Update the role')}</Text>
</Button>
</DropButton>
{isModalRoleOpen && (
<ModalRole
access={access}
currentRole={currentRole}
onClose={() => setIsModalRoleOpen(false)}
teamId={teamId}
/>
)}
</>
);
};

View File

@@ -0,0 +1,137 @@
import {
Button,
Modal,
ModalSize,
Radio,
RadioGroup,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
import { t } from 'i18next';
import { useState } from 'react';
import { Box, Text, TextErrors } from '@/components';
import { useAuthStore } from '@/features/auth';
import { Access, Role, useUpdateTeamAccess } from '@/features/teams/api/';
interface ModalRoleProps {
access: Access;
currentRole: Role;
onClose: () => void;
teamId: string;
}
export const ModalRole = ({
access,
currentRole,
onClose,
teamId,
}: ModalRoleProps) => {
const [localRole, setLocalRole] = useState(access.role);
const { userData } = useAuthStore();
const { toast } = useToastProvider();
const {
mutate: updateTeamAccess,
error: errorUpdate,
isError: isErrorUpdate,
} = useUpdateTeamAccess({
onSuccess: () => {
toast(t('The role has been updated'), VariantType.SUCCESS, {
duration: 4000,
});
onClose();
},
});
const rolesAllowed = access.abilities.set_role_to;
const isLastOwner =
!rolesAllowed.length &&
access.role === Role.OWNER &&
userData?.id === access.user.id;
const isOtherOwner =
access.role === Role.OWNER &&
userData?.id &&
userData.id !== access.user.id;
const isNotAllowed = isOtherOwner || isLastOwner;
return (
<Modal
isOpen
leftActions={
<Button color="secondary" fullWidth onClick={() => onClose()}>
{t('Cancel')}
</Button>
}
onClose={() => onClose()}
rightActions={
<Button
color="primary"
fullWidth
onClick={() => {
updateTeamAccess({
role: localRole,
teamId,
accessId: access.id,
});
}}
disabled={isNotAllowed}
>
{t('Validate')}
</Button>
}
size={ModalSize.MEDIUM}
title={t('Update the role')}
>
<Box aria-label={t('Radio buttons to update the roles')}>
{isErrorUpdate && (
<TextErrors className="mb-s" causes={errorUpdate.cause} />
)}
{(isLastOwner || isOtherOwner) && (
<Text
$theme="warning"
$direction="row"
$align="center"
$gap="0.5rem"
className="mb-t"
$justify="center"
>
<span className="material-icons">warning</span>
{isLastOwner &&
t('You are the last owner, you cannot change your role.')}
{isOtherOwner && t('You cannot update the role of other owner.')}
</Text>
)}
<RadioGroup>
<Radio
label={t('Admin')}
value={Role.ADMIN}
name="role"
onChange={(evt) => setLocalRole(evt.target.value as Role)}
defaultChecked={access.role === Role.ADMIN}
disabled={isNotAllowed}
/>
<Radio
label={t('Member')}
value={Role.MEMBER}
name="role"
onChange={(evt) => setLocalRole(evt.target.value as Role)}
defaultChecked={access.role === Role.MEMBER}
disabled={isNotAllowed}
/>
<Radio
label={t('Owner')}
value={Role.OWNER}
name="role"
onChange={(evt) => setLocalRole(evt.target.value as Role)}
defaultChecked={access.role === Role.OWNER}
disabled={isNotAllowed || currentRole !== Role.OWNER}
/>
</RadioGroup>
</Box>
</Modal>
);
};

View File

@@ -29,6 +29,12 @@
"Emails": "Emails",
"Roles": "Rôles",
"Member options": "Options des Membres",
"Radio buttons to update the roles": "Boutons radio pour mettre à jour les rôles",
"The role has been updated": "Le rôle a bien été mis à jour",
"Update the role": "Mettre à jour ce rôle",
"Validate": "Valider",
"You are the last owner, you cannot change your role.": "Vous êtes le dernier propriétaire, vous ne pouvez pas changer votre rôle.",
"You cannot update the role of other owner.": "Vous ne pouvez pas mettre à jour les rôles d'autre propriétaire.",
"Sort the teams": "Trier les groupes",
"Sort teams icon": "Icône trier les groupes",
"Add a team": "Ajouter un groupe",

View File

@@ -41,11 +41,11 @@ test.describe('Team', () => {
).toBeVisible();
});
test('checks the admin members is displayed correctly', async ({
test('checks the owner member is displayed correctly', async ({
page,
browserName,
}) => {
await createTeam(page, 'team-admin', browserName, 1);
await createTeam(page, 'team-owner', browserName, 1);
const table = page.getByLabel('List members card').getByRole('table');
@@ -62,4 +62,37 @@ test.describe('Team', () => {
await expect(cells.nth(2)).toHaveText(`user@${browserName}.e2e`);
await expect(cells.nth(3)).toHaveText('owner');
});
test('try to update the owner role but cannot because it is the last owner', async ({
page,
browserName,
}) => {
await createTeam(page, 'team-owner-role', browserName, 1);
const table = page.getByLabel('List members card').getByRole('table');
const cells = table.getByRole('row').nth(1).getByRole('cell');
await expect(cells.nth(1)).toHaveText(
new RegExp(`E2E ${browserName}`, 'i'),
);
await cells.nth(4).getByLabel('Member options').click();
await page.getByText('Update the role').click();
await expect(
page.getByText('You are the last owner, you cannot change your role.'),
).toBeVisible();
const radioGroup = page.getByLabel('Radio buttons to update the roles');
const radios = await radioGroup.getByRole('radio').all();
for (const radio of radios) {
await expect(radio).toBeDisabled();
}
await expect(
page.getByRole('button', {
name: 'Validate',
}),
).toBeDisabled();
});
});