mirror of
https://github.com/stashapp/stash.git
synced 2025-12-09 18:04:33 +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 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 { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { Pagination, PaginationIndex } from "./Pagination";
|
import { Pagination, PaginationIndex } from "./Pagination";
|
||||||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||||
import { ErrorMessage } from "../Shared/ErrorMessage";
|
import { ErrorMessage } from "../Shared/ErrorMessage";
|
||||||
import { FormattedMessage } from "react-intl";
|
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<
|
export const PagedList: React.FC<
|
||||||
PropsWithChildren<{
|
PropsWithChildren<{
|
||||||
result: QueryResult;
|
result: QueryResult;
|
||||||
|
|
@ -63,25 +89,8 @@ export const PagedList: React.FC<
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const content = useMemo(() => {
|
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 (
|
return (
|
||||||
<>
|
<LoadedContent loading={result.loading} error={result.error}>
|
||||||
{children}
|
{children}
|
||||||
{!!pages && (
|
{!!pages && (
|
||||||
<>
|
<>
|
||||||
|
|
@ -89,7 +98,7 @@ export const PagedList: React.FC<
|
||||||
{pagination}
|
{pagination}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</LoadedContent>
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
result.loading,
|
result.loading,
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,19 @@ import useFocus from "src/utils/focus";
|
||||||
import { Icon } from "../Shared/Icon";
|
import { Icon } from "../Shared/Icon";
|
||||||
import { faCheck, faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
import { faCheck, faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { useStopWheelScroll } from "src/utils/form";
|
import { useStopWheelScroll } from "src/utils/form";
|
||||||
|
import { Placement } from "react-bootstrap/esm/Overlay";
|
||||||
|
|
||||||
const PageCount: React.FC<{
|
const PageCount: React.FC<{
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
onChangePage: (page: number) => void;
|
onChangePage: (page: number) => void;
|
||||||
}> = ({ totalPages, currentPage, onChangePage }) => {
|
pagePopupPlacement?: Placement;
|
||||||
|
}> = ({
|
||||||
|
totalPages,
|
||||||
|
currentPage,
|
||||||
|
onChangePage,
|
||||||
|
pagePopupPlacement = "bottom",
|
||||||
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const currentPageCtrl = useRef(null);
|
const currentPageCtrl = useRef(null);
|
||||||
const [pageInput, pageFocus] = useFocus();
|
const [pageInput, pageFocus] = useFocus();
|
||||||
|
|
@ -94,7 +101,7 @@ const PageCount: React.FC<{
|
||||||
<Overlay
|
<Overlay
|
||||||
target={currentPageCtrl.current}
|
target={currentPageCtrl.current}
|
||||||
show={showSelectPage}
|
show={showSelectPage}
|
||||||
placement="bottom"
|
placement={pagePopupPlacement}
|
||||||
rootClose
|
rootClose
|
||||||
onHide={() => setShowSelectPage(false)}
|
onHide={() => setShowSelectPage(false)}
|
||||||
>
|
>
|
||||||
|
|
@ -138,9 +145,11 @@ interface IPaginationProps {
|
||||||
totalItems: number;
|
totalItems: number;
|
||||||
metadataByline?: React.ReactNode;
|
metadataByline?: React.ReactNode;
|
||||||
onChangePage: (page: number) => void;
|
onChangePage: (page: number) => void;
|
||||||
|
pagePopupPlacement?: Placement;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPaginationIndexProps {
|
interface IPaginationIndexProps {
|
||||||
|
loading?: boolean;
|
||||||
itemsPerPage: number;
|
itemsPerPage: number;
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
totalItems: number;
|
totalItems: number;
|
||||||
|
|
@ -154,6 +163,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
||||||
currentPage,
|
currentPage,
|
||||||
totalItems,
|
totalItems,
|
||||||
onChangePage,
|
onChangePage,
|
||||||
|
pagePopupPlacement,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const totalPages = useMemo(
|
const totalPages = useMemo(
|
||||||
|
|
@ -168,6 +178,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
onChangePage={onChangePage}
|
onChangePage={onChangePage}
|
||||||
|
pagePopupPlacement={pagePopupPlacement}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -183,7 +194,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
||||||
<FormattedNumber value={page} />
|
<FormattedNumber value={page} />
|
||||||
</Button>
|
</Button>
|
||||||
));
|
));
|
||||||
}, [totalPages, currentPage, onChangePage]);
|
}, [totalPages, currentPage, onChangePage, pagePopupPlacement]);
|
||||||
|
|
||||||
if (totalPages <= 1) return <div />;
|
if (totalPages <= 1) return <div />;
|
||||||
|
|
||||||
|
|
@ -227,6 +238,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PaginationIndex: React.FC<IPaginationIndexProps> = ({
|
export const PaginationIndex: React.FC<IPaginationIndexProps> = ({
|
||||||
|
loading,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
currentPage,
|
currentPage,
|
||||||
totalItems,
|
totalItems,
|
||||||
|
|
@ -234,6 +246,8 @@ export const PaginationIndex: React.FC<IPaginationIndexProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
if (loading) return null;
|
||||||
|
|
||||||
// Build the pagination index string
|
// Build the pagination index string
|
||||||
const firstItemCount: number = Math.min(
|
const firstItemCount: number = Math.min(
|
||||||
(currentPage - 1) * itemsPerPage + 1,
|
(currentPage - 1) * itemsPerPage + 1,
|
||||||
|
|
|
||||||
|
|
@ -942,3 +942,19 @@ input[type="range"].zoom-slider {
|
||||||
margin-right: 0.5rem;
|
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 TextUtils from "src/utils/text";
|
||||||
import { View } from "../List/views";
|
import { View } from "../List/views";
|
||||||
import { FileSize } from "../Shared/FileSize";
|
import { FileSize } from "../Shared/FileSize";
|
||||||
import { PagedList } from "../List/PagedList";
|
import { LoadedContent } from "../List/PagedList";
|
||||||
import { useCloseEditDelete, useFilterOperations } from "../List/util";
|
import { useCloseEditDelete, useFilterOperations } from "../List/util";
|
||||||
import { IListFilterOperation } from "../List/ListOperationButtons";
|
import { IListFilterOperation } from "../List/ListOperationButtons";
|
||||||
import { FilteredListToolbar } from "../List/FilteredListToolbar";
|
import { FilteredListToolbar } from "../List/FilteredListToolbar";
|
||||||
|
|
@ -48,6 +48,7 @@ import {
|
||||||
useFilteredSidebarKeybinds,
|
useFilteredSidebarKeybinds,
|
||||||
} from "../List/Filters/FilterSidebar";
|
} from "../List/Filters/FilterSidebar";
|
||||||
import { PatchContainerComponent } from "src/patch";
|
import { PatchContainerComponent } from "src/patch";
|
||||||
|
import { Pagination, PaginationIndex } from "../List/Pagination";
|
||||||
|
|
||||||
function renderMetadataByline(result: GQL.FindScenesQueryResult) {
|
function renderMetadataByline(result: GQL.FindScenesQueryResult) {
|
||||||
const duration = result?.data?.findScenes?.duration;
|
const duration = result?.data?.findScenes?.duration;
|
||||||
|
|
@ -488,14 +489,15 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
||||||
onRemoveAll={() => clearAllCriteria()}
|
onRemoveAll={() => clearAllCriteria()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PagedList
|
<PaginationIndex
|
||||||
result={result}
|
loading={cachedResult.loading}
|
||||||
cachedResult={cachedResult}
|
itemsPerPage={filter.itemsPerPage}
|
||||||
filter={filter}
|
currentPage={filter.currentPage}
|
||||||
totalCount={totalCount}
|
totalItems={totalCount}
|
||||||
onChangePage={setPage}
|
|
||||||
metadataByline={metadataByline}
|
metadataByline={metadataByline}
|
||||||
>
|
/>
|
||||||
|
|
||||||
|
<LoadedContent loading={result.loading} error={result.error}>
|
||||||
<SceneList
|
<SceneList
|
||||||
filter={effectiveFilter}
|
filter={effectiveFilter}
|
||||||
scenes={items}
|
scenes={items}
|
||||||
|
|
@ -503,7 +505,20 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
||||||
onSelectChange={onSelectChange}
|
onSelectChange={onSelectChange}
|
||||||
fromGroupId={fromGroupId}
|
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>
|
</div>
|
||||||
</SidebarPane>
|
</SidebarPane>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -51,15 +51,21 @@ body {
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding: 4rem 0 0 0;
|
padding: $navbar-height 0 0 0;
|
||||||
|
|
||||||
@include media-breakpoint-down(xs) {
|
@include media-breakpoint-down(xs) {
|
||||||
@media (orientation: portrait) {
|
@media (orientation: portrait) {
|
||||||
padding: 1rem 0 5rem;
|
padding: 1rem 0 $navbar-height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
@include media-breakpoint-up(sm) {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#group-page,
|
#group-page,
|
||||||
#performer-page,
|
#performer-page,
|
||||||
#studio-page,
|
#studio-page,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue