diff --git a/src/frontend/apps/impress/src/features/members/__tests__/MemberAction.test.tsx b/src/frontend/apps/impress/src/features/members/__tests__/MemberAction.test.tsx deleted file mode 100644 index 780ce6eb..00000000 --- a/src/frontend/apps/impress/src/features/members/__tests__/MemberAction.test.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import '@testing-library/jest-dom'; -import { render, screen } from '@testing-library/react'; -import fetchMock from 'fetch-mock'; - -import { Role } from '@/features/teams'; -import { AppWrapper } from '@/tests/utils'; - -import { MemberAction } from '../components/MemberAction'; -import { Access } from '../types'; - -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('MemberAction', () => { - afterEach(() => { - fetchMock.restore(); - }); - - it('checks the render when owner', async () => { - render( - , - { - wrapper: AppWrapper, - }, - ); - - expect( - await screen.findByLabelText('Open the member options modal'), - ).toBeInTheDocument(); - }); - - it('checks the render when member', () => { - render( - , - { - wrapper: AppWrapper, - }, - ); - - expect( - screen.queryByLabelText('Open the member options modal'), - ).not.toBeInTheDocument(); - }); - - it('checks the render when admin', async () => { - render( - , - { - wrapper: AppWrapper, - }, - ); - - expect( - await screen.findByLabelText('Open the member options modal'), - ).toBeInTheDocument(); - }); - - it('checks the render when admin to owner', () => { - render( - , - { - wrapper: AppWrapper, - }, - ); - - expect( - screen.queryByLabelText('Open the member options modal'), - ).not.toBeInTheDocument(); - }); -}); diff --git a/src/frontend/apps/impress/src/features/members/__tests__/MemberGrid.test.tsx b/src/frontend/apps/impress/src/features/members/__tests__/MemberGrid.test.tsx deleted file mode 100644 index 9c8b1745..00000000 --- a/src/frontend/apps/impress/src/features/members/__tests__/MemberGrid.test.tsx +++ /dev/null @@ -1,348 +0,0 @@ -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 { Role, Team } from '@/features/teams'; -import { AppWrapper } from '@/tests/utils'; - -import { MemberGrid } from '../components/MemberGrid'; -import { Access } from '../types'; - -const team = { - id: '123456', - name: 'teamName', -} as Team; - -describe('MemberGrid', () => { - afterEach(() => { - fetchMock.restore(); - }); - - it('renders with no member to display', async () => { - fetchMock.mock(`/api/teams/123456/accesses/?page=1`, { - count: 0, - results: [], - }); - - render(, { - wrapper: AppWrapper, - }); - - expect(screen.getByRole('status')).toBeInTheDocument(); - - expect(await screen.findByRole('img')).toHaveAttribute( - 'alt', - 'Illustration of an empty table', - ); - - expect(screen.getByText('This table is empty')).toBeInTheDocument(); - expect( - screen.getByLabelText('Add members to the team'), - ).toBeInTheDocument(); - }); - - it('checks the render with members', async () => { - const accesses: Access[] = [ - { - id: '1', - role: Role.OWNER, - user: { - id: '11', - name: 'username1', - email: 'user1@test.com', - }, - abilities: {} as any, - }, - { - id: '2', - role: Role.MEMBER, - user: { - id: '22', - name: 'username2', - email: 'user2@test.com', - }, - abilities: {} as any, - }, - { - id: '32', - role: Role.ADMIN, - user: { - id: '33', - name: 'username3', - email: 'user3@test.com', - }, - abilities: {} as any, - }, - ]; - - fetchMock.mock(`/api/teams/123456/accesses/?page=1`, { - count: 3, - results: accesses, - }); - - render(, { - wrapper: AppWrapper, - }); - - expect(screen.getByRole('status')).toBeInTheDocument(); - - expect(await screen.findByText('username1')).toBeInTheDocument(); - expect(screen.getByText('username2')).toBeInTheDocument(); - expect(screen.getByText('username3')).toBeInTheDocument(); - expect(screen.getByText('user1@test.com')).toBeInTheDocument(); - expect(screen.getByText('user2@test.com')).toBeInTheDocument(); - expect(screen.getByText('user3@test.com')).toBeInTheDocument(); - expect(screen.getByText('Owner')).toBeInTheDocument(); - expect(screen.getByText('Admin')).toBeInTheDocument(); - expect(screen.getByText('Member')).toBeInTheDocument(); - }); - - it('checks the pagination', async () => { - fetchMock.get(`begin:/api/teams/123456/accesses/?page=`, { - count: 40, - results: Array.from({ length: 20 }, (_, i) => ({ - id: i, - role: Role.OWNER, - user: { - id: i, - name: 'username' + i, - email: `user${i}@test.com`, - }, - abilities: {} as any, - })), - }); - - render(, { - wrapper: AppWrapper, - }); - - expect(screen.getByRole('status')).toBeInTheDocument(); - - expect(fetchMock.lastUrl()).toBe('/api/teams/123456/accesses/?page=1'); - - expect( - await screen.findByLabelText('You are currently on page 1'), - ).toBeInTheDocument(); - - await userEvent.click(screen.getByLabelText('Go to page 2')); - - expect( - await screen.findByLabelText('You are currently on page 2'), - ).toBeInTheDocument(); - - expect(fetchMock.lastUrl()).toBe('/api/teams/123456/accesses/?page=2'); - }); - - [ - { - role: Role.OWNER, - expected: true, - }, - { - role: Role.MEMBER, - expected: false, - }, - { - role: Role.ADMIN, - expected: true, - }, - ].forEach(({ role, expected }) => { - it(`checks action button when ${role}`, async () => { - fetchMock.get(`begin:/api/teams/123456/accesses/?page=`, { - count: 1, - results: [ - { - id: 1, - role: Role.ADMIN, - user: { - id: 1, - name: 'username1', - email: `user1@test.com`, - }, - abilities: {} as any, - }, - ], - }); - - render(, { - wrapper: AppWrapper, - }); - - expect(screen.getByRole('status')).toBeInTheDocument(); - - /* eslint-disable jest/no-conditional-expect */ - if (expected) { - expect( - await screen.findAllByRole('button', { - name: 'Open the member options modal', - }), - ).toBeDefined(); - } else { - expect( - screen.queryByRole('button', { - name: 'Open the member options modal', - }), - ).not.toBeInTheDocument(); - } - /* eslint-enable jest/no-conditional-expect */ - }); - }); - - it('controls the render when api error', async () => { - fetchMock.mock(`/api/teams/123456/accesses/?page=1`, { - status: 500, - body: { - cause: 'All broken :(', - }, - }); - - render(, { - wrapper: AppWrapper, - }); - - expect(screen.getByRole('status')).toBeInTheDocument(); - - expect(await screen.findByText('All broken :(')).toBeInTheDocument(); - }); - - it('cannot add members when current role is member', () => { - fetchMock.get(`/api/teams/123456/accesses/?page=1`, 200); - - render(, { - wrapper: AppWrapper, - }); - - expect( - screen.queryByLabelText('Add members to the team'), - ).not.toBeInTheDocument(); - }); - - it.each([ - ['name', 'Names'], - ['email', 'Emails'], - ['role', 'Roles'], - ])('checks the sorting', async (ordering, header_name) => { - const mockedData = [ - { - id: '123', - role: Role.ADMIN, - user: { - id: '123', - name: 'albert', - email: 'albert@test.com', - }, - abilities: {} as any, - }, - { - id: '789', - role: Role.OWNER, - user: { - id: '456', - name: 'philipp', - email: 'philipp@test.com', - }, - abilities: {} as any, - }, - { - id: '456', - role: Role.MEMBER, - user: { - id: '789', - name: 'fany', - email: 'fany@test.com', - }, - abilities: {} as any, - }, - ]; - - const sortedMockedData = [...mockedData].sort((a, b) => - a.id > b.id ? 1 : -1, - ); - const reversedMockedData = [...sortedMockedData].reverse(); - - fetchMock.get(`/api/teams/123456/accesses/?page=1`, { - count: 3, - results: mockedData, - }); - - fetchMock.get(`/api/teams/123456/accesses/?page=1&ordering=${ordering}`, { - count: 3, - results: sortedMockedData, - }); - - fetchMock.get(`/api/teams/123456/accesses/?page=1&ordering=-${ordering}`, { - count: 3, - results: reversedMockedData, - }); - - render(, { - wrapper: AppWrapper, - }); - - expect(screen.getByRole('status')).toBeInTheDocument(); - - expect(fetchMock.lastUrl()).toBe(`/api/teams/123456/accesses/?page=1`); - - await waitFor(() => { - expect(screen.queryByRole('status')).not.toBeInTheDocument(); - }); - - let rows = screen.getAllByRole('row'); - expect(rows[1]).toHaveTextContent('albert'); - expect(rows[2]).toHaveTextContent('philipp'); - expect(rows[3]).toHaveTextContent('fany'); - - expect(screen.queryByLabelText('arrow_drop_down')).not.toBeInTheDocument(); - expect(screen.queryByLabelText('arrow_drop_up')).not.toBeInTheDocument(); - - await userEvent.click(screen.getByText(header_name)); - - expect(fetchMock.lastUrl()).toBe( - `/api/teams/123456/accesses/?page=1&ordering=${ordering}`, - ); - - await waitFor(() => { - expect(screen.queryByRole('status')).not.toBeInTheDocument(); - }); - - rows = screen.getAllByRole('row'); - expect(rows[1]).toHaveTextContent('albert'); - expect(rows[2]).toHaveTextContent('fany'); - expect(rows[3]).toHaveTextContent('philipp'); - - expect(await screen.findByText('arrow_drop_up')).toBeInTheDocument(); - - await userEvent.click(screen.getByText(header_name)); - - expect(fetchMock.lastUrl()).toBe( - `/api/teams/123456/accesses/?page=1&ordering=-${ordering}`, - ); - await waitFor(() => { - expect(screen.queryByRole('status')).not.toBeInTheDocument(); - }); - - rows = screen.getAllByRole('row'); - expect(rows[1]).toHaveTextContent('philipp'); - expect(rows[2]).toHaveTextContent('fany'); - expect(rows[3]).toHaveTextContent('albert'); - - expect(await screen.findByText('arrow_drop_down')).toBeInTheDocument(); - - await userEvent.click(screen.getByText(header_name)); - - expect(fetchMock.lastUrl()).toBe('/api/teams/123456/accesses/?page=1'); - - await waitFor(() => { - expect(screen.queryByRole('status')).not.toBeInTheDocument(); - }); - - rows = screen.getAllByRole('row'); - expect(rows[1]).toHaveTextContent('albert'); - expect(rows[2]).toHaveTextContent('philipp'); - expect(rows[3]).toHaveTextContent('fany'); - - expect(screen.queryByLabelText('arrow_drop_down')).not.toBeInTheDocument(); - expect(screen.queryByLabelText('arrow_drop_up')).not.toBeInTheDocument(); - }); -}); diff --git a/src/frontend/apps/impress/src/features/members/__tests__/ModalRole.test.tsx b/src/frontend/apps/impress/src/features/members/__tests__/ModalRole.test.tsx deleted file mode 100644 index 0716b663..00000000 --- a/src/frontend/apps/impress/src/features/members/__tests__/ModalRole.test.tsx +++ /dev/null @@ -1,287 +0,0 @@ -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 '@/core/auth'; -import { Role } from '@/features/teams'; -import { AppWrapper } from '@/tests/utils'; - -import { ModalRole } from '../components/ModalRole'; -import { Access } from '../types'; - -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/impress/src/features/members/api/index.ts b/src/frontend/apps/impress/src/features/members/api/index.ts deleted file mode 100644 index 1a8460f8..00000000 --- a/src/frontend/apps/impress/src/features/members/api/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './useDeleteTeamAccess'; -export * from './useTeamsAccesses'; -export * from './useUpdateTeamAccess'; diff --git a/src/frontend/apps/impress/src/features/members/api/useDeleteTeamAccess.ts b/src/frontend/apps/impress/src/features/members/api/useDeleteTeamAccess.ts deleted file mode 100644 index 0a81cfbe..00000000 --- a/src/frontend/apps/impress/src/features/members/api/useDeleteTeamAccess.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - UseMutationOptions, - useMutation, - useQueryClient, -} from '@tanstack/react-query'; - -import { APIError, errorCauses, fetchAPI } from '@/api'; -import { KEY_LIST_TEAM, KEY_TEAM } from '@/features/teams/'; - -import { KEY_LIST_TEAM_ACCESSES } from './useTeamsAccesses'; - -interface DeleteTeamAccessProps { - teamId: string; - accessId: string; -} - -export const deleteTeamAccess = async ({ - teamId, - accessId, -}: DeleteTeamAccessProps): Promise => { - const response = await fetchAPI(`teams/${teamId}/accesses/${accessId}/`, { - method: 'DELETE', - }); - - if (!response.ok) { - throw new APIError( - 'Failed to delete the member', - await errorCauses(response), - ); - } -}; - -type UseDeleteTeamAccessOptions = UseMutationOptions< - void, - APIError, - DeleteTeamAccessProps ->; - -export const useDeleteTeamAccess = (options?: UseDeleteTeamAccessOptions) => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: deleteTeamAccess, - ...options, - onSuccess: (data, variables, context) => { - void queryClient.invalidateQueries({ - queryKey: [KEY_LIST_TEAM_ACCESSES], - }); - void queryClient.invalidateQueries({ - queryKey: [KEY_TEAM], - }); - void queryClient.invalidateQueries({ - queryKey: [KEY_LIST_TEAM], - }); - if (options?.onSuccess) { - options.onSuccess(data, variables, context); - } - }, - onError: (error, variables, context) => { - if (options?.onError) { - options.onError(error, variables, context); - } - }, - }); -}; diff --git a/src/frontend/apps/impress/src/features/members/api/useTeamsAccesses.tsx b/src/frontend/apps/impress/src/features/members/api/useTeamsAccesses.tsx deleted file mode 100644 index ee4f4ea2..00000000 --- a/src/frontend/apps/impress/src/features/members/api/useTeamsAccesses.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { UseQueryOptions, useQuery } from '@tanstack/react-query'; - -import { APIError, APIList, errorCauses, fetchAPI } from '@/api'; - -import { Access } from '../types'; - -export type TeamAccessesAPIParams = { - page: number; - teamId: string; - ordering?: string; -}; - -type AccessesResponse = APIList; - -export const getTeamAccesses = async ({ - page, - teamId, - ordering, -}: TeamAccessesAPIParams): Promise => { - let url = `teams/${teamId}/accesses/?page=${page}`; - - if (ordering) { - url += '&ordering=' + ordering; - } - - const response = await fetchAPI(url); - - if (!response.ok) { - throw new APIError( - 'Failed to get the team accesses', - await errorCauses(response), - ); - } - - return response.json() as Promise; -}; - -export const KEY_LIST_TEAM_ACCESSES = 'teams-accesses'; - -export function useTeamAccesses( - params: TeamAccessesAPIParams, - queryConfig?: UseQueryOptions, -) { - return useQuery({ - queryKey: [KEY_LIST_TEAM_ACCESSES, params], - queryFn: () => getTeamAccesses(params), - ...queryConfig, - }); -} diff --git a/src/frontend/apps/impress/src/features/members/api/useUpdateTeamAccess.ts b/src/frontend/apps/impress/src/features/members/api/useUpdateTeamAccess.ts deleted file mode 100644 index 1c501fdb..00000000 --- a/src/frontend/apps/impress/src/features/members/api/useUpdateTeamAccess.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - UseMutationOptions, - useMutation, - useQueryClient, -} from '@tanstack/react-query'; - -import { APIError, errorCauses, fetchAPI } from '@/api'; -import { KEY_TEAM, Role } from '@/features/teams/'; - -import { Access } from '../types'; - -import { KEY_LIST_TEAM_ACCESSES } from './useTeamsAccesses'; - -interface UpdateTeamAccessProps { - teamId: string; - accessId: string; - role: Role; -} - -export const updateTeamAccess = async ({ - teamId, - accessId, - role, -}: UpdateTeamAccessProps): Promise => { - const response = await fetchAPI(`teams/${teamId}/accesses/${accessId}/`, { - method: 'PATCH', - body: JSON.stringify({ - role, - }), - }); - - if (!response.ok) { - throw new APIError('Failed to update role', await errorCauses(response)); - } - - return response.json() as Promise; -}; - -type UseUpdateTeamAccess = Partial; - -type UseUpdateTeamAccessOptions = UseMutationOptions< - Access, - APIError, - UseUpdateTeamAccess ->; - -export const useUpdateTeamAccess = (options?: UseUpdateTeamAccessOptions) => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: updateTeamAccess, - ...options, - onSuccess: (data, variables, context) => { - void queryClient.invalidateQueries({ - queryKey: [KEY_LIST_TEAM_ACCESSES], - }); - void queryClient.invalidateQueries({ - queryKey: [KEY_TEAM], - }); - if (options?.onSuccess) { - options.onSuccess(data, variables, context); - } - }, - onError: (error, variables, context) => { - if (options?.onError) { - options.onError(error, variables, context); - } - }, - }); -}; diff --git a/src/frontend/apps/impress/src/features/members/assets/icon-remove-member.svg b/src/frontend/apps/impress/src/features/members/assets/icon-remove-member.svg deleted file mode 100644 index 4316c35c..00000000 --- a/src/frontend/apps/impress/src/features/members/assets/icon-remove-member.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/src/frontend/apps/impress/src/features/members/components/ChooseRole.tsx b/src/frontend/apps/impress/src/features/members/components/ChooseRole.tsx deleted file mode 100644 index fdafeb57..00000000 --- a/src/frontend/apps/impress/src/features/members/components/ChooseRole.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Radio, RadioGroup } from '@openfun/cunningham-react'; -import { useTranslation } from 'react-i18next'; - -import { Role } from '@/features/teams'; - -interface ChooseRoleProps { - currentRole: Role; - disabled: boolean; - defaultRole: Role; - setRole: (role: Role) => void; -} - -export const ChooseRole = ({ - defaultRole, - disabled, - currentRole, - setRole, -}: ChooseRoleProps) => { - const { t } = useTranslation(); - - return ( - - setRole(evt.target.value as Role)} - defaultChecked={defaultRole === Role.ADMIN} - disabled={disabled} - /> - setRole(evt.target.value as Role)} - defaultChecked={defaultRole === Role.MEMBER} - disabled={disabled} - /> - setRole(evt.target.value as Role)} - defaultChecked={defaultRole === Role.OWNER} - disabled={disabled || currentRole !== Role.OWNER} - /> - - ); -}; diff --git a/src/frontend/apps/impress/src/features/members/components/MemberAction.tsx b/src/frontend/apps/impress/src/features/members/components/MemberAction.tsx deleted file mode 100644 index 6a06da5b..00000000 --- a/src/frontend/apps/impress/src/features/members/components/MemberAction.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { Button } from '@openfun/cunningham-react'; -import React, { useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { Box, DropButton, IconOptions, Text } from '@/components'; -import { Role, Team } from '@/features/teams'; - -import { Access } from '../types'; - -import { ModalDelete } from './ModalDelete'; -import { ModalRole } from './ModalRole'; - -interface MemberActionProps { - access: Access; - currentRole: Role; - team: Team; -} - -export const MemberAction = ({ - access, - currentRole, - team, -}: MemberActionProps) => { - const { t } = useTranslation(); - const [isModalRoleOpen, setIsModalRoleOpen] = useState(false); - const [isModalDeleteOpen, setIsModalDeleteOpen] = useState(false); - const [isDropOpen, setIsDropOpen] = useState(false); - - if ( - currentRole === Role.MEMBER || - (access.role === Role.OWNER && currentRole === Role.ADMIN) - ) { - return null; - } - - return ( - <> - - } - onOpenChange={(isOpen) => setIsDropOpen(isOpen)} - isOpen={isDropOpen} - > - - - - - - {isModalRoleOpen && ( - setIsModalRoleOpen(false)} - teamId={team.id} - /> - )} - {isModalDeleteOpen && ( - setIsModalDeleteOpen(false)} - team={team} - /> - )} - - ); -}; diff --git a/src/frontend/apps/impress/src/features/members/components/MemberGrid.tsx b/src/frontend/apps/impress/src/features/members/components/MemberGrid.tsx deleted file mode 100644 index 2baea306..00000000 --- a/src/frontend/apps/impress/src/features/members/components/MemberGrid.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { - Button, - DataGrid, - SortModel, - usePagination, -} from '@openfun/cunningham-react'; -import React, { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import IconUser from '@/assets/icons/icon-user.svg'; -import { Box, Card, TextErrors } from '@/components'; -import { useCunninghamTheme } from '@/cunningham'; -import { ModalAddMembers } from '@/features/addMembers'; -import { Role, Team } from '@/features/teams'; - -import { useTeamAccesses } from '../api/'; -import { PAGE_SIZE } from '../conf'; - -import { MemberAction } from './MemberAction'; - -interface MemberGridProps { - team: Team; - currentRole: Role; -} - -// FIXME : ask Cunningham to export this type -type SortModelItem = { - field: string; - sort: 'asc' | 'desc' | null; -}; - -const defaultOrderingMapping: Record = { - 'user.name': 'name', - 'user.email': 'email', - localizedRole: 'role', -}; - -/** - * Formats the sorting model based on a given mapping. - * @param {SortModelItem} sortModel The sorting model item containing field and sort direction. - * @param {Record} mapping The mapping object to map field names. - * @returns {string} The formatted sorting string. - */ -function formatSortModel( - sortModel: SortModelItem, - mapping = defaultOrderingMapping, -) { - const { field, sort } = sortModel; - const orderingField = mapping[field] || field; - return sort === 'desc' ? `-${orderingField}` : orderingField; -} - -export const MemberGrid = ({ team, currentRole }: MemberGridProps) => { - const [isModalMemberOpen, setIsModalMemberOpen] = useState(false); - const { t } = useTranslation(); - const { colorsTokens } = useCunninghamTheme(); - const pagination = usePagination({ - pageSize: PAGE_SIZE, - }); - const [sortModel, setSortModel] = useState([]); - const { page, pageSize, setPagesCount } = pagination; - - const ordering = sortModel.length ? formatSortModel(sortModel[0]) : undefined; - - const { data, isLoading, error } = useTeamAccesses({ - teamId: team.id, - page, - ordering, - }); - - const localizedRoles = { - [Role.ADMIN]: t('Admin'), - [Role.MEMBER]: t('Member'), - [Role.OWNER]: t('Owner'), - }; - - /* - * Bug occurs from the Cunningham Datagrid component, when applying sorting - * on null values. Sanitize empty values to ensure consistent sorting functionality. - */ - const accesses = - data?.results?.map((access) => ({ - ...access, - localizedRole: localizedRoles[access.role], - user: { - ...access.user, - name: access.user.name || '', - email: access.user.email || '', - }, - })) || []; - - useEffect(() => { - setPagesCount(data?.count ? Math.ceil(data.count / pageSize) : 0); - }, [data?.count, pageSize, setPagesCount]); - - return ( - <> - {currentRole !== Role.MEMBER && ( - - - - )} - - {error && } - - - - - ); - }, - }, - { - headerName: t('Names'), - field: 'user.name', - }, - { - field: 'user.email', - headerName: t('Emails'), - }, - { - field: 'localizedRole', - headerName: t('Roles'), - }, - { - id: 'column-actions', - renderCell: ({ row }) => { - return ( - - ); - }, - }, - ]} - rows={accesses} - isLoading={isLoading} - pagination={pagination} - onSortModelChange={setSortModel} - sortModel={sortModel} - /> - - {isModalMemberOpen && ( - setIsModalMemberOpen(false)} - team={team} - /> - )} - - ); -}; diff --git a/src/frontend/apps/impress/src/features/members/components/ModalDelete.tsx b/src/frontend/apps/impress/src/features/members/components/ModalDelete.tsx deleted file mode 100644 index bd8954a4..00000000 --- a/src/frontend/apps/impress/src/features/members/components/ModalDelete.tsx +++ /dev/null @@ -1,136 +0,0 @@ -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 { Role, Team } from '@/features/teams/'; - -import { useDeleteTeamAccess } from '../api/useDeleteTeamAccess'; -import IconRemoveMember from '../assets/icon-remove-member.svg'; -import { useWhoAmI } from '../hooks/useWhoAmI'; -import { Access } from '../types'; - -interface ModalDeleteProps { - access: Access; - currentRole: Role; - onClose: () => void; - team: Team; -} - -export const ModalDelete = ({ access, onClose, team }: ModalDeleteProps) => { - const { toast } = useToastProvider(); - const { colorsTokens } = useCunninghamTheme(); - const router = useRouter(); - - const { isMyself, isLastOwner, isOtherOwner } = useWhoAmI(access); - const isNotAllowed = isOtherOwner || isLastOwner; - - const { - mutate: removeTeamAccess, - error: errorUpdate, - isError: isErrorUpdate, - } = useDeleteTeamAccess({ - onSuccess: () => { - toast( - t('The member has been removed from the team'), - 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 ( - onClose()}> - {t('Cancel')} - - } - onClose={onClose} - rightActions={ - - } - size={ModalSize.MEDIUM} - title={ - - - - {t('Remove the member')} - - - } - > - - - {t( - 'Are you sure you want to remove this member from the {{team}} group?', - { team: team.name }, - )} - - - {isErrorUpdate && ( - - )} - - {(isLastOwner || isOtherOwner) && ( - - warning - {isLastOwner && - t( - 'You are the last owner, you cannot be removed from your team.', - )} - {isOtherOwner && t('You cannot remove other owner.')} - - )} - - - - {access.user.name} - - - - ); -}; diff --git a/src/frontend/apps/impress/src/features/members/components/ModalRole.tsx b/src/frontend/apps/impress/src/features/members/components/ModalRole.tsx deleted file mode 100644 index 27f48e4a..00000000 --- a/src/frontend/apps/impress/src/features/members/components/ModalRole.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { - Button, - Modal, - ModalSize, - VariantType, - useToastProvider, -} from '@openfun/cunningham-react'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { Box, Text, TextErrors } from '@/components'; -import { Role } from '@/features/teams'; - -import { useUpdateTeamAccess } from '../api/useUpdateTeamAccess'; -import { useWhoAmI } from '../hooks/useWhoAmI'; -import { Access } from '../types'; - -import { ChooseRole } from './ChooseRole'; - -interface ModalRoleProps { - access: Access; - currentRole: Role; - onClose: () => void; - teamId: string; -} - -export const ModalRole = ({ - access, - currentRole, - onClose, - teamId, -}: ModalRoleProps) => { - const { t } = useTranslation(); - const [localRole, setLocalRole] = useState(access.role); - 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 { isLastOwner, isOtherOwner } = useWhoAmI(access); - - const isNotAllowed = isOtherOwner || isLastOwner; - - return ( - onClose()}> - {t('Cancel')} - - } - onClose={() => onClose()} - closeOnClickOutside - hideCloseButton - 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.')} - - )} - - - - - ); -}; diff --git a/src/frontend/apps/impress/src/features/members/conf.ts b/src/frontend/apps/impress/src/features/members/conf.ts deleted file mode 100644 index bfab9067..00000000 --- a/src/frontend/apps/impress/src/features/members/conf.ts +++ /dev/null @@ -1 +0,0 @@ -export const PAGE_SIZE = 20; diff --git a/src/frontend/apps/impress/src/features/members/hooks/useWhoAmI.tsx b/src/frontend/apps/impress/src/features/members/hooks/useWhoAmI.tsx deleted file mode 100644 index 79eb5068..00000000 --- a/src/frontend/apps/impress/src/features/members/hooks/useWhoAmI.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useAuthStore } from '@/core/auth'; -import { Role } from '@/features/teams'; - -import { Access } from '../types'; - -export const useWhoAmI = (access: Access) => { - const { userData } = useAuthStore(); - - const isMyself = userData?.id === access.user.id; - const rolesAllowed = access.abilities.set_role_to; - - const isLastOwner = - !rolesAllowed.length && access.role === Role.OWNER && isMyself; - - const isOtherOwner = access.role === Role.OWNER && userData?.id && !isMyself; - - return { - isLastOwner, - isOtherOwner, - isMyself, - }; -}; diff --git a/src/frontend/apps/impress/src/features/members/index.ts b/src/frontend/apps/impress/src/features/members/index.ts deleted file mode 100644 index 4d6f0021..00000000 --- a/src/frontend/apps/impress/src/features/members/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './api/useTeamsAccesses'; -export * from './components/ChooseRole'; -export * from './components/MemberGrid'; -export * from './types'; diff --git a/src/frontend/apps/impress/src/features/members/types.tsx b/src/frontend/apps/impress/src/features/members/types.tsx deleted file mode 100644 index e8e858d2..00000000 --- a/src/frontend/apps/impress/src/features/members/types.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { User } from '@/core/auth'; -import { Role, Team } from '@/features/teams/'; - -export interface Access { - id: string; - role: Role; - user: User; - abilities: { - delete: boolean; - get: boolean; - patch: boolean; - put: boolean; - set_role_to: Role[]; - }; -} - -export interface Invitation { - id: string; - created_at: string; - email: string; - team: Team['id']; - role: Role; - issuer: User['id']; - is_expired: boolean; -}