feat(ui): tweak pagination buttons

- Fix off-by-one error when going to last page
- Update component to have minimal/no layout shift
This commit is contained in:
psychedelicious 2024-06-21 18:20:45 +10:00
parent 1f22f6ae02
commit 689dc30f87
2 changed files with 149 additions and 138 deletions

View File

@ -1,10 +1,23 @@
import { Button, Flex, IconButton, Text } from '@invoke-ai/ui-library'; import { Button, Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
import { useGalleryPagination } from '../../hooks/useGalleryPagination'; import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination';
import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi'; import { PiCaretDoubleLeftBold, PiCaretDoubleRightBold, PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi';
export const GalleryPagination = () => { export const GalleryPagination = () => {
const { goPrev, goNext, isPrevEnabled, isNextEnabled, pageButtons, goToPage, currentPage, rangeDisplay, total } = const {
useGalleryPagination(); goPrev,
goNext,
goToFirst,
goToLast,
isFirstEnabled,
isLastEnabled,
isPrevEnabled,
isNextEnabled,
pageButtons,
goToPage,
currentPage,
rangeDisplay,
total,
} = useGalleryPagination();
if (!total) { if (!total) {
return <Flex flexDir="column" alignItems="center" gap="2" height="48px"></Flex>; return <Flex flexDir="column" alignItems="center" gap="2" height="48px"></Flex>;
@ -12,7 +25,14 @@ export const GalleryPagination = () => {
return ( return (
<Flex flexDir="column" alignItems="center" gap="2" height="48px"> <Flex flexDir="column" alignItems="center" gap="2" height="48px">
<Flex gap={2} alignItems="flex-end"> <Flex gap={2} alignItems="center" w="full">
<IconButton
size="sm"
aria-label="prev"
icon={<PiCaretDoubleLeftBold />}
onClick={goToFirst}
isDisabled={!isFirstEnabled}
/>
<IconButton <IconButton
size="sm" size="sm"
aria-label="prev" aria-label="prev"
@ -20,20 +40,18 @@ export const GalleryPagination = () => {
onClick={goPrev} onClick={goPrev}
isDisabled={!isPrevEnabled} isDisabled={!isPrevEnabled}
/> />
{pageButtons.map((page) => <Spacer />
typeof page === 'number' ? ( {pageButtons.map((page) => (
<Button <Button
size="sm" size="sm"
key={page} key={page}
onClick={goToPage.bind(null, page)} onClick={goToPage.bind(null, page)}
variant={currentPage === page ? 'solid' : 'outline'} variant={currentPage === page ? 'solid' : 'outline'}
> >
{page + 1} {page + 1}
</Button> </Button>
) : ( ))}
<Text fontSize="md">...</Text> <Spacer />
)
)}
<IconButton <IconButton
size="sm" size="sm"
aria-label="next" aria-label="next"
@ -41,6 +59,13 @@ export const GalleryPagination = () => {
onClick={goNext} onClick={goNext}
isDisabled={!isNextEnabled} isDisabled={!isNextEnabled}
/> />
<IconButton
size="sm"
aria-label="next"
icon={<PiCaretDoubleRightBold />}
onClick={goToLast}
isDisabled={!isLastEnabled}
/>
</Flex> </Flex>
<Text>{rangeDisplay} Images</Text> <Text>{rangeDisplay} Images</Text>
</Flex> </Flex>

View File

@ -1,133 +1,119 @@
import { useMemo, useCallback } from "react"; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAppDispatch, useAppSelector } from "../../../app/store/storeHooks"; import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
import { useGetBoardAssetsTotalQuery, useGetBoardImagesTotalQuery } from "../../../services/api/endpoints/boards"; import { offsetChanged } from 'features/gallery/store/gallerySlice';
import { useListImagesQuery } from "../../../services/api/endpoints/images"; import { useCallback, useMemo } from 'react';
import { selectListImagesQueryArgs } from "../store/gallerySelectors"; import { useListImagesQuery } from 'services/api/endpoints/images';
import { offsetChanged } from "../store/gallerySlice";
export const useGalleryPagination = () => { export const useGalleryPagination = (pageButtonsPerSide: number = 2) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { offset, limit } = useAppSelector((s) => s.gallery); const { offset, limit } = useAppSelector((s) => s.gallery);
const queryArgs = useAppSelector(selectListImagesQueryArgs); const queryArgs = useAppSelector(selectListImagesQueryArgs);
const { count, total } = useListImagesQuery(queryArgs, { const { count, total } = useListImagesQuery(queryArgs, {
selectFromResult: ({ data }) => ({ count: data?.items.length ?? 0, total: data?.total ?? 0 }), selectFromResult: ({ data }) => ({ count: data?.items.length ?? 0, total: data?.total ?? 0 }),
}); });
const currentPage = useMemo(() => Math.ceil(offset / (limit || 0)), [offset, limit]); const currentPage = useMemo(() => Math.ceil(offset / (limit || 0)), [offset, limit]);
const pages = useMemo(() => Math.ceil(total / (limit || 0)), [total, limit]); const pages = useMemo(() => Math.ceil(total / (limit || 0)), [total, limit]);
const isNextEnabled = useMemo(() => { const isNextEnabled = useMemo(() => {
if (!count) { if (!count) {
return false; return false;
} }
return currentPage + 1 < pages; return currentPage + 1 < pages;
}, [count, currentPage, pages]); }, [count, currentPage, pages]);
const isPrevEnabled = useMemo(() => { const isPrevEnabled = useMemo(() => {
if (!count) { if (!count) {
return false; return false;
} }
return offset > 0; return offset > 0;
}, [count, offset]); }, [count, offset]);
const goNext = useCallback(() => { const goNext = useCallback(() => {
dispatch(offsetChanged(offset + (limit || 0))); dispatch(offsetChanged(offset + (limit || 0)));
}, [dispatch, offset, limit]); }, [dispatch, offset, limit]);
const goPrev = useCallback(() => { const goPrev = useCallback(() => {
dispatch(offsetChanged(Math.max(offset - (limit || 0), 0))); dispatch(offsetChanged(Math.max(offset - (limit || 0), 0)));
}, [dispatch, offset, limit]); }, [dispatch, offset, limit]);
const goToPage = useCallback( const goToPage = useCallback(
(page: number) => { (page: number) => {
const p = Math.max(0, Math.min(page, pages - 1)); const p = Math.max(0, Math.min(page, pages - 1));
dispatch(offsetChanged(page * (limit || 0))); dispatch(offsetChanged(page * (limit || 0)));
}, },
[dispatch, pages, limit] [dispatch, pages, limit]
); );
const goToFirst = useCallback(() => { const goToFirst = useCallback(() => {
dispatch(offsetChanged(0)); dispatch(offsetChanged(0));
}, [dispatch]); }, [dispatch]);
const goToLast = useCallback(() => { const goToLast = useCallback(() => {
dispatch(offsetChanged(pages * (limit || 0))); dispatch(offsetChanged((pages - 1) * (limit || 0)));
}, [dispatch, pages, limit]); }, [dispatch, pages, limit]);
// calculate the page buttons to display - current page with 3 around it // calculate the page buttons to display - current page with 3 around it
const pageButtons = useMemo(() => { const pageButtons = useMemo(() => {
const buttons = []; const buttons = [];
const maxPageButtons = 3; const maxPageButtons = pageButtonsPerSide * 2 + 1;
let startPage = Math.max(currentPage - (Math.floor(maxPageButtons / 2)), 0); let startPage = Math.max(currentPage - Math.floor(maxPageButtons / 2), 0);
let endPage = Math.min(startPage + maxPageButtons - 1, pages - 1); const endPage = Math.min(startPage + maxPageButtons - 1, pages - 1);
if (endPage - startPage < maxPageButtons - 1) { if (endPage - startPage < maxPageButtons - 1) {
startPage = Math.max(endPage - maxPageButtons + 1, 0); startPage = Math.max(endPage - maxPageButtons + 1, 0);
} }
if (startPage > 0) { for (let i = startPage; i <= endPage; i++) {
buttons.push(0); buttons.push(i);
if (startPage > 1) { }
buttons.push('...');
}
}
for (let i = startPage; i <= endPage; i++) { return buttons;
buttons.push(i); }, [currentPage, pageButtonsPerSide, pages]);
}
if (endPage < pages - 1) { const isFirstEnabled = useMemo(() => currentPage > 0, [currentPage]);
if (endPage < pages - 2) { const isLastEnabled = useMemo(() => currentPage < pages - 1, [currentPage, pages]);
buttons.push('...');
}
buttons.push(pages - 1);
}
return buttons; const rangeDisplay = useMemo(() => {
}, [currentPage, pages]); const startItem = currentPage * (limit || 0) + 1;
const endItem = Math.min((currentPage + 1) * (limit || 0), total);
return `${startItem}-${endItem} of ${total}`;
}, [total, currentPage, limit]);
const isFirstEnabled = useMemo(() => currentPage > 0, [currentPage]); const api = useMemo(
const isLastEnabled = useMemo(() => currentPage < pages - 1, [currentPage, pages]); () => ({
count,
total,
currentPage,
pages,
isNextEnabled,
isPrevEnabled,
goNext,
goPrev,
goToPage,
goToFirst,
goToLast,
pageButtons,
isFirstEnabled,
isLastEnabled,
rangeDisplay,
}),
[
count,
total,
currentPage,
pages,
isNextEnabled,
isPrevEnabled,
goNext,
goPrev,
goToPage,
goToFirst,
goToLast,
pageButtons,
isFirstEnabled,
isLastEnabled,
rangeDisplay,
]
);
const rangeDisplay = useMemo(() => { return api;
const startItem = currentPage * (limit || 0) + 1;
const endItem = Math.min((currentPage + 1) * (limit || 0), total);
return `${startItem}-${endItem} of ${total}`;
}, [total, currentPage, limit]);
const api = useMemo(
() => ({
count,
total,
currentPage,
pages,
isNextEnabled,
isPrevEnabled,
goNext,
goPrev,
goToPage,
goToFirst,
goToLast,
pageButtons,
isFirstEnabled,
isLastEnabled,
rangeDisplay
}),
[
count,
total,
currentPage,
pages,
isNextEnabled,
isPrevEnabled,
goNext,
goPrev,
goToPage,
goToFirst,
goToLast,
pageButtons,
isFirstEnabled,
isLastEnabled,
rangeDisplay
]
);
return api;
}; };