🐛(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
- 🐛(front) fix missing pagination mail domains
- 🐛(front) fix button add mail domain
- ✨(teams) add matrix webhook for teams #904
- ✨(resource-server) add SCIM /Me endpoint #895

View File

@@ -1,5 +1,5 @@
import { Button, DataGrid } from '@openfun/cunningham-react';
import { useMemo } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, StyledLink, Tag, Text } from '@/components';
@@ -17,7 +17,8 @@ export function MailDomainsListView({ querySearch }: MailDomainsListViewProps) {
const { t } = useTranslation();
const { ordering } = useMailDomainsStore();
const { data, isLoading } = useMailDomains({ ordering });
const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } =
useMailDomains({ ordering });
const mailDomains = useMemo(() => {
return data?.pages.reduce((acc, page) => {
return acc.concat(page.results);
@@ -38,6 +39,31 @@ export function MailDomainsListView({ querySearch }: MailDomainsListViewProps) {
);
}, [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 (
<div role="listbox">
{filteredMailDomains && filteredMailDomains.length ? (
@@ -47,17 +73,17 @@ export function MailDomainsListView({ querySearch }: MailDomainsListViewProps) {
columns={[
{
field: 'name',
headerName: 'Domaine',
headerName: `${t('Domain')} (${filteredMailDomains.length})`,
enableSorting: true,
},
{
field: 'count_mailboxes',
headerName: "Nombre d'adresses",
headerName: `${t('Number of mailboxes')}`,
enableSorting: true,
},
{
id: 'status',
headerName: 'Statut',
headerName: `${t('Status')}`,
enableSorting: true,
renderCell({ row }) {
return (
@@ -96,6 +122,8 @@ export function MailDomainsListView({ querySearch }: MailDomainsListViewProps) {
isLoading={isLoading}
/>
) : null}
<div ref={loadMoreRef} style={{ height: 32 }} />
{isFetchingNextPage && <div>{t('Loading more...')}</div>}
{!filteredMailDomains ||
(!filteredMailDomains.length && (
<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 { useMemo, useState } from 'react';
import { DataGrid, SortModel } from '@openfun/cunningham-react';
import type { InfiniteData } from '@tanstack/react-query';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Tag, Text, TextErrors } from '@/components';
@@ -9,11 +10,17 @@ import {
MailDomainMailboxStatus,
} from '@/features/mail-domains/mailboxes/types';
import { PAGE_SIZE } from '../../../conf';
import { useMailboxes } from '../../api/useMailboxes';
import { useMailboxesInfinite } from '../../api/useMailboxesInfinite';
import { PanelActions } from './PanelActions';
type MailDomainMailboxesResponse = {
count: number;
next: string | null;
previous: string | null;
results: MailDomainMailbox[];
};
interface MailBoxesListViewProps {
mailDomain: MailDomain;
querySearch: string;
@@ -43,101 +50,133 @@ export function MailBoxesListView({
const [sortModel] = useState<SortModel>([]);
const pagination = usePagination({
defaultPage: 1,
pageSize: PAGE_SIZE,
});
const { page } = pagination;
const ordering = sortModel.length ? formatSortModel(sortModel[0]) : undefined;
const { data, isLoading, error } = useMailboxes({
const {
data,
isLoading,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useMailboxesInfinite({
mailDomainSlug: mailDomain.slug,
page,
ordering,
});
}) as {
data: InfiniteData<MailDomainMailboxesResponse, number> | undefined;
isLoading: boolean;
error: { cause?: string[] };
fetchNextPage: () => void;
hasNextPage: boolean | undefined;
isFetchingNextPage: boolean;
};
const mailboxes: ViewMailbox[] = useMemo(() => {
if (!mailDomain || !data?.results?.length) {
if (!mailDomain || !data?.pages?.length) {
return [];
}
return data.results.map((mailbox: MailDomainMailbox) => ({
email: `${mailbox.local_part}@${mailDomain.name}`,
name: `${mailbox.first_name} ${mailbox.last_name}`,
id: mailbox.id,
status: mailbox.status,
mailbox,
}));
}, [data?.results, mailDomain]);
return data.pages.flatMap((page) =>
page.results.map((mailbox: MailDomainMailbox) => ({
email: `${mailbox.local_part}@${mailDomain.name}`,
name: `${mailbox.first_name} ${mailbox.last_name}`,
id: mailbox.id,
status: mailbox.status,
mailbox,
})),
);
}, [data, mailDomain]);
const filteredMailboxes = useMemo(() => {
if (!querySearch) {
return mailboxes;
}
const lowerCaseSearch = querySearch.toLowerCase();
return (
(mailboxes &&
mailboxes.filter((mailbox) =>
mailbox.email.toLowerCase().includes(lowerCaseSearch),
)) ||
[]
return mailboxes.filter((mailbox) =>
mailbox.email.toLowerCase().includes(lowerCaseSearch),
);
}, [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 (
<div>
{error && <TextErrors causes={error.cause} />}
{error && <TextErrors causes={error.cause ?? []} />}
{filteredMailboxes && filteredMailboxes.length ? (
<DataGrid
aria-label="listbox"
rows={filteredMailboxes}
columns={[
{
field: 'email',
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>
);
<>
<DataGrid
aria-label="listbox"
rows={filteredMailboxes}
columns={[
{
field: 'email',
headerName: `${t('Address')}${filteredMailboxes.length}`,
renderCell: ({ row }) => <Text>{row.email}</Text>,
},
},
{
id: 'actions',
renderCell: ({ row }) => (
<PanelActions mailDomain={mailDomain} mailbox={row} />
),
},
]}
isLoading={isLoading}
/>
{
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>
);
},
},
{
id: 'actions',
renderCell: ({ row }) => (
<PanelActions mailDomain={mailDomain} mailbox={row} />
),
},
]}
isLoading={isLoading}
/>
<div ref={loadMoreRef} style={{ height: 32 }} />
{isFetchingNextPage && <div>{t('Loading more...')}</div>}
</>
) : null}
</div>
);

View File

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