diff --git a/CHANGELOG.md b/CHANGELOG.md index 09ca750d..581ac010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to - ✨(demo) add aliases to demo #1050 - ✨(front) add icon to button to configure a domain +- ✨(datagrid) add sort to mailboxes list + mail domain list - ✨(invitations) allow delete invitations mails domains access by an admin - ✨(front) delete invitations mails domains access - ✨(front) add show invitations mails domains access #1040 diff --git a/src/frontend/apps/desk/src/features/mail-domains/aliases/components/panel/AliasesListView.tsx b/src/frontend/apps/desk/src/features/mail-domains/aliases/components/panel/AliasesListView.tsx index c8821092..e3adafd3 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/aliases/components/panel/AliasesListView.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/aliases/components/panel/AliasesListView.tsx @@ -7,6 +7,7 @@ import { Box, Text, TextErrors } from '@/components'; import { useAuthStore } from '@/core/auth'; import { Alias, AliasGroup } from '@/features/mail-domains/aliases/types'; import { MailDomain } from '@/features/mail-domains/domains'; +import { sortData } from '@/utils/sortData'; import { useAliasesInfinite } from '../../api/useAliasesInfinite'; import { ModalEditAlias } from '../ModalEditAlias'; @@ -23,15 +24,6 @@ interface AliasesListViewProps { querySearch: string; } -type SortModelItem = { - field: string; - sort: 'asc' | 'desc' | null; -}; - -function formatSortModel(sortModel: SortModelItem) { - return sortModel.sort === 'desc' ? `-${sortModel.field}` : sortModel.field; -} - export function AliasesListView({ mailDomain, querySearch, @@ -42,9 +34,13 @@ export function AliasesListView({ null, ); - const [sortModel] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: 'local_part', + sort: 'desc', + }, + ]); - const ordering = sortModel.length ? formatSortModel(sortModel[0]) : undefined; const { data, isLoading, @@ -54,7 +50,6 @@ export function AliasesListView({ isFetchingNextPage, } = useAliasesInfinite({ mailDomainSlug: mailDomain.slug, - ordering, }) as { data: InfiniteData | undefined; isLoading: boolean; @@ -90,7 +85,7 @@ export function AliasesListView({ local_part: alias.local_part, destinations: [alias.destination], destinationIds, - count_destinations: 1, + count_destinations: [alias.destination].length, }); } }); @@ -100,18 +95,21 @@ export function AliasesListView({ }, [data, mailDomain]); const filteredAliases = useMemo(() => { - if (!querySearch) { - return aliasGroups; + let result = aliasGroups; + + if (querySearch) { + const lowerCaseSearch = querySearch.toLowerCase(); + result = result.filter( + (alias) => + alias.email.toLowerCase().includes(lowerCaseSearch) || + alias.destinations.some((dest) => + dest.toLowerCase().includes(lowerCaseSearch), + ), + ); } - const lowerCaseSearch = querySearch.toLowerCase(); - return aliasGroups.filter( - (alias) => - alias.email.toLowerCase().includes(lowerCaseSearch) || - alias.destinations.some((dest) => - dest.toLowerCase().includes(lowerCaseSearch), - ), - ); - }, [querySearch, aliasGroups]); + + return sortData(result, sortModel); + }, [querySearch, aliasGroups, sortModel]); const loadMoreRef = useRef(null); @@ -155,7 +153,7 @@ export function AliasesListView({ rows={filteredAliases} columns={[ { - field: 'email', + field: 'local_part', headerName: `${t('Alias')} • ${filteredAliases.length}`, renderCell: ({ row }) => ( @@ -164,9 +162,8 @@ export function AliasesListView({ ), }, { - field: 'destinations', + field: 'count_destinations', headerName: t('Destinations'), - enableSorting: false, renderCell: ({ row }) => ( {row.count_destinations} destination @@ -205,6 +202,8 @@ export function AliasesListView({ }, ]} isLoading={isLoading} + sortModel={sortModel} + onSortModelChange={setSortModel} /> {isFetchingNextPage &&
{t('Loading more...')}
} diff --git a/src/frontend/apps/desk/src/features/mail-domains/domains/api/useMailDomains.tsx b/src/frontend/apps/desk/src/features/mail-domains/domains/api/useMailDomains.tsx index 26b848e7..987f3ac3 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/domains/api/useMailDomains.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/domains/api/useMailDomains.tsx @@ -17,7 +17,7 @@ export enum EnumMailDomainsOrdering { } export type MailDomainsParams = { - ordering: EnumMailDomainsOrdering; + ordering?: string | EnumMailDomainsOrdering; }; type MailDomainsAPIParams = MailDomainsParams & { diff --git a/src/frontend/apps/desk/src/features/mail-domains/domains/components/panel/MailDomainsListView.tsx b/src/frontend/apps/desk/src/features/mail-domains/domains/components/panel/MailDomainsListView.tsx index 5d0ea962..3b2d83ad 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/domains/components/panel/MailDomainsListView.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/domains/components/panel/MailDomainsListView.tsx @@ -1,43 +1,65 @@ -import { Button, DataGrid } from '@openfun/cunningham-react'; -import { useEffect, useMemo, useRef } from 'react'; +import { Button, DataGrid, SortModel } from '@openfun/cunningham-react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Box, StyledLink, Tag, Text } from '@/components'; -import { - MailDomain, - useMailDomains, - useMailDomainsStore, -} from '@/features/mail-domains/domains'; +import { MailDomain, useMailDomains } from '@/features/mail-domains/domains'; +import { sortData } from '@/utils/sortData'; interface MailDomainsListViewProps { querySearch: string; } +type ViewMailDomain = { + id: MailDomain['id']; + name: string; + count_mailboxes?: number; + status: MailDomain['status']; + slug: string; + mailDomain: MailDomain; +}; + export function MailDomainsListView({ querySearch }: MailDomainsListViewProps) { const { t } = useTranslation(); - const { ordering } = useMailDomainsStore(); + const [sortModel, setSortModel] = useState([ + { + field: 'name', + sort: 'desc', + }, + ]); + const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = - useMailDomains({ ordering }); - const mailDomains = useMemo(() => { - return data?.pages.reduce((acc, page) => { - return acc.concat(page.results); - }, [] as MailDomain[]); - }, [data?.pages]); + useMailDomains({}); + + const mailDomains: ViewMailDomain[] = useMemo(() => { + if (!data?.pages?.length) { + return []; + } + return data.pages.flatMap((page) => + page.results.map((mailDomain: MailDomain) => ({ + id: mailDomain.id, + name: mailDomain.name, + count_mailboxes: mailDomain.count_mailboxes, + status: mailDomain.status, + slug: mailDomain.slug, + mailDomain: mailDomain, + })), + ); + }, [data]); const filteredMailDomains = useMemo(() => { - if (!querySearch) { - return mailDomains; + let result = mailDomains; + + if (querySearch) { + const lowerCaseSearch = querySearch.toLowerCase(); + result = result.filter((domain) => + domain.name.toLowerCase().includes(lowerCaseSearch), + ); } - const lowerCaseSearch = querySearch.toLowerCase(); - return ( - (mailDomains && - mailDomains.filter((domain) => - domain.name.toLowerCase().includes(lowerCaseSearch), - )) || - [] - ); - }, [querySearch, mailDomains]); + + return sortData(result, sortModel); + }, [querySearch, mailDomains, sortModel]); const loadMoreRef = useRef(null); @@ -74,17 +96,15 @@ export function MailDomainsListView({ querySearch }: MailDomainsListViewProps) { { field: 'name', headerName: `${t('Domain')} (${filteredMailDomains.length})`, - enableSorting: true, }, { field: 'count_mailboxes', headerName: `${t('Number of mailboxes')}`, - enableSorting: true, }, { id: 'status', + field: 'status', headerName: `${t('Status')}`, - enableSorting: true, renderCell({ row }) { return ( @@ -120,6 +140,8 @@ export function MailDomainsListView({ querySearch }: MailDomainsListViewProps) { }, ]} isLoading={isLoading} + sortModel={sortModel} + onSortModelChange={setSortModel} /> ) : null}
diff --git a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/MailBoxesView.tsx b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/MailBoxesView.tsx index ada1e4f4..a604290d 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/MailBoxesView.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/MailBoxesView.tsx @@ -294,11 +294,8 @@ const AlertStatus = ({ status }: { status: MailDomain['status'] }) => { message: ( {t( - 'The domain name encounters an error. Please contact our support team to solve the problem: ', + 'The domain name encounters an error. Please contact our support team to solve the problem. ', )} - - suiteterritoriale@anct.gouv.fr - ), }; diff --git a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/__tests__/MailDomainsContent.test.tsx b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/__tests__/MailDomainsContent.test.tsx index bfe4f556..d84380ef 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/__tests__/MailDomainsContent.test.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/__tests__/MailDomainsContent.test.tsx @@ -75,9 +75,11 @@ describe('MailBoxesView', () => { }); it('renders with no mailboxes and displays empty placeholder', async () => { - fetchMock.get('end:/mail-domains/example-com/mailboxes/?page=1', { + fetchMock.get(/mail-domains\/example-com\/mailboxes\/\?page=1/, { count: 0, results: [], + next: null, + previous: null, }); render(, { @@ -90,25 +92,34 @@ describe('MailBoxesView', () => { }); it('renders mailboxes and displays them correctly', async () => { - fetchMock.get('end:/mail-domains/example-com/mailboxes/?page=1', { + fetchMock.get(/mail-domains\/example-com\/mailboxes\/\?page=1/, { count: 2, results: mockMailboxes, + next: null, + previous: null, }); - render(, { + const mailDomainWithCount = { + ...mockMailDomain, + count_mailboxes: 2, + }; + + render(, { wrapper: AppWrapper, }); await waitFor(() => { - expect(screen.getByText('John Doe')).toBeInTheDocument(); + expect(screen.getByText('Doe John')).toBeInTheDocument(); }); expect(screen.getByText('jane.smith@example.com')).toBeInTheDocument(); }); it('opens the create mailbox modal when button is clicked by granted user', async () => { - fetchMock.get('end:/mail-domains/example-com/mailboxes/?page=1', { + fetchMock.get(/mail-domains\/example-com\/mailboxes\/\?page=1/, { count: 0, results: [], + next: null, + previous: null, }); render(, { @@ -123,9 +134,11 @@ describe('MailBoxesView', () => { }); it('displays the correct alert based on mail domain status', async () => { - fetchMock.get('end:/mail-domains/example-com/mailboxes/?page=1', { + fetchMock.get(/mail-domains\/example-com\/mailboxes\/\?page=1/, { count: 0, results: [], + next: null, + previous: null, }); const statuses = [ @@ -154,7 +167,7 @@ describe('MailBoxesView', () => { }); it('handles API errors and displays the error message', async () => { - fetchMock.get('end:/mail-domains/example-com/mailboxes/?page=1', { + fetchMock.get(/mail-domains\/example-com\/mailboxes\/\?page=1/, { status: 500, body: { cause: 'An unexpected error occurred.', @@ -171,9 +184,11 @@ describe('MailBoxesView', () => { }); it('hides buttons to ungranted users', async () => { - fetchMock.get('end:/mail-domains/example-com/mailboxes/?page=1', { + fetchMock.get(/mail-domains\/example-com\/mailboxes\/\?page=1/, { count: 0, results: [], + next: null, + previous: null, }); render(, { diff --git a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/panel/MailBoxesListView.tsx b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/panel/MailBoxesListView.tsx index 54326d4a..56357042 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/panel/MailBoxesListView.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/mailboxes/components/panel/MailBoxesListView.tsx @@ -43,7 +43,12 @@ export function MailBoxesListView({ const { t } = useTranslation(); const { userData } = useAuthStore(); - const [sortModel] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: 'local_part', + sort: 'desc', + }, + ]); const ordering = sortModel.length ? formatSortModel(sortModel[0]) : undefined; const { @@ -69,6 +74,10 @@ export function MailBoxesListView({ if (!mailDomain || !data?.pages?.length) { return []; } + + const capitalize = (value: string) => + value ? value.charAt(0).toUpperCase() + value.slice(1) : value; + return data.pages.flatMap((page) => page.results.map((mailbox: MailDomainMailbox) => { const email = `${mailbox.local_part}@${mailDomain.name}`; @@ -77,7 +86,9 @@ export function MailBoxesListView({ return { id: mailbox.id, email, - name: `${mailbox.first_name} ${mailbox.last_name}`, + name: `${capitalize(mailbox.last_name)} ${capitalize( + mailbox.first_name, + )}`, first_name: mailbox.first_name, last_name: mailbox.last_name, local_part: mailbox.local_part, @@ -136,7 +147,7 @@ export function MailBoxesListView({ rows={filteredMailboxes} columns={[ { - field: 'email', + field: 'local_part', headerName: `${t('Address')} • ${filteredMailboxes.length}`, renderCell: ({ row }) => ( ( - + {row.name} ), }, { + field: 'status', id: 'status', headerName: t('Status'), - enableSorting: true, renderCell({ row }) { return ( @@ -185,6 +191,8 @@ export function MailBoxesListView({ }, ]} isLoading={isLoading} + sortModel={sortModel} + onSortModelChange={setSortModel} /> {isFetchingNextPage &&
{t('Loading more...')}
} diff --git a/src/frontend/apps/desk/src/utils/sortData.ts b/src/frontend/apps/desk/src/utils/sortData.ts new file mode 100644 index 00000000..94dda477 --- /dev/null +++ b/src/frontend/apps/desk/src/utils/sortData.ts @@ -0,0 +1,36 @@ +import { SortModel } from '@openfun/cunningham-react'; + +export function sortData( + data: T[], + sortModel: SortModel, +): T[] { + const [sort] = sortModel; + + if (!sort?.sort) { + return data; + } + + const field: string = Array.isArray(sort.field) + ? String(sort.field[0]) + : String(sort.field); + + const direction = sort.sort === 'asc' ? 1 : -1; + + return [...data].sort((a, b) => { + const aRecord = a as Record; + const bRecord = b as Record; + const aValue = aRecord[field] as string | number | null | undefined; + const bValue = bRecord[field] as string | number | null | undefined; + + if (typeof aValue === 'number' && typeof bValue === 'number') { + return direction * (aValue - bValue); + } + + return ( + direction * + String(aValue ?? '').localeCompare(String(bValue ?? ''), undefined, { + sensitivity: 'base', + }) + ); + }); +} diff --git a/src/frontend/apps/e2e/__tests__/app-desk/mail-domain.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/mail-domain.spec.ts index 4903448f..f181a0b1 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/mail-domain.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/mail-domain.spec.ts @@ -73,7 +73,7 @@ const assertFilledMailboxesTableElementsAreVisible = async ( const table = page.locator('table'); await expect(table).toBeVisible(); - const tdNames = await table.getByText('John Doe').all(); + const tdNames = await table.getByText('Doe John').all(); expect(tdNames.length).toEqual(20); }; @@ -542,14 +542,10 @@ test.describe('Mail domain', () => { await expect( page.getByText( - 'The domain name encounters an error. Please contact our support team to solve the problem:', + 'The domain name encounters an error. Please contact our support team to solve the problem.', ), ).toBeVisible(); - await expect( - page.getByRole('link', { name: 'suiteterritoriale@anct.gouv.fr' }), - ).toBeVisible(); - await expect(page.getByTestId('button-new-mailbox')).toBeDisabled(); await expect( @@ -971,7 +967,7 @@ test.describe('Mail domain', () => { await expect( page.getByText( - 'The domain name encounters an error. Please contact our support team to solve the problem:', + 'The domain name encounters an error. Please contact our support team to solve the problem.', ), ).toBeVisible();