✨(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:
@@ -434,3 +434,10 @@ input:-webkit-autofill:focus {
|
|||||||
--c--components--button--danger--background--color-disabled
|
--c--components--button--danger--background--color-disabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal
|
||||||
|
*/
|
||||||
|
.c__modal__backdrop {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { DropButton, Text } from '@/components';
|
import { DropButton, Text } from '@/components';
|
||||||
import { Access, Role } from '@/features/teams/api';
|
import { Access, Role } from '@/features/teams/api';
|
||||||
|
|
||||||
|
import { ModalRole } from './ModalRole';
|
||||||
|
|
||||||
interface MemberActionProps {
|
interface MemberActionProps {
|
||||||
access: Access;
|
access: Access;
|
||||||
currentRole: Role;
|
currentRole: Role;
|
||||||
@@ -17,6 +19,7 @@ export const MemberAction = ({
|
|||||||
teamId,
|
teamId,
|
||||||
}: MemberActionProps) => {
|
}: MemberActionProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [isModalRoleOpen, setIsModalRoleOpen] = useState(false);
|
||||||
const [isDropOpen, setIsDropOpen] = useState(false);
|
const [isDropOpen, setIsDropOpen] = useState(false);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -46,6 +49,7 @@ export const MemberAction = ({
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
setIsModalRoleOpen(true);
|
||||||
setIsDropOpen(false);
|
setIsDropOpen(false);
|
||||||
}}
|
}}
|
||||||
color="primary-text"
|
color="primary-text"
|
||||||
@@ -54,6 +58,14 @@ export const MemberAction = ({
|
|||||||
<Text $theme="primary">{t('Update the role')}</Text>
|
<Text $theme="primary">{t('Update the role')}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</DropButton>
|
</DropButton>
|
||||||
|
{isModalRoleOpen && (
|
||||||
|
<ModalRole
|
||||||
|
access={access}
|
||||||
|
currentRole={currentRole}
|
||||||
|
onClose={() => setIsModalRoleOpen(false)}
|
||||||
|
teamId={teamId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -29,6 +29,12 @@
|
|||||||
"Emails": "Emails",
|
"Emails": "Emails",
|
||||||
"Roles": "Rôles",
|
"Roles": "Rôles",
|
||||||
"Member options": "Options des Membres",
|
"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 the teams": "Trier les groupes",
|
||||||
"Sort teams icon": "Icône trier les groupes",
|
"Sort teams icon": "Icône trier les groupes",
|
||||||
"Add a team": "Ajouter un groupe",
|
"Add a team": "Ajouter un groupe",
|
||||||
|
|||||||
@@ -41,11 +41,11 @@ test.describe('Team', () => {
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('checks the admin members is displayed correctly', async ({
|
test('checks the owner member is displayed correctly', async ({
|
||||||
page,
|
page,
|
||||||
browserName,
|
browserName,
|
||||||
}) => {
|
}) => {
|
||||||
await createTeam(page, 'team-admin', browserName, 1);
|
await createTeam(page, 'team-owner', browserName, 1);
|
||||||
|
|
||||||
const table = page.getByLabel('List members card').getByRole('table');
|
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(2)).toHaveText(`user@${browserName}.e2e`);
|
||||||
await expect(cells.nth(3)).toHaveText('owner');
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user