mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
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:
parent
1f22f6ae02
commit
689dc30f87
@ -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>
|
||||||
|
@ -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;
|
|
||||||
};
|
};
|
Loading…
Reference in New Issue
Block a user