🐛(front) fix missing pagination mail domains (#946)

fix missing pagination mail domains + mailboxes list
This commit is contained in:
elvoisin
2025-07-02 09:03:11 +02:00
committed by GitHub
parent 405e5fda90
commit 06d4d5c9e8
5 changed files with 197 additions and 84 deletions

View File

@@ -12,6 +12,7 @@ and this project adheres to
### Added ### Added
- 🐛(front) fix missing pagination mail domains
- 🐛(front) fix button add mail domain - 🐛(front) fix button add mail domain
- ✨(teams) add matrix webhook for teams #904 - ✨(teams) add matrix webhook for teams #904
- ✨(resource-server) add SCIM /Me endpoint #895 - ✨(resource-server) add SCIM /Me endpoint #895

View File

@@ -1,5 +1,5 @@
import { Button, DataGrid } from '@openfun/cunningham-react'; import { Button, DataGrid } from '@openfun/cunningham-react';
import { useMemo } from 'react'; import { useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Box, StyledLink, Tag, Text } from '@/components'; import { Box, StyledLink, Tag, Text } from '@/components';
@@ -17,7 +17,8 @@ export function MailDomainsListView({ querySearch }: MailDomainsListViewProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { ordering } = useMailDomainsStore(); const { ordering } = useMailDomainsStore();
const { data, isLoading } = useMailDomains({ ordering }); const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } =
useMailDomains({ ordering });
const mailDomains = useMemo(() => { const mailDomains = useMemo(() => {
return data?.pages.reduce((acc, page) => { return data?.pages.reduce((acc, page) => {
return acc.concat(page.results); return acc.concat(page.results);
@@ -38,6 +39,31 @@ export function MailDomainsListView({ querySearch }: MailDomainsListViewProps) {
); );
}, [querySearch, mailDomains]); }, [querySearch, mailDomains]);
const loadMoreRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!hasNextPage) {
return;
}
const ref = loadMoreRef.current;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
void fetchNextPage();
}
},
{ threshold: 1 },
);
if (ref) {
observer.observe(ref);
}
return () => {
if (ref) {
observer.unobserve(ref);
}
};
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
return ( return (
<div role="listbox"> <div role="listbox">
{filteredMailDomains && filteredMailDomains.length ? ( {filteredMailDomains && filteredMailDomains.length ? (
@@ -47,17 +73,17 @@ export function MailDomainsListView({ querySearch }: MailDomainsListViewProps) {
columns={[ columns={[
{ {
field: 'name', field: 'name',
headerName: 'Domaine', headerName: `${t('Domain')} (${filteredMailDomains.length})`,
enableSorting: true, enableSorting: true,
}, },
{ {
field: 'count_mailboxes', field: 'count_mailboxes',
headerName: "Nombre d'adresses", headerName: `${t('Number of mailboxes')}`,
enableSorting: true, enableSorting: true,
}, },
{ {
id: 'status', id: 'status',
headerName: 'Statut', headerName: `${t('Status')}`,
enableSorting: true, enableSorting: true,
renderCell({ row }) { renderCell({ row }) {
return ( return (
@@ -96,6 +122,8 @@ export function MailDomainsListView({ querySearch }: MailDomainsListViewProps) {
isLoading={isLoading} isLoading={isLoading}
/> />
) : null} ) : null}
<div ref={loadMoreRef} style={{ height: 32 }} />
{isFetchingNextPage && <div>{t('Loading more...')}</div>}
{!filteredMailDomains || {!filteredMailDomains ||
(!filteredMailDomains.length && ( (!filteredMailDomains.length && (
<Text $align="center" $size="small"> <Text $align="center" $size="small">

View File

@@ -0,0 +1,45 @@
import { useInfiniteQuery } from '@tanstack/react-query';
import { APIError, APIList } from '@/api';
import { MailDomainMailbox } from '../types';
import {
KEY_LIST_MAILBOX,
MailDomainMailboxesParams,
getMailDomainMailboxes,
} from './useMailboxes';
// Redéfinition locale du type
type MailDomainMailboxesResponse = APIList<MailDomainMailbox>;
export function useMailboxesInfinite(
param: Omit<MailDomainMailboxesParams, 'page'>,
queryConfig = {},
) {
return useInfiniteQuery<
MailDomainMailboxesResponse,
APIError,
MailDomainMailboxesResponse,
[string, Omit<MailDomainMailboxesParams, 'page'>],
number
>({
queryKey: [KEY_LIST_MAILBOX, param],
queryFn: ({ pageParam = 1 }) =>
getMailDomainMailboxes({ ...param, page: pageParam }),
getNextPageParam: (lastPage): number | undefined => {
if (!lastPage.next) {
return undefined;
}
try {
const url = new URL(lastPage.next, window.location.origin);
const nextPage = url.searchParams.get('page');
return nextPage ? Number(nextPage) : undefined;
} catch {
return undefined;
}
},
initialPageParam: 1,
...queryConfig,
});
}

View File

@@ -1,5 +1,6 @@
import { DataGrid, SortModel, usePagination } from '@openfun/cunningham-react'; import { DataGrid, SortModel } from '@openfun/cunningham-react';
import { useMemo, useState } from 'react'; import type { InfiniteData } from '@tanstack/react-query';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Box, Tag, Text, TextErrors } from '@/components'; import { Box, Tag, Text, TextErrors } from '@/components';
@@ -9,11 +10,17 @@ import {
MailDomainMailboxStatus, MailDomainMailboxStatus,
} from '@/features/mail-domains/mailboxes/types'; } from '@/features/mail-domains/mailboxes/types';
import { PAGE_SIZE } from '../../../conf'; import { useMailboxesInfinite } from '../../api/useMailboxesInfinite';
import { useMailboxes } from '../../api/useMailboxes';
import { PanelActions } from './PanelActions'; import { PanelActions } from './PanelActions';
type MailDomainMailboxesResponse = {
count: number;
next: string | null;
previous: string | null;
results: MailDomainMailbox[];
};
interface MailBoxesListViewProps { interface MailBoxesListViewProps {
mailDomain: MailDomain; mailDomain: MailDomain;
querySearch: string; querySearch: string;
@@ -43,101 +50,133 @@ export function MailBoxesListView({
const [sortModel] = useState<SortModel>([]); const [sortModel] = useState<SortModel>([]);
const pagination = usePagination({
defaultPage: 1,
pageSize: PAGE_SIZE,
});
const { page } = pagination;
const ordering = sortModel.length ? formatSortModel(sortModel[0]) : undefined; const ordering = sortModel.length ? formatSortModel(sortModel[0]) : undefined;
const { data, isLoading, error } = useMailboxes({ const {
data,
isLoading,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useMailboxesInfinite({
mailDomainSlug: mailDomain.slug, mailDomainSlug: mailDomain.slug,
page,
ordering, ordering,
}); }) as {
data: InfiniteData<MailDomainMailboxesResponse, number> | undefined;
isLoading: boolean;
error: { cause?: string[] };
fetchNextPage: () => void;
hasNextPage: boolean | undefined;
isFetchingNextPage: boolean;
};
const mailboxes: ViewMailbox[] = useMemo(() => { const mailboxes: ViewMailbox[] = useMemo(() => {
if (!mailDomain || !data?.results?.length) { if (!mailDomain || !data?.pages?.length) {
return []; return [];
} }
return data.pages.flatMap((page) =>
return data.results.map((mailbox: MailDomainMailbox) => ({ page.results.map((mailbox: MailDomainMailbox) => ({
email: `${mailbox.local_part}@${mailDomain.name}`, email: `${mailbox.local_part}@${mailDomain.name}`,
name: `${mailbox.first_name} ${mailbox.last_name}`, name: `${mailbox.first_name} ${mailbox.last_name}`,
id: mailbox.id, id: mailbox.id,
status: mailbox.status, status: mailbox.status,
mailbox, mailbox,
})); })),
}, [data?.results, mailDomain]); );
}, [data, mailDomain]);
const filteredMailboxes = useMemo(() => { const filteredMailboxes = useMemo(() => {
if (!querySearch) { if (!querySearch) {
return mailboxes; return mailboxes;
} }
const lowerCaseSearch = querySearch.toLowerCase(); const lowerCaseSearch = querySearch.toLowerCase();
return ( return mailboxes.filter((mailbox) =>
(mailboxes && mailbox.email.toLowerCase().includes(lowerCaseSearch),
mailboxes.filter((mailbox) =>
mailbox.email.toLowerCase().includes(lowerCaseSearch),
)) ||
[]
); );
}, [querySearch, mailboxes]); }, [querySearch, mailboxes]);
const loadMoreRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!hasNextPage) {
return;
}
const ref = loadMoreRef.current;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
},
{ threshold: 1 },
);
if (ref) {
observer.observe(ref);
}
return () => {
if (ref) {
observer.unobserve(ref);
}
};
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
return ( return (
<div> <div>
{error && <TextErrors causes={error.cause} />} {error && <TextErrors causes={error.cause ?? []} />}
{filteredMailboxes && filteredMailboxes.length ? ( {filteredMailboxes && filteredMailboxes.length ? (
<DataGrid <>
aria-label="listbox" <DataGrid
rows={filteredMailboxes} aria-label="listbox"
columns={[ rows={filteredMailboxes}
{ columns={[
field: 'email', {
headerName: `${t('Address')}${filteredMailboxes.length}`, field: 'email',
renderCell: ({ row }) => <Text>{row.email}</Text>, headerName: `${t('Address')}${filteredMailboxes.length}`,
}, renderCell: ({ row }) => <Text>{row.email}</Text>,
{
field: 'name',
headerName: t('User'),
enableSorting: true,
renderCell: ({ row }) => (
<Text
$weight="500"
$theme="greyscale"
$css="text-transform: capitalize;"
>
{row.name}
</Text>
),
},
{
id: 'status',
headerName: t('Status'),
enableSorting: true,
renderCell({ row }) {
return (
<Box $direction="row" $align="center">
<Tag
showTooltip={true}
status={row.status}
tooltipType="mail"
></Tag>
</Box>
);
}, },
}, {
{ field: 'name',
id: 'actions', headerName: t('User'),
renderCell: ({ row }) => ( enableSorting: true,
<PanelActions mailDomain={mailDomain} mailbox={row} /> renderCell: ({ row }) => (
), <Text
}, $weight="500"
]} $theme="greyscale"
isLoading={isLoading} $css="text-transform: capitalize;"
/> >
{row.name}
</Text>
),
},
{
id: 'status',
headerName: t('Status'),
enableSorting: true,
renderCell({ row }) {
return (
<Box $direction="row" $align="center">
<Tag
showTooltip={true}
status={row.status}
tooltipType="mail"
></Tag>
</Box>
);
},
},
{
id: 'actions',
renderCell: ({ row }) => (
<PanelActions mailDomain={mailDomain} mailbox={row} />
),
},
]}
isLoading={isLoading}
/>
<div ref={loadMoreRef} style={{ height: 32 }} />
{isFetchingNextPage && <div>{t('Loading more...')}</div>}
</>
) : null} ) : null}
</div> </div>
); );

View File

@@ -63,7 +63,7 @@ const Page: NextPageWithLayout = () => {
$gap="1em" $gap="1em"
$css="margin-bottom: 20px;" $css="margin-bottom: 20px;"
> >
<Box $css="width: calc(100% - 245px);" $flex="1"> <Box $flex="1">
<Input <Input
style={{ width: '100%' }} style={{ width: '100%' }}
label={t('Search a mail domain')} label={t('Search a mail domain')}