diff --git a/src/frontend/apps/desk/src/features/members/__tests__/MemberGrid.test.tsx b/src/frontend/apps/desk/src/features/members/__tests__/MemberGrid.test.tsx
index e9187e6..9103b71 100644
--- a/src/frontend/apps/desk/src/features/members/__tests__/MemberGrid.test.tsx
+++ b/src/frontend/apps/desk/src/features/members/__tests__/MemberGrid.test.tsx
@@ -1,5 +1,5 @@
import '@testing-library/jest-dom';
-import { render, screen } from '@testing-library/react';
+import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
@@ -215,4 +215,132 @@ describe('MemberGrid', () => {
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/desk/src/features/members/api/useTeamsAccesses.tsx b/src/frontend/apps/desk/src/features/members/api/useTeamsAccesses.tsx
index cfadc9f..ee4f4ea 100644
--- a/src/frontend/apps/desk/src/features/members/api/useTeamsAccesses.tsx
+++ b/src/frontend/apps/desk/src/features/members/api/useTeamsAccesses.tsx
@@ -7,6 +7,7 @@ import { Access } from '../types';
export type TeamAccessesAPIParams = {
page: number;
teamId: string;
+ ordering?: string;
};
type AccessesResponse = APIList;
@@ -14,8 +15,15 @@ type AccessesResponse = APIList;
export const getTeamAccesses = async ({
page,
teamId,
+ ordering,
}: TeamAccessesAPIParams): Promise => {
- const response = await fetchAPI(`teams/${teamId}/accesses/?page=${page}`);
+ let url = `teams/${teamId}/accesses/?page=${page}`;
+
+ if (ordering) {
+ url += '&ordering=' + ordering;
+ }
+
+ const response = await fetchAPI(url);
if (!response.ok) {
throw new APIError(
diff --git a/src/frontend/apps/desk/src/features/members/components/MemberGrid.tsx b/src/frontend/apps/desk/src/features/members/components/MemberGrid.tsx
index 7d6771f..bbf0f8f 100644
--- a/src/frontend/apps/desk/src/features/members/components/MemberGrid.tsx
+++ b/src/frontend/apps/desk/src/features/members/components/MemberGrid.tsx
@@ -1,4 +1,9 @@
-import { Button, DataGrid, usePagination } from '@openfun/cunningham-react';
+import {
+ Button,
+ DataGrid,
+ SortModel,
+ usePagination,
+} from '@openfun/cunningham-react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -18,6 +23,33 @@ interface MemberGridProps {
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();
@@ -25,24 +57,42 @@ export const MemberGrid = ({ team, currentRole }: MemberGridProps) => {
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 accesses = data?.results;
-
- useEffect(() => {
- setPagesCount(data?.count ? Math.ceil(data.count / pageSize) : 0);
- }, [data?.count, pageSize, setPagesCount]);
-
- const dictRole = {
+ 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 && (
@@ -104,11 +154,8 @@ export const MemberGrid = ({ team, currentRole }: MemberGridProps) => {
headerName: t('Emails'),
},
{
- id: 'role',
+ field: 'localizedRole',
headerName: t('Roles'),
- renderCell({ row }) {
- return dictRole[row.role];
- },
},
{
id: 'column-actions',
@@ -123,9 +170,11 @@ export const MemberGrid = ({ team, currentRole }: MemberGridProps) => {
},
},
]}
- rows={accesses || []}
+ rows={accesses}
isLoading={isLoading}
pagination={pagination}
+ onSortModelChange={setSortModel}
+ sortModel={sortModel}
/>
{isModalMemberOpen && (