Skip to content
63 changes: 55 additions & 8 deletions frontend/src/components/ui/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,19 @@ import { classMap } from "lit/directives/class-map.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { when } from "lit/directives/when.js";

import { SearchParamsController } from "@/controllers/searchParams";
import { srOnly } from "@/utils/css";
import chevronLeft from "~assets/icons/chevron-left.svg";
import chevronRight from "~assets/icons/chevron-right.svg";

export const parsePage = (value: string | undefined | null) => {
const page = parseInt(value || "1");
if (!Number.isFinite(page)) {
throw new Error("couldn't parse page value from search");
}
return page;
};

type PageChangeDetail = {
page: number;
pages: number;
Expand All @@ -19,9 +28,19 @@ export type PageChangeEvent = CustomEvent<PageChangeDetail>;
/**
* Pagination
*
* Persists via a search param in the URL. Defaults to `page`, but can be set with the `name` attribute.
*
* Usage example:
* ```ts
* <btrix-pagination totalCount="11" @page-change=${this.console.log}>
* <btrix-pagination totalCount="11" @page-change=${console.log}>
* </btrix-pagination>
* ```
*
* You can have multiple paginations on one page by setting different names:
* ```ts
* <btrix-pagination name="page-a" totalCount="11" @page-change=${console.log}>
* </btrix-pagination>
* <btrix-pagination name="page-b" totalCount="2" @page-change=${console.log}>
* </btrix-pagination>
* ```
*
Expand Down Expand Up @@ -120,9 +139,17 @@ export class Pagination extends LitElement {
`,
];

@property({ type: Number })
searchParams = new SearchParamsController(this, (params) => {
const page = parsePage(params.get(this.name));
this.onPageChange(page);
});

@state()
page = 1;

@property({ type: String })
name = "page";

@property({ type: Number })
totalCount = 0;

Expand All @@ -148,6 +175,15 @@ export class Pagination extends LitElement {
this.calculatePages();
}

const parsedPage = parseFloat(
this.searchParams.searchParams.get(this.name) ?? "1",
);
if (parsedPage != this.page) {
const page = parsePage(this.searchParams.searchParams.get(this.name));
const constrainedPage = Math.max(1, Math.min(this.pages, page));
this.onPageChange(constrainedPage);
}

if (changedProperties.get("page") && this.page) {
this.inputValue = `${this.page}`;
}
Expand Down Expand Up @@ -310,12 +346,23 @@ export class Pagination extends LitElement {
}

private onPageChange(page: number) {
this.dispatchEvent(
new CustomEvent<PageChangeDetail>("page-change", {
detail: { page: page, pages: this.pages },
composed: true,
}),
);
if (this.page !== page) {
this.searchParams.set((params) => {
if (page === 1) {
params.delete(this.name);
} else {
params.set(this.name, page.toString());
}
return params;
});
this.dispatchEvent(
new CustomEvent<PageChangeDetail>("page-change", {
detail: { page: page, pages: this.pages },
composed: true,
}),
);
}
this.page = page;
}

private calculatePages() {
Expand Down
57 changes: 57 additions & 0 deletions frontend/src/controllers/searchParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { ReactiveController, ReactiveControllerHost } from "lit";

export class SearchParamsController implements ReactiveController {
private readonly host: ReactiveControllerHost;
private readonly changeHandler?: (
searchParams: URLSearchParams,
prevParams: URLSearchParams,
) => void;
private prevParams = new URLSearchParams(location.search);

public get searchParams() {
return new URLSearchParams(location.search);
}

public set(
update: URLSearchParams | ((prev: URLSearchParams) => URLSearchParams),
options: { replace?: boolean; data?: unknown } = { replace: false },
) {
this.prevParams = new URLSearchParams(this.searchParams);
const url = new URL(location.toString());
url.search =
typeof update === "function"
? update(this.searchParams).toString()
: update.toString();

if (options.replace) {
history.replaceState(options.data, "", url);
} else {
history.pushState(options.data, "", url);
}
}

constructor(
host: ReactiveControllerHost,
onChange?: (
searchParams: URLSearchParams,
prevParams: URLSearchParams,
) => void,
) {
this.host = host;
host.addController(this);
this.changeHandler = onChange;
}

hostConnected(): void {
window.addEventListener("popstate", this.onPopState);
}

hostDisconnected(): void {
window.removeEventListener("popstate", this.onPopState);
}

private readonly onPopState = (_e: PopStateEvent) => {
this.changeHandler?.(this.searchParams, this.prevParams);
this.prevParams = new URLSearchParams(this.searchParams);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { html } from "lit";
import { customElement, property, state } from "lit/decorators.js";

import { BtrixElement } from "@/classes/BtrixElement";
import { type PageChangeEvent } from "@/components/ui/pagination";
import { parsePage, type PageChangeEvent } from "@/components/ui/pagination";

type URLs = string[];

Expand All @@ -24,7 +24,7 @@ export class CrawlPendingExclusions extends BtrixElement {
matchedURLs: URLs | null = null;

@state()
private page = 1;
private page = parsePage(new URLSearchParams(location.search).get("page"));

private get pageSize() {
return 10;
Expand Down
16 changes: 11 additions & 5 deletions frontend/src/features/collections/collection-items-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type {

import { BtrixElement } from "@/classes/BtrixElement";
import type { Dialog } from "@/components/ui/dialog";
import type { PageChangeEvent } from "@/components/ui/pagination";
import { parsePage, type PageChangeEvent } from "@/components/ui/pagination";
import { type CheckboxChangeEventDetail } from "@/features/archived-items/archived-item-list";
import type {
FilterBy,
Expand Down Expand Up @@ -694,18 +694,24 @@ export class CollectionItemsDialog extends BtrixElement {
}

private async initSelection() {
void this.fetchCrawls({ page: 1, pageSize: DEFAULT_PAGE_SIZE });
void this.fetchUploads({ page: 1, pageSize: DEFAULT_PAGE_SIZE });
void this.fetchCrawls({
page: parsePage(new URLSearchParams(location.search).get("page")),
pageSize: DEFAULT_PAGE_SIZE,
});
void this.fetchUploads({
page: parsePage(new URLSearchParams(location.search).get("page")),
pageSize: DEFAULT_PAGE_SIZE,
});
void this.fetchSearchValues();

const [crawls, uploads] = await Promise.all([
this.getCrawls({
page: 1,
page: parsePage(new URLSearchParams(location.search).get("page")),
pageSize: COLLECTION_ITEMS_MAX,
collectionId: this.collectionId,
}).then(({ items }) => items),
this.getUploads({
page: 1,
page: parsePage(new URLSearchParams(location.search).get("page")),
pageSize: COLLECTION_ITEMS_MAX,
collectionId: this.collectionId,
}).then(({ items }) => items),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import RegexColorize from "regex-colorize";
import type { Exclusion } from "./queue-exclusion-form";

import { TailwindElement } from "@/classes/TailwindElement";
import { type PageChangeEvent } from "@/components/ui/pagination";
import { parsePage, type PageChangeEvent } from "@/components/ui/pagination";
import type { SeedConfig } from "@/pages/org/types";
import { regexEscape, regexUnescape } from "@/utils/string";
import { tw } from "@/utils/tailwind";
Expand Down Expand Up @@ -90,7 +90,7 @@ export class QueueExclusionTable extends TailwindElement {
private results: Exclusion[] = [];

@state()
private page = 1;
private page = parsePage(new URLSearchParams(location.search).get("page"));

@state()
private exclusionToRemove?: string;
Expand Down
1 change: 0 additions & 1 deletion frontend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ export class App extends BtrixElement {
willUpdate(changedProperties: Map<string, unknown>) {
if (changedProperties.has("settings")) {
AppStateService.updateSettings(this.settings || null);

}
if (changedProperties.has("viewState")) {
this.handleViewStateChange(
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/pages/crawls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { when } from "lit/directives/when.js";
import queryString from "query-string";

import { BtrixElement } from "@/classes/BtrixElement";
import type { PageChangeEvent } from "@/components/ui/pagination";
import { parsePage, type PageChangeEvent } from "@/components/ui/pagination";
import needLogin from "@/decorators/needLogin";
import { CrawlStatus } from "@/features/archived-items/crawl-status";
import type { APIPaginatedList, APIPaginationQuery } from "@/types/api";
Expand Down Expand Up @@ -354,7 +354,10 @@ export class Crawls extends BtrixElement {
{
...this.filterBy,
...queryParams,
page: queryParams?.page || this.crawls?.page || 1,
page:
queryParams?.page ||
this.crawls?.page ||
parsePage(new URLSearchParams(location.search).get("page")),
pageSize: queryParams?.pageSize || this.crawls?.pageSize || 100,
sortBy: this.orderBy.field,
sortDirection: this.orderBy.direction === "desc" ? -1 : 1,
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/pages/org/archived-item-detail/ui/qa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import queryString from "query-string";

import { BtrixElement } from "@/classes/BtrixElement";
import { type Dialog } from "@/components/ui/dialog";
import type { PageChangeEvent } from "@/components/ui/pagination";
import { parsePage, type PageChangeEvent } from "@/components/ui/pagination";
import { ClipboardController } from "@/controllers/clipboard";
import { iconFor as iconForPageReview } from "@/features/qa/page-list/helpers";
import * as pageApproval from "@/features/qa/page-list/helpers/approval";
Expand Down Expand Up @@ -892,7 +892,10 @@ export class ArchivedItemDetailQA extends BtrixElement {
}

this.pages = await this.getPages({
page: params?.page ?? this.pages?.page ?? 1,
page:
params?.page ??
this.pages?.page ??
parsePage(new URLSearchParams(location.search).get("page")),
pageSize: params?.pageSize ?? this.pages?.pageSize ?? 10,
sortBy,
sortDirection,
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/org/archived-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import queryString from "query-string";
import type { ArchivedItem, Crawl, Workflow } from "./types";

import { BtrixElement } from "@/classes/BtrixElement";
import type { PageChangeEvent } from "@/components/ui/pagination";
import { parsePage, type PageChangeEvent } from "@/components/ui/pagination";
import { ClipboardController } from "@/controllers/clipboard";
import { CrawlStatus } from "@/features/archived-items/crawl-status";
import { pageHeader } from "@/layouts/pageHeader";
Expand Down Expand Up @@ -92,7 +92,7 @@ export class CrawlsList extends BtrixElement {

@state()
private pagination: Required<APIPaginationQuery> = {
page: 1,
page: parsePage(new URLSearchParams(location.search).get("page")),
pageSize: INITIAL_PAGE_SIZE,
};

Expand Down
7 changes: 5 additions & 2 deletions frontend/src/pages/org/browser-profiles-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { Profile } from "./types";
import type { SelectNewDialogEvent } from ".";

import { BtrixElement } from "@/classes/BtrixElement";
import type { PageChangeEvent } from "@/components/ui/pagination";
import { parsePage, type PageChangeEvent } from "@/components/ui/pagination";
import {
SortDirection,
type SortValues,
Expand Down Expand Up @@ -441,7 +441,10 @@ export class BrowserProfilesList extends BtrixElement {
try {
this.isLoading = true;
const data = await this.getProfiles({
page: params?.page || this.browserProfiles?.page || 1,
page:
params?.page ||
this.browserProfiles?.page ||
parsePage(new URLSearchParams(location.search).get("page")),
pageSize:
params?.pageSize ||
this.browserProfiles?.pageSize ||
Expand Down
11 changes: 8 additions & 3 deletions frontend/src/pages/org/collection-detail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { Embed as ReplayWebPage } from "replaywebpage";

import { BtrixElement } from "@/classes/BtrixElement";
import type { MarkdownEditor } from "@/components/ui/markdown-editor";
import type { PageChangeEvent } from "@/components/ui/pagination";
import { parsePage, type PageChangeEvent } from "@/components/ui/pagination";
import { viewStateContext, type ViewStateContext } from "@/context/view-state";
import { ClipboardController } from "@/controllers/clipboard";
import type { EditDialogTab } from "@/features/collections/collection-edit-dialog";
Expand Down Expand Up @@ -129,7 +129,9 @@ export class CollectionDetail extends BtrixElement {
) {
if (changedProperties.has("collectionId")) {
void this.fetchCollection();
void this.fetchArchivedItems({ page: 1 });
void this.fetchArchivedItems({
page: parsePage(new URLSearchParams(location.search).get("page")),
});
}
if (changedProperties.has("collectionTab") && this.collectionTab === null) {
this.collectionTab = Tab.Replay;
Expand Down Expand Up @@ -1033,7 +1035,10 @@ export class CollectionDetail extends BtrixElement {
const query = queryString.stringify(
{
...params,
page: params?.page || this.archivedItems?.page || 1,
page:
params?.page ||
this.archivedItems?.page ||
parsePage(new URLSearchParams(location.search).get("page")),
pageSize:
params?.pageSize ||
this.archivedItems?.pageSize ||
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/pages/org/collections-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import queryString from "query-string";
import type { SelectNewDialogEvent } from ".";

import { BtrixElement } from "@/classes/BtrixElement";
import type { PageChangeEvent } from "@/components/ui/pagination";
import { parsePage, type PageChangeEvent } from "@/components/ui/pagination";
import { ClipboardController } from "@/controllers/clipboard";
import type { CollectionSavedEvent } from "@/features/collections/collection-create-dialog";
import { SelectCollectionAccess } from "@/features/collections/select-collection-access";
Expand Down Expand Up @@ -757,7 +757,10 @@ export class CollectionsList extends BtrixElement {
const query = queryString.stringify(
{
...this.filterBy,
page: queryParams?.page || this.collections?.page || 1,
page:
queryParams?.page ||
this.collections?.page ||
parsePage(new URLSearchParams(location.search).get("page")),
pageSize:
queryParams?.pageSize ||
this.collections?.pageSize ||
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/org/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import queryString from "query-string";
import type { SelectNewDialogEvent } from ".";

import { BtrixElement } from "@/classes/BtrixElement";
import { type PageChangeEvent } from "@/components/ui/pagination";
import { parsePage, type PageChangeEvent } from "@/components/ui/pagination";
import { type CollectionSavedEvent } from "@/features/collections/collection-edit-dialog";
import { pageHeading } from "@/layouts/page";
import { pageHeader } from "@/layouts/pageHeader";
Expand Down Expand Up @@ -70,7 +70,7 @@ export class Dashboard extends BtrixElement {
collectionsView = CollectionGridView.Public;

@state()
collectionPage = 1;
collectionPage = parsePage(new URLSearchParams(location.search).get("page"));

// Used for busting cache when updating visible collection
cacheBust = 0;
Expand Down
Loading
Loading