🔥(app-impress) remove Members feature
Members feature was a part of the People project, we don't need it in the Impress project.
This commit is contained in:
@@ -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(
|
||||
<MemberAction access={access} currentRole={Role.OWNER} teamId="123" />,
|
||||
{
|
||||
wrapper: AppWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByLabelText('Open the member options modal'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('checks the render when member', () => {
|
||||
render(
|
||||
<MemberAction access={access} currentRole={Role.MEMBER} teamId="123" />,
|
||||
{
|
||||
wrapper: AppWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByLabelText('Open the member options modal'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('checks the render when admin', async () => {
|
||||
render(
|
||||
<MemberAction access={access} currentRole={Role.ADMIN} teamId="123" />,
|
||||
{
|
||||
wrapper: AppWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByLabelText('Open the member options modal'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('checks the render when admin to owner', () => {
|
||||
render(
|
||||
<MemberAction
|
||||
access={{ ...access, role: Role.OWNER }}
|
||||
currentRole={Role.ADMIN}
|
||||
teamId="123"
|
||||
/>,
|
||||
{
|
||||
wrapper: AppWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByLabelText('Open the member options modal'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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(<MemberGrid team={team} currentRole={Role.ADMIN} />, {
|
||||
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(<MemberGrid team={team} currentRole={Role.ADMIN} />, {
|
||||
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(<MemberGrid team={team} currentRole={Role.ADMIN} />, {
|
||||
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(<MemberGrid team={team} currentRole={role} />, {
|
||||
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(<MemberGrid team={team} currentRole={Role.ADMIN} />, {
|
||||
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(<MemberGrid team={team} currentRole={Role.MEMBER} />, {
|
||||
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(<MemberGrid team={team} currentRole={Role.ADMIN} />, {
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
<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();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './useDeleteTeamAccess';
|
||||
export * from './useTeamsAccesses';
|
||||
export * from './useUpdateTeamAccess';
|
||||
@@ -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<void> => {
|
||||
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<void, APIError, DeleteTeamAccessProps>({
|
||||
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);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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<Access>;
|
||||
|
||||
export const getTeamAccesses = async ({
|
||||
page,
|
||||
teamId,
|
||||
ordering,
|
||||
}: TeamAccessesAPIParams): Promise<AccessesResponse> => {
|
||||
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<AccessesResponse>;
|
||||
};
|
||||
|
||||
export const KEY_LIST_TEAM_ACCESSES = 'teams-accesses';
|
||||
|
||||
export function useTeamAccesses(
|
||||
params: TeamAccessesAPIParams,
|
||||
queryConfig?: UseQueryOptions<AccessesResponse, APIError, AccessesResponse>,
|
||||
) {
|
||||
return useQuery<AccessesResponse, APIError, AccessesResponse>({
|
||||
queryKey: [KEY_LIST_TEAM_ACCESSES, params],
|
||||
queryFn: () => getTeamAccesses(params),
|
||||
...queryConfig,
|
||||
});
|
||||
}
|
||||
@@ -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<Access> => {
|
||||
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<Access>;
|
||||
};
|
||||
|
||||
type UseUpdateTeamAccess = Partial<Access>;
|
||||
|
||||
type UseUpdateTeamAccessOptions = UseMutationOptions<
|
||||
Access,
|
||||
APIError,
|
||||
UseUpdateTeamAccess
|
||||
>;
|
||||
|
||||
export const useUpdateTeamAccess = (options?: UseUpdateTeamAccessOptions) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<Access, APIError, UpdateTeamAccessProps>({
|
||||
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);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -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 (
|
||||
<RadioGroup>
|
||||
<Radio
|
||||
label={t('Admin')}
|
||||
value={Role.ADMIN}
|
||||
name="role"
|
||||
onChange={(evt) => setRole(evt.target.value as Role)}
|
||||
defaultChecked={defaultRole === Role.ADMIN}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Radio
|
||||
label={t('Member')}
|
||||
value={Role.MEMBER}
|
||||
name="role"
|
||||
onChange={(evt) => setRole(evt.target.value as Role)}
|
||||
defaultChecked={defaultRole === Role.MEMBER}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Radio
|
||||
label={t('Owner')}
|
||||
value={Role.OWNER}
|
||||
name="role"
|
||||
onChange={(evt) => setRole(evt.target.value as Role)}
|
||||
defaultChecked={defaultRole === Role.OWNER}
|
||||
disabled={disabled || currentRole !== Role.OWNER}
|
||||
/>
|
||||
</RadioGroup>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<>
|
||||
<DropButton
|
||||
button={
|
||||
<IconOptions
|
||||
isOpen={isDropOpen}
|
||||
aria-label={t('Open the member options modal')}
|
||||
/>
|
||||
}
|
||||
onOpenChange={(isOpen) => setIsDropOpen(isOpen)}
|
||||
isOpen={isDropOpen}
|
||||
>
|
||||
<Box>
|
||||
<Button
|
||||
aria-label={t('Open the modal to update the role of this member')}
|
||||
onClick={() => {
|
||||
setIsModalRoleOpen(true);
|
||||
setIsDropOpen(false);
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">edit</span>}
|
||||
>
|
||||
<Text $theme="primary">{t('Update the role')}</Text>
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('Open the modal to delete this member')}
|
||||
onClick={() => {
|
||||
setIsModalDeleteOpen(true);
|
||||
setIsDropOpen(false);
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">delete</span>}
|
||||
>
|
||||
<Text $theme="primary">{t('Delete')}</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</DropButton>
|
||||
{isModalRoleOpen && (
|
||||
<ModalRole
|
||||
access={access}
|
||||
currentRole={currentRole}
|
||||
onClose={() => setIsModalRoleOpen(false)}
|
||||
teamId={team.id}
|
||||
/>
|
||||
)}
|
||||
{isModalDeleteOpen && (
|
||||
<ModalDelete
|
||||
access={access}
|
||||
currentRole={currentRole}
|
||||
onClose={() => setIsModalDeleteOpen(false)}
|
||||
team={team}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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<string, string> = {
|
||||
'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<string, string>} 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<SortModel>([]);
|
||||
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 && (
|
||||
<Box className="m-b mb-s" $align="flex-end">
|
||||
<Button
|
||||
aria-label={t('Add members to the team')}
|
||||
style={{
|
||||
width: 'fit-content',
|
||||
minWidth: '8rem',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onClick={() => setIsModalMemberOpen(true)}
|
||||
>
|
||||
{t('Add')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Card
|
||||
className="m-b pb-s"
|
||||
$overflow="auto"
|
||||
$css={`
|
||||
margin-top:0;
|
||||
& .c__pagination__goto {
|
||||
display: none;
|
||||
}
|
||||
& table th:first-child,
|
||||
& table td:first-child {
|
||||
padding-right: 0;
|
||||
width: 3.5rem;
|
||||
}
|
||||
& table td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
`}
|
||||
aria-label={t('List members card')}
|
||||
>
|
||||
{error && <TextErrors causes={error.cause} />}
|
||||
|
||||
<DataGrid
|
||||
columns={[
|
||||
{
|
||||
id: 'icon-user',
|
||||
renderCell() {
|
||||
return (
|
||||
<Box $direction="row" $align="center">
|
||||
<IconUser
|
||||
aria-label={t('Member icon')}
|
||||
width={20}
|
||||
height={20}
|
||||
color={colorsTokens()['primary-600']}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Names'),
|
||||
field: 'user.name',
|
||||
},
|
||||
{
|
||||
field: 'user.email',
|
||||
headerName: t('Emails'),
|
||||
},
|
||||
{
|
||||
field: 'localizedRole',
|
||||
headerName: t('Roles'),
|
||||
},
|
||||
{
|
||||
id: 'column-actions',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<MemberAction
|
||||
team={team}
|
||||
access={row}
|
||||
currentRole={currentRole}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
rows={accesses}
|
||||
isLoading={isLoading}
|
||||
pagination={pagination}
|
||||
onSortModelChange={setSortModel}
|
||||
sortModel={sortModel}
|
||||
/>
|
||||
</Card>
|
||||
{isModalMemberOpen && (
|
||||
<ModalAddMembers
|
||||
currentRole={currentRole}
|
||||
onClose={() => setIsModalMemberOpen(false)}
|
||||
team={team}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<Modal
|
||||
isOpen
|
||||
closeOnClickOutside
|
||||
hideCloseButton
|
||||
leftActions={
|
||||
<Button color="secondary" fullWidth onClick={() => onClose()}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
}
|
||||
onClose={onClose}
|
||||
rightActions={
|
||||
<Button
|
||||
color="primary"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
removeTeamAccess({
|
||||
teamId: team.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" className="m-0">
|
||||
{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 {{team}} group?',
|
||||
{ team: team.name },
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{isErrorUpdate && (
|
||||
<TextErrors className="mb-s" causes={errorUpdate.cause} />
|
||||
)}
|
||||
|
||||
{(isLastOwner || isOtherOwner) && (
|
||||
<Text
|
||||
$theme="warning"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="0.5rem"
|
||||
className="m-t"
|
||||
$justify="center"
|
||||
>
|
||||
<span className="material-icons">warning</span>
|
||||
{isLastOwner &&
|
||||
t(
|
||||
'You are the last owner, you cannot be removed from your team.',
|
||||
)}
|
||||
{isOtherOwner && t('You cannot remove other owner.')}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text
|
||||
as="p"
|
||||
className="p-b"
|
||||
$direction="row"
|
||||
$gap="0.5rem"
|
||||
$background={colorsTokens()['primary-150']}
|
||||
$theme="primary"
|
||||
>
|
||||
<IconUser width={20} height={20} />
|
||||
<Text>{access.user.name}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<Modal
|
||||
isOpen
|
||||
leftActions={
|
||||
<Button color="secondary" fullWidth onClick={() => onClose()}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
}
|
||||
onClose={() => onClose()}
|
||||
closeOnClickOutside
|
||||
hideCloseButton
|
||||
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>
|
||||
)}
|
||||
|
||||
<ChooseRole
|
||||
defaultRole={access.role}
|
||||
currentRole={currentRole}
|
||||
disabled={isNotAllowed}
|
||||
setRole={setLocalRole}
|
||||
/>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export const PAGE_SIZE = 20;
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './api/useTeamsAccesses';
|
||||
export * from './components/ChooseRole';
|
||||
export * from './components/MemberGrid';
|
||||
export * from './types';
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user