Skip to content

Persist pagination state in url #2538

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 9, 2025
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