diff --git a/src/frontend/apps/desk/src/cunningham/cunningham-style.css b/src/frontend/apps/desk/src/cunningham/cunningham-style.css
index 6793987..83a5e89 100644
--- a/src/frontend/apps/desk/src/cunningham/cunningham-style.css
+++ b/src/frontend/apps/desk/src/cunningham/cunningham-style.css
@@ -434,3 +434,10 @@ input:-webkit-autofill:focus {
--c--components--button--danger--background--color-disabled
);
}
+
+/**
+ * Modal
+*/
+.c__modal__backdrop {
+ z-index: 1000;
+}
diff --git a/src/frontend/apps/desk/src/features/teams/__tests__/ModalRole.test.tsx b/src/frontend/apps/desk/src/features/teams/__tests__/ModalRole.test.tsx
new file mode 100644
index 0000000..d9efaf7
--- /dev/null
+++ b/src/frontend/apps/desk/src/features/teams/__tests__/ModalRole.test.tsx
@@ -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(
+ ,
+ {
+ 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(
+ ,
+ { 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(
+ ,
+ { 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(
+ ,
+ { 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(
+ ,
+ { 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(
+ ,
+ { wrapper: AppWrapper },
+ );
+
+ expect(
+ screen.getByRole('radio', {
+ name: 'Member',
+ }),
+ ).toBeEnabled();
+
+ expect(
+ screen.getByRole('radio', {
+ name: 'Admin',
+ }),
+ ).toBeEnabled();
+
+ expect(
+ screen.getByRole('radio', {
+ name: 'Owner',
+ }),
+ ).toBeDisabled();
+ });
+});
diff --git a/src/frontend/apps/desk/src/features/teams/components/Member/MemberAction.tsx b/src/frontend/apps/desk/src/features/teams/components/Member/MemberAction.tsx
index cc4e775..d70c7be 100644
--- a/src/frontend/apps/desk/src/features/teams/components/Member/MemberAction.tsx
+++ b/src/frontend/apps/desk/src/features/teams/components/Member/MemberAction.tsx
@@ -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 = ({
>
+ {isModalRoleOpen && (
+ setIsModalRoleOpen(false)}
+ teamId={teamId}
+ />
+ )}
>
);
};
diff --git a/src/frontend/apps/desk/src/features/teams/components/Member/ModalRole.tsx b/src/frontend/apps/desk/src/features/teams/components/Member/ModalRole.tsx
new file mode 100644
index 0000000..117311c
--- /dev/null
+++ b/src/frontend/apps/desk/src/features/teams/components/Member/ModalRole.tsx
@@ -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 (
+ onClose()}>
+ {t('Cancel')}
+
+ }
+ onClose={() => onClose()}
+ rightActions={
+
+ }
+ size={ModalSize.MEDIUM}
+ title={t('Update the role')}
+ >
+
+ {isErrorUpdate && (
+
+ )}
+
+ {(isLastOwner || isOtherOwner) && (
+
+ warning
+ {isLastOwner &&
+ t('You are the last owner, you cannot change your role.')}
+ {isOtherOwner && t('You cannot update the role of other owner.')}
+
+ )}
+
+
+ setLocalRole(evt.target.value as Role)}
+ defaultChecked={access.role === Role.ADMIN}
+ disabled={isNotAllowed}
+ />
+ setLocalRole(evt.target.value as Role)}
+ defaultChecked={access.role === Role.MEMBER}
+ disabled={isNotAllowed}
+ />
+ setLocalRole(evt.target.value as Role)}
+ defaultChecked={access.role === Role.OWNER}
+ disabled={isNotAllowed || currentRole !== Role.OWNER}
+ />
+
+
+
+ );
+};
diff --git a/src/frontend/apps/desk/src/i18n/translations.json b/src/frontend/apps/desk/src/i18n/translations.json
index b88aad1..3517d67 100644
--- a/src/frontend/apps/desk/src/i18n/translations.json
+++ b/src/frontend/apps/desk/src/i18n/translations.json
@@ -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",
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 11c18cc..f86da6f 100644
--- a/src/frontend/apps/e2e/__tests__/app-desk/team.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-desk/team.spec.ts
@@ -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();
+ });
});