✨(app-desk) integrate grid member action
Integrate the action button dropdown in the member grid. For the moment it will be used to update the role of a member. Manage use cases: - Does not display when member's role - Does not display when member is an admin that wants to update owner role.
This commit is contained in:
@@ -276,6 +276,13 @@ const config = {
|
||||
'color-hover': '#ffffff',
|
||||
'color-active': '#ffffff',
|
||||
},
|
||||
'primary-text': {
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||
'color-active': 'var(--c--theme--colors--primary-100)',
|
||||
},
|
||||
'color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
},
|
||||
secondary: {
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||
@@ -309,6 +316,7 @@ const config = {
|
||||
},
|
||||
'forms-checkbox': {
|
||||
'border-radius': '0',
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
},
|
||||
'forms-datepicker': {
|
||||
'border-radius': '0',
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"@tanstack/react-query-devtools": "5.24.8",
|
||||
"@testing-library/jest-dom": "6.4.2",
|
||||
"@testing-library/react": "14.2.1",
|
||||
"@testing-library/user-event": "14.5.2",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/node": "*",
|
||||
|
||||
@@ -329,6 +329,22 @@ input:-webkit-autofill:focus {
|
||||
border-color: var(--c--components--button--primary--border--color-active);
|
||||
}
|
||||
|
||||
.c__button--primary-text:active,
|
||||
.c__button--primary-text.c__button--active {
|
||||
border: none;
|
||||
background-color: var(
|
||||
--c--components--button--primary-text--background--color-active
|
||||
);
|
||||
}
|
||||
|
||||
.c__button--primary-text:hover,
|
||||
.c__button--primary-text:focus-visible {
|
||||
background-color: var(
|
||||
--c--components--button--primary-text--background--color-hover
|
||||
);
|
||||
color: var(--c--components--button--primary-text--color-hover);
|
||||
}
|
||||
|
||||
.c__button:disabled {
|
||||
background-color: var(--c--components--button--disabled--background--color);
|
||||
color: var(--c--components--button--disabled--color);
|
||||
|
||||
@@ -409,6 +409,15 @@
|
||||
--c--components--button--primary--color: #fff;
|
||||
--c--components--button--primary--color-hover: #fff;
|
||||
--c--components--button--primary--color-active: #fff;
|
||||
--c--components--button--primary-text--background--color-hover: var(
|
||||
--c--theme--colors--primary-100
|
||||
);
|
||||
--c--components--button--primary-text--background--color-active: var(
|
||||
--c--theme--colors--primary-100
|
||||
);
|
||||
--c--components--button--primary-text--color-hover: var(
|
||||
--c--theme--colors--primary-text
|
||||
);
|
||||
--c--components--button--secondary--background--color-hover: var(
|
||||
--c--theme--colors--primary-100
|
||||
);
|
||||
@@ -438,6 +447,7 @@
|
||||
--c--theme--colors--primary-300
|
||||
);
|
||||
--c--components--forms-checkbox--border-radius: 0;
|
||||
--c--components--forms-checkbox--color: var(--c--theme--colors--primary-text);
|
||||
--c--components--forms-datepicker--border-radius: 0;
|
||||
--c--components--forms-fileuploader--border-radius: 0;
|
||||
--c--components--forms-input--border-radius: 4px;
|
||||
|
||||
@@ -415,6 +415,13 @@ export const tokens = {
|
||||
'color-hover': '#ffffff',
|
||||
'color-active': '#ffffff',
|
||||
},
|
||||
'primary-text': {
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||
'color-active': 'var(--c--theme--colors--primary-100)',
|
||||
},
|
||||
'color-hover': 'var(--c--theme--colors--primary-text)',
|
||||
},
|
||||
secondary: {
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||
@@ -444,7 +451,10 @@ export const tokens = {
|
||||
'background-color-active': 'var(--c--theme--colors--primary-300)',
|
||||
},
|
||||
},
|
||||
'forms-checkbox': { 'border-radius': '0' },
|
||||
'forms-checkbox': {
|
||||
'border-radius': '0',
|
||||
color: 'var(--c--theme--colors--primary-text)',
|
||||
},
|
||||
'forms-datepicker': { 'border-radius': '0' },
|
||||
'forms-fileuploader': { 'border-radius': '0' },
|
||||
'forms-input': {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import { AppWrapper } from '@/tests/utils';
|
||||
|
||||
import { Access, Role } from '../api';
|
||||
import { MemberAction } from '../components/Member/MemberAction';
|
||||
|
||||
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('Member options')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('checks the render when member', () => {
|
||||
render(
|
||||
<MemberAction access={access} currentRole={Role.MEMBER} teamId="123" />,
|
||||
{
|
||||
wrapper: AppWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText('Member options')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('checks the render when admin', async () => {
|
||||
render(
|
||||
<MemberAction access={access} currentRole={Role.ADMIN} teamId="123" />,
|
||||
{
|
||||
wrapper: AppWrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(await screen.findByLabelText('Member options')).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('Member options')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import { AppWrapper } from '@/tests/utils';
|
||||
|
||||
import { Access, Role } from '../api';
|
||||
import { MemberGrid } from '../components/Member/MemberGrid';
|
||||
|
||||
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 teamId="123456" 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();
|
||||
});
|
||||
|
||||
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 teamId="123456" 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(Role.OWNER)).toBeInTheDocument();
|
||||
expect(screen.getByText(Role.ADMIN)).toBeInTheDocument();
|
||||
expect(screen.getByText(Role.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 teamId="123456" 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 teamId="123456" currentRole={role} />, {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
if (expected) {
|
||||
expect(
|
||||
await screen.findAllByRole('button', {
|
||||
name: 'Member options',
|
||||
}),
|
||||
).toBeDefined();
|
||||
} else {
|
||||
expect(
|
||||
screen.queryByRole('button', {
|
||||
name: 'Member options',
|
||||
}),
|
||||
).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 teamId="123456" currentRole={Role.ADMIN} />, {
|
||||
wrapper: AppWrapper,
|
||||
});
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
|
||||
expect(await screen.findByText('All broken :(')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { DropButton, Text } from '@/components';
|
||||
import { Access, Role } from '@/features/teams/api';
|
||||
|
||||
interface MemberActionProps {
|
||||
access: Access;
|
||||
currentRole: Role;
|
||||
teamId: string;
|
||||
}
|
||||
|
||||
export const MemberAction = ({
|
||||
access,
|
||||
currentRole,
|
||||
teamId,
|
||||
}: MemberActionProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [isDropOpen, setIsDropOpen] = useState(false);
|
||||
|
||||
if (
|
||||
currentRole === Role.MEMBER ||
|
||||
(access.role === Role.OWNER && currentRole === Role.ADMIN)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropButton
|
||||
button={
|
||||
<span
|
||||
aria-label={t('Member options')}
|
||||
className="material-icons"
|
||||
style={{
|
||||
transition: 'all 0.3s ease-in-out',
|
||||
transform: `rotate(${isDropOpen ? '90' : '0'}deg)`,
|
||||
}}
|
||||
>
|
||||
more_vert
|
||||
</span>
|
||||
}
|
||||
onOpenChange={(isOpen) => setIsDropOpen(isOpen)}
|
||||
isOpen={isDropOpen}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsDropOpen(false);
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">edit</span>}
|
||||
>
|
||||
<Text $theme="primary">{t('Update the role')}</Text>
|
||||
</Button>
|
||||
</DropButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -5,23 +5,25 @@ import { useTranslation } from 'react-i18next';
|
||||
import IconUser from '@/assets/icons/icon-user.svg';
|
||||
import { Box, Card, TextErrors } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Team, useTeamAccesses } from '@/features/teams/api/';
|
||||
import { Role, useTeamAccesses } from '@/features/teams/api/';
|
||||
import { PAGE_SIZE } from '@/features/teams/conf';
|
||||
|
||||
import { MemberAction } from './MemberAction';
|
||||
|
||||
interface MemberGridProps {
|
||||
team: Team;
|
||||
teamId: string;
|
||||
currentRole: Role;
|
||||
}
|
||||
|
||||
export const MemberGrid = ({ team }: MemberGridProps) => {
|
||||
export const MemberGrid = ({ teamId, currentRole }: MemberGridProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const pagination = usePagination({
|
||||
pageSize: PAGE_SIZE,
|
||||
});
|
||||
const { page, pageSize, setPagesCount } = pagination;
|
||||
const { data, isLoading, error } = useTeamAccesses({
|
||||
teamId: team.id,
|
||||
teamId: teamId,
|
||||
page,
|
||||
});
|
||||
|
||||
@@ -79,6 +81,18 @@ export const MemberGrid = ({ team }: MemberGridProps) => {
|
||||
field: 'role',
|
||||
headerName: t('Roles'),
|
||||
},
|
||||
{
|
||||
id: 'column-actions',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<MemberAction
|
||||
teamId={teamId}
|
||||
access={row}
|
||||
currentRole={currentRole}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
rows={accesses || []}
|
||||
isLoading={isLoading}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"Names": "Noms",
|
||||
"Emails": "Emails",
|
||||
"Roles": "Rôles",
|
||||
"Member options": "Options des Membres",
|
||||
"Sort the teams": "Trier les groupes",
|
||||
"Sort teams icon": "Icône trier les groupes",
|
||||
"Add a team": "Ajouter un groupe",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ReactElement } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { TextErrors } from '@/components/TextErrors';
|
||||
import { MemberGrid, TeamInfo, useTeam } from '@/features/teams/';
|
||||
import { MemberGrid, Role, TeamInfo, useTeam } from '@/features/teams/';
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
import TeamLayout from './TeamLayout';
|
||||
@@ -47,10 +47,16 @@ const Team = ({ id }: TeamProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
const currentRole = team.abilities.delete
|
||||
? Role.OWNER
|
||||
: team.abilities.manage_accesses
|
||||
? Role.ADMIN
|
||||
: Role.MEMBER;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TeamInfo team={team} />
|
||||
<MemberGrid team={team} />
|
||||
<MemberGrid teamId={team.id} currentRole={currentRole} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3059,6 +3059,11 @@
|
||||
"@testing-library/dom" "^9.0.0"
|
||||
"@types/react-dom" "^18.0.0"
|
||||
|
||||
"@testing-library/user-event@14.5.2":
|
||||
version "14.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.2.tgz#db7257d727c891905947bd1c1a99da20e03c2ebd"
|
||||
integrity sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==
|
||||
|
||||
"@tootallnate/once@2":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
|
||||
@@ -8895,7 +8900,7 @@ typed-array-length@^1.0.5:
|
||||
is-typed-array "^1.1.13"
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
typescript@*, typescript@5.3.3, typescript@^5.0.4:
|
||||
typescript@*, typescript@^5.0.4:
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
|
||||
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
|
||||
|
||||
Reference in New Issue
Block a user