mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Move pagination to a sticky bottom toolbar on scenes page (#5924)
* Adjust main padding to be the same as navbar height * Add LoadedContent component for loading and error display * Add option for pagination popup placement * Show results summary at top only. Add sticky bottom pagination
This commit is contained in:
parent
a145576f39
commit
7d692232ed
5 changed files with 94 additions and 34 deletions
|
|
@ -1,11 +1,37 @@
|
|||
import React, { PropsWithChildren, useMemo } from "react";
|
||||
import { QueryResult } from "@apollo/client";
|
||||
import { ApolloError, QueryResult } from "@apollo/client";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { Pagination, PaginationIndex } from "./Pagination";
|
||||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||
import { ErrorMessage } from "../Shared/ErrorMessage";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
export const LoadedContent: React.FC<
|
||||
PropsWithChildren<{
|
||||
loading?: boolean;
|
||||
error?: ApolloError;
|
||||
}>
|
||||
> = ({ loading, error, children }) => {
|
||||
if (loading) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorMessage
|
||||
message={
|
||||
<FormattedMessage
|
||||
id="errors.loading_type"
|
||||
values={{ type: "items" }}
|
||||
/>
|
||||
}
|
||||
error={error.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export const PagedList: React.FC<
|
||||
PropsWithChildren<{
|
||||
result: QueryResult;
|
||||
|
|
@ -63,25 +89,8 @@ export const PagedList: React.FC<
|
|||
]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (result.loading) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
if (result.error) {
|
||||
return (
|
||||
<ErrorMessage
|
||||
message={
|
||||
<FormattedMessage
|
||||
id="errors.loading_type"
|
||||
values={{ type: "items" }}
|
||||
/>
|
||||
}
|
||||
error={result.error.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadedContent loading={result.loading} error={result.error}>
|
||||
{children}
|
||||
{!!pages && (
|
||||
<>
|
||||
|
|
@ -89,7 +98,7 @@ export const PagedList: React.FC<
|
|||
{pagination}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</LoadedContent>
|
||||
);
|
||||
}, [
|
||||
result.loading,
|
||||
|
|
|
|||
|
|
@ -13,12 +13,19 @@ import useFocus from "src/utils/focus";
|
|||
import { Icon } from "../Shared/Icon";
|
||||
import { faCheck, faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useStopWheelScroll } from "src/utils/form";
|
||||
import { Placement } from "react-bootstrap/esm/Overlay";
|
||||
|
||||
const PageCount: React.FC<{
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
onChangePage: (page: number) => void;
|
||||
}> = ({ totalPages, currentPage, onChangePage }) => {
|
||||
pagePopupPlacement?: Placement;
|
||||
}> = ({
|
||||
totalPages,
|
||||
currentPage,
|
||||
onChangePage,
|
||||
pagePopupPlacement = "bottom",
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const currentPageCtrl = useRef(null);
|
||||
const [pageInput, pageFocus] = useFocus();
|
||||
|
|
@ -94,7 +101,7 @@ const PageCount: React.FC<{
|
|||
<Overlay
|
||||
target={currentPageCtrl.current}
|
||||
show={showSelectPage}
|
||||
placement="bottom"
|
||||
placement={pagePopupPlacement}
|
||||
rootClose
|
||||
onHide={() => setShowSelectPage(false)}
|
||||
>
|
||||
|
|
@ -138,9 +145,11 @@ interface IPaginationProps {
|
|||
totalItems: number;
|
||||
metadataByline?: React.ReactNode;
|
||||
onChangePage: (page: number) => void;
|
||||
pagePopupPlacement?: Placement;
|
||||
}
|
||||
|
||||
interface IPaginationIndexProps {
|
||||
loading?: boolean;
|
||||
itemsPerPage: number;
|
||||
currentPage: number;
|
||||
totalItems: number;
|
||||
|
|
@ -154,6 +163,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
|||
currentPage,
|
||||
totalItems,
|
||||
onChangePage,
|
||||
pagePopupPlacement,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const totalPages = useMemo(
|
||||
|
|
@ -168,6 +178,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
|||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
onChangePage={onChangePage}
|
||||
pagePopupPlacement={pagePopupPlacement}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -183,7 +194,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
|||
<FormattedNumber value={page} />
|
||||
</Button>
|
||||
));
|
||||
}, [totalPages, currentPage, onChangePage]);
|
||||
}, [totalPages, currentPage, onChangePage, pagePopupPlacement]);
|
||||
|
||||
if (totalPages <= 1) return <div />;
|
||||
|
||||
|
|
@ -227,6 +238,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
|||
};
|
||||
|
||||
export const PaginationIndex: React.FC<IPaginationIndexProps> = ({
|
||||
loading,
|
||||
itemsPerPage,
|
||||
currentPage,
|
||||
totalItems,
|
||||
|
|
@ -234,6 +246,8 @@ export const PaginationIndex: React.FC<IPaginationIndexProps> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
// Build the pagination index string
|
||||
const firstItemCount: number = Math.min(
|
||||
(currentPage - 1) * itemsPerPage + 1,
|
||||
|
|
|
|||
|
|
@ -942,3 +942,19 @@ input[type="range"].zoom-slider {
|
|||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-footer {
|
||||
background-color: $body-bg;
|
||||
bottom: $navbar-height;
|
||||
padding: 0.5rem 1rem;
|
||||
position: sticky;
|
||||
z-index: 10;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { objectTitle } from "src/core/files";
|
|||
import TextUtils from "src/utils/text";
|
||||
import { View } from "../List/views";
|
||||
import { FileSize } from "../Shared/FileSize";
|
||||
import { PagedList } from "../List/PagedList";
|
||||
import { LoadedContent } from "../List/PagedList";
|
||||
import { useCloseEditDelete, useFilterOperations } from "../List/util";
|
||||
import { IListFilterOperation } from "../List/ListOperationButtons";
|
||||
import { FilteredListToolbar } from "../List/FilteredListToolbar";
|
||||
|
|
@ -48,6 +48,7 @@ import {
|
|||
useFilteredSidebarKeybinds,
|
||||
} from "../List/Filters/FilterSidebar";
|
||||
import { PatchContainerComponent } from "src/patch";
|
||||
import { Pagination, PaginationIndex } from "../List/Pagination";
|
||||
|
||||
function renderMetadataByline(result: GQL.FindScenesQueryResult) {
|
||||
const duration = result?.data?.findScenes?.duration;
|
||||
|
|
@ -488,14 +489,15 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
|||
onRemoveAll={() => clearAllCriteria()}
|
||||
/>
|
||||
|
||||
<PagedList
|
||||
result={result}
|
||||
cachedResult={cachedResult}
|
||||
filter={filter}
|
||||
totalCount={totalCount}
|
||||
onChangePage={setPage}
|
||||
<PaginationIndex
|
||||
loading={cachedResult.loading}
|
||||
itemsPerPage={filter.itemsPerPage}
|
||||
currentPage={filter.currentPage}
|
||||
totalItems={totalCount}
|
||||
metadataByline={metadataByline}
|
||||
>
|
||||
/>
|
||||
|
||||
<LoadedContent loading={result.loading} error={result.error}>
|
||||
<SceneList
|
||||
filter={effectiveFilter}
|
||||
scenes={items}
|
||||
|
|
@ -503,7 +505,20 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
|||
onSelectChange={onSelectChange}
|
||||
fromGroupId={fromGroupId}
|
||||
/>
|
||||
</PagedList>
|
||||
</LoadedContent>
|
||||
|
||||
{totalCount > filter.itemsPerPage && (
|
||||
<div className="pagination-footer">
|
||||
<Pagination
|
||||
itemsPerPage={filter.itemsPerPage}
|
||||
currentPage={filter.currentPage}
|
||||
totalItems={totalCount}
|
||||
metadataByline={metadataByline}
|
||||
onChangePage={setPage}
|
||||
pagePopupPlacement="top"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SidebarPane>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -51,15 +51,21 @@ body {
|
|||
-webkit-font-smoothing: antialiased;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
padding: 4rem 0 0 0;
|
||||
padding: $navbar-height 0 0 0;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
@media (orientation: portrait) {
|
||||
padding: 1rem 0 5rem;
|
||||
padding: 1rem 0 $navbar-height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
@include media-breakpoint-up(sm) {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
#group-page,
|
||||
#performer-page,
|
||||
#studio-page,
|
||||
|
|
|
|||
Loading…
Reference in a new issue