Commit 07d53241 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #844 from blockscout/pagination/next-page-query-param

store all api pagination param in one query param
parents 21bb0fc5 d7b82bbe
...@@ -18,7 +18,8 @@ export default function buildUrl<R extends ResourceName>( ...@@ -18,7 +18,8 @@ export default function buildUrl<R extends ResourceName>(
const url = new URL(compile(path)(pathParams), baseUrl); const url = new URL(compile(path)(pathParams), baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => { queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
value && url.searchParams.append(key, String(value)); // there are some pagination params that can be null for the next page
(value || value === null) && url.searchParams.append(key, String(value));
}); });
return url.toString(); return url.toString();
......
...@@ -160,7 +160,6 @@ export const RESOURCES = { ...@@ -160,7 +160,6 @@ export const RESOURCES = {
// BLOCKS, TXS // BLOCKS, TXS
blocks: { blocks: {
path: '/api/v2/blocks', path: '/api/v2/blocks',
paginationFields: [ 'block_number' as const, 'items_count' as const ],
filterFields: [ 'type' as const ], filterFields: [ 'type' as const ],
}, },
block: { block: {
...@@ -170,28 +169,23 @@ export const RESOURCES = { ...@@ -170,28 +169,23 @@ export const RESOURCES = {
block_txs: { block_txs: {
path: '/api/v2/blocks/:height_or_hash/transactions', path: '/api/v2/blocks/:height_or_hash/transactions',
pathParams: [ 'height_or_hash' as const ], pathParams: [ 'height_or_hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [], filterFields: [],
}, },
block_withdrawals: { block_withdrawals: {
path: '/api/v2/blocks/:height_or_hash/withdrawals', path: '/api/v2/blocks/:height_or_hash/withdrawals',
pathParams: [ 'height_or_hash' as const ], pathParams: [ 'height_or_hash' as const ],
paginationFields: [ 'items_count' as const, 'index' as const ],
filterFields: [], filterFields: [],
}, },
txs_validated: { txs_validated: {
path: '/api/v2/transactions', path: '/api/v2/transactions',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'filter' as const, 'index' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'method' as const ], filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
}, },
txs_pending: { txs_pending: {
path: '/api/v2/transactions', path: '/api/v2/transactions',
paginationFields: [ 'filter' as const, 'hash' as const, 'inserted_at' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'method' as const ], filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
}, },
txs_watchlist: { txs_watchlist: {
path: '/api/v2/transactions/watchlist', path: '/api/v2/transactions/watchlist',
paginationFields: [ 'block_number' as const, 'index' as const, 'items_count' as const ],
filterFields: [ ], filterFields: [ ],
}, },
tx: { tx: {
...@@ -201,19 +195,16 @@ export const RESOURCES = { ...@@ -201,19 +195,16 @@ export const RESOURCES = {
tx_internal_txs: { tx_internal_txs: {
path: '/api/v2/transactions/:hash/internal-transactions', path: '/api/v2/transactions/:hash/internal-transactions',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'transaction_hash' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ ], filterFields: [ ],
}, },
tx_logs: { tx_logs: {
path: '/api/v2/transactions/:hash/logs', path: '/api/v2/transactions/:hash/logs',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'transaction_hash' as const, 'index' as const ],
filterFields: [ ], filterFields: [ ],
}, },
tx_token_transfers: { tx_token_transfers: {
path: '/api/v2/transactions/:hash/token-transfers', path: '/api/v2/transactions/:hash/token-transfers',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'transaction_hash' as const, 'index' as const ],
filterFields: [ 'type' as const ], filterFields: [ 'type' as const ],
}, },
tx_raw_trace: { tx_raw_trace: {
...@@ -226,7 +217,6 @@ export const RESOURCES = { ...@@ -226,7 +217,6 @@ export const RESOURCES = {
}, },
withdrawals: { withdrawals: {
path: '/api/v2/withdrawals', path: '/api/v2/withdrawals',
paginationFields: [ 'index' as const, 'items_count' as const ],
filterFields: [], filterFields: [],
}, },
withdrawals_counters: { withdrawals_counters: {
...@@ -236,7 +226,6 @@ export const RESOURCES = { ...@@ -236,7 +226,6 @@ export const RESOURCES = {
// ADDRESSES // ADDRESSES
addresses: { addresses: {
path: '/api/v2/addresses/', path: '/api/v2/addresses/',
paginationFields: [ 'fetched_coin_balance' as const, 'hash' as const, 'items_count' as const ],
filterFields: [ ], filterFields: [ ],
}, },
...@@ -256,31 +245,26 @@ export const RESOURCES = { ...@@ -256,31 +245,26 @@ export const RESOURCES = {
address_txs: { address_txs: {
path: '/api/v2/addresses/:hash/transactions', path: '/api/v2/addresses/:hash/transactions',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [ 'filter' as const ], filterFields: [ 'filter' as const ],
}, },
address_internal_txs: { address_internal_txs: {
path: '/api/v2/addresses/:hash/internal-transactions', path: '/api/v2/addresses/:hash/internal-transactions',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ 'filter' as const ], filterFields: [ 'filter' as const ],
}, },
address_token_transfers: { address_token_transfers: {
path: '/api/v2/addresses/:hash/token-transfers', path: '/api/v2/addresses/:hash/token-transfers',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'token' as const ], filterFields: [ 'filter' as const, 'type' as const, 'token' as const ],
}, },
address_blocks_validated: { address_blocks_validated: {
path: '/api/v2/addresses/:hash/blocks-validated', path: '/api/v2/addresses/:hash/blocks-validated',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'block_number' as const ],
filterFields: [ ], filterFields: [ ],
}, },
address_coin_balance: { address_coin_balance: {
path: '/api/v2/addresses/:hash/coin-balance-history', path: '/api/v2/addresses/:hash/coin-balance-history',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'block_number' as const ],
filterFields: [ ], filterFields: [ ],
}, },
address_coin_balance_chart: { address_coin_balance_chart: {
...@@ -290,19 +274,16 @@ export const RESOURCES = { ...@@ -290,19 +274,16 @@ export const RESOURCES = {
address_logs: { address_logs: {
path: '/api/v2/addresses/:hash/logs', path: '/api/v2/addresses/:hash/logs',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'transaction_index' as const, 'index' as const, 'block_number' as const ],
filterFields: [ ], filterFields: [ ],
}, },
address_tokens: { address_tokens: {
path: '/api/v2/addresses/:hash/tokens', path: '/api/v2/addresses/:hash/tokens',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'token_name' as const, 'token_type' as const, 'value' as const, 'fiat_value' as const, 'id' as const ],
filterFields: [ 'type' as const ], filterFields: [ 'type' as const ],
}, },
address_withdrawals: { address_withdrawals: {
path: '/api/v2/addresses/:hash/withdrawals', path: '/api/v2/addresses/:hash/withdrawals',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'index' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -341,7 +322,6 @@ export const RESOURCES = { ...@@ -341,7 +322,6 @@ export const RESOURCES = {
verified_contracts: { verified_contracts: {
path: '/api/v2/smart-contracts', path: '/api/v2/smart-contracts',
paginationFields: [ 'items_count' as const, 'smart_contract_id' as const ],
filterFields: [ 'q' as const, 'filter' as const ], filterFields: [ 'q' as const, 'filter' as const ],
}, },
verified_contracts_counters: { verified_contracts_counters: {
...@@ -366,24 +346,20 @@ export const RESOURCES = { ...@@ -366,24 +346,20 @@ export const RESOURCES = {
token_holders: { token_holders: {
path: '/api/v2/tokens/:hash/holders', path: '/api/v2/tokens/:hash/holders',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'value' as const ],
filterFields: [], filterFields: [],
}, },
token_transfers: { token_transfers: {
path: '/api/v2/tokens/:hash/transfers', path: '/api/v2/tokens/:hash/transfers',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [], filterFields: [],
}, },
token_inventory: { token_inventory: {
path: '/api/v2/tokens/:hash/instances', path: '/api/v2/tokens/:hash/instances',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'unique_token' as const ],
filterFields: [], filterFields: [],
}, },
tokens: { tokens: {
path: '/api/v2/tokens', path: '/api/v2/tokens',
paginationFields: [ 'holder_count' as const, 'items_count' as const, 'name' as const, 'market_cap' as const ],
filterFields: [ 'q' as const, 'type' as const ], filterFields: [ 'q' as const, 'type' as const ],
}, },
...@@ -399,13 +375,11 @@ export const RESOURCES = { ...@@ -399,13 +375,11 @@ export const RESOURCES = {
token_instance_transfers: { token_instance_transfers: {
path: '/api/v2/tokens/:hash/instances/:id/transfers', path: '/api/v2/tokens/:hash/instances/:id/transfers',
pathParams: [ 'hash' as const, 'id' as const ], pathParams: [ 'hash' as const, 'id' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'token_id' as const ],
filterFields: [], filterFields: [],
}, },
token_instance_holders: { token_instance_holders: {
path: '/api/v2/tokens/:hash/instances/:id/holders', path: '/api/v2/tokens/:hash/instances/:id/holders',
pathParams: [ 'hash' as const, 'id' as const ], pathParams: [ 'hash' as const, 'id' as const ],
paginationFields: [ 'items_count' as const, 'token_id' as const, 'value' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -438,17 +412,6 @@ export const RESOURCES = { ...@@ -438,17 +412,6 @@ export const RESOURCES = {
// SEARCH // SEARCH
search: { search: {
path: '/api/v2/search', path: '/api/v2/search',
paginationFields: [
'address_hash' as const,
'block_hash' as const,
'holder_count' as const,
'inserted_at' as const,
'item_type' as const,
'items_count' as const,
'name' as const,
'q' as const,
'tx_hash' as const,
],
filterFields: [ 'q' ], filterFields: [ 'q' ],
}, },
search_check_redirect: { search_check_redirect: {
...@@ -463,7 +426,6 @@ export const RESOURCES = { ...@@ -463,7 +426,6 @@ export const RESOURCES = {
// L2 // L2
l2_deposits: { l2_deposits: {
path: '/api/v2/optimism/deposits', path: '/api/v2/optimism/deposits',
paginationFields: [ 'nonce' as const, 'items_count' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -473,7 +435,6 @@ export const RESOURCES = { ...@@ -473,7 +435,6 @@ export const RESOURCES = {
l2_withdrawals: { l2_withdrawals: {
path: '/api/v2/optimism/withdrawals', path: '/api/v2/optimism/withdrawals',
paginationFields: [ 'nonce' as const, 'items_count' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -483,7 +444,6 @@ export const RESOURCES = { ...@@ -483,7 +444,6 @@ export const RESOURCES = {
l2_output_roots: { l2_output_roots: {
path: '/api/v2/optimism/output-roots', path: '/api/v2/optimism/output-roots',
paginationFields: [ 'index' as const, 'items_count' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -493,7 +453,6 @@ export const RESOURCES = { ...@@ -493,7 +453,6 @@ export const RESOURCES = {
l2_txn_batches: { l2_txn_batches: {
path: '/api/v2/optimism/txn-batches', path: '/api/v2/optimism/txn-batches',
paginationFields: [ 'block_number' as const, 'items_count' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -527,10 +486,6 @@ export type ResourceFiltersKey<R extends ResourceName> = typeof RESOURCES[R] ext ...@@ -527,10 +486,6 @@ export type ResourceFiltersKey<R extends ResourceName> = typeof RESOURCES[R] ext
ArrayElement<typeof RESOURCES[R]['filterFields']> : ArrayElement<typeof RESOURCES[R]['filterFields']> :
never; never;
export type ResourcePaginationKey<R extends ResourceName> = typeof RESOURCES[R] extends {paginationFields: Array<unknown>} ?
ArrayElement<typeof RESOURCES[R]['paginationFields']> :
never;
export const resourceKey = (x: keyof typeof RESOURCES) => x; export const resourceKey = (x: keyof typeof RESOURCES) => x;
type ResourcePathParamName<Resource extends ResourceName> = type ResourcePathParamName<Resource extends ResourceName> =
......
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import difference from 'lodash/difference';
import mapValues from 'lodash/mapValues';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import pick from 'lodash/pick';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll'; import { animateScroll } from 'react-scroll';
import type { PaginatedResources, PaginatedResponse, PaginationFilters } from 'lib/api/resources'; import type { PaginatedResources, PaginationFilters } from 'lib/api/resources';
import { RESOURCES } from 'lib/api/resources'; import { RESOURCES } from 'lib/api/resources';
import type { Params as UseApiQueryParams } from 'lib/api/useApiQuery'; import type { Params as UseApiQueryParams } from 'lib/api/useApiQuery';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
interface Params<Resource extends PaginatedResources> { interface Params<Resource extends PaginatedResources> {
resourceName: Resource; resourceName: Resource;
...@@ -20,6 +18,18 @@ interface Params<Resource extends PaginatedResources> { ...@@ -20,6 +18,18 @@ interface Params<Resource extends PaginatedResources> {
scrollRef?: React.RefObject<HTMLDivElement>; scrollRef?: React.RefObject<HTMLDivElement>;
} }
type NextPageParams = Record<string, unknown>;
function getPaginationParamsFromQuery(queryString: string | Array<string> | undefined) {
if (queryString) {
try {
return JSON.parse(decodeURIComponent(getQueryParamString(queryString))) as NextPageParams;
} catch (error) {}
}
return {};
}
export default function useQueryWithPages<Resource extends PaginatedResources>({ export default function useQueryWithPages<Resource extends PaginatedResources>({
resourceName, resourceName,
filters, filters,
...@@ -31,14 +41,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -31,14 +41,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
type NextPageParams = {
[K in keyof PaginatedResponse<Resource>['next_page_params']]: string;
}
const currPageParams = mapValues(pick(router.query, resource.paginationFields), (value) => value?.toString()) as NextPageParams;
const [ page, setPage ] = React.useState<number>(router.query.page && !Array.isArray(router.query.page) ? Number(router.query.page) : 1); const [ page, setPage ] = React.useState<number>(router.query.page && !Array.isArray(router.query.page) ? Number(router.query.page) : 1);
const [ pageParams, setPageParams ] = React.useState<Record<number, NextPageParams>>({ const [ pageParams, setPageParams ] = React.useState<Record<number, NextPageParams>>({
[page]: currPageParams, [page]: getPaginationParamsFromQuery(router.query.next_page_params),
}); });
const [ hasPagination, setHasPagination ] = React.useState(page > 1); const [ hasPagination, setHasPagination ] = React.useState(page > 1);
...@@ -65,21 +70,21 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -65,21 +70,21 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
// we hide next page button if no next_page_params // we hide next page button if no next_page_params
return; return;
} }
const nextPageParams = data.next_page_params;
setPageParams((prev) => ({ setPageParams((prev) => ({
...prev, ...prev,
[page + 1]: mapValues(nextPageParams, (value) => String(value)) as NextPageParams, [page + 1]: data.next_page_params as NextPageParams,
})); }));
setPage(prev => prev + 1); setPage(prev => prev + 1);
const nextPageQuery = { ...router.query }; const nextPageQuery = {
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = String(val)); ...router.query,
nextPageQuery.page = String(page + 1); page: String(page + 1),
setHasPagination(true); next_page_params: encodeURIComponent(JSON.stringify(data.next_page_params)),
};
setHasPagination(true);
scrollToTop(); scrollToTop();
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }); router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true });
}, [ data?.next_page_params, page, router, scrollToTop ]); }, [ data?.next_page_params, page, router, scrollToTop ]);
...@@ -88,7 +93,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -88,7 +93,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
// we dont have pagination params for the first page // we dont have pagination params for the first page
let nextPageQuery: typeof router.query = { ...router.query }; let nextPageQuery: typeof router.query = { ...router.query };
if (page === 2) { if (page === 2) {
nextPageQuery = omit(router.query, difference(resource.paginationFields, resource.filterFields), 'page'); nextPageQuery = omit(router.query, [ 'next_page_params', 'page' ]);
canGoBackwards.current = true; canGoBackwards.current = true;
} else { } else {
const nextPageParams = pageParams[page - 1]; const nextPageParams = pageParams[page - 1];
...@@ -103,13 +108,13 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -103,13 +108,13 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] }); page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] });
}); });
setHasPagination(true); setHasPagination(true);
}, [ router, page, resource.paginationFields, resource.filterFields, pageParams, scrollToTop, queryClient, resourceName ]); }, [ router, page, pageParams, scrollToTop, queryClient, resourceName ]);
const resetPage = useCallback(() => { const resetPage = useCallback(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] }); queryClient.removeQueries({ queryKey: [ resourceName ] });
scrollToTop(); scrollToTop();
const nextRouterQuery = omit(router.query, difference(resource.paginationFields, resource.filterFields), 'page'); const nextRouterQuery = omit(router.query, [ 'next_page_params', 'page' ]);
router.push({ pathname: router.pathname, query: nextRouterQuery }, undefined, { shallow: true }).then(() => { router.push({ pathname: router.pathname, query: nextRouterQuery }, undefined, { shallow: true }).then(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] }); queryClient.removeQueries({ queryKey: [ resourceName ] });
setPage(1); setPage(1);
...@@ -123,10 +128,10 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -123,10 +128,10 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
}); });
setHasPagination(true); setHasPagination(true);
}, [ queryClient, resourceName, router, resource.paginationFields, resource.filterFields, scrollToTop ]); }, [ queryClient, resourceName, router, scrollToTop ]);
const onFilterChange = useCallback((newFilters: PaginationFilters<Resource> | undefined) => { const onFilterChange = useCallback((newFilters: PaginationFilters<Resource> | undefined) => {
const newQuery = omit<typeof router.query>(router.query, resource.paginationFields, 'page', resource.filterFields); const newQuery = omit<typeof router.query>(router.query, 'next_page_params', 'page', resource.filterFields);
if (newFilters) { if (newFilters) {
Object.entries(newFilters).forEach(([ key, value ]) => { Object.entries(newFilters).forEach(([ key, value ]) => {
if (value && value.length) { if (value && value.length) {
...@@ -147,7 +152,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -147,7 +152,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
setPage(1); setPage(1);
setPageParams({}); setPageParams({});
}); });
}, [ router, resource.paginationFields, resource.filterFields, scrollToTop ]); }, [ router, resource.filterFields, scrollToTop ]);
const nextPageParams = data?.next_page_params; const nextPageParams = data?.next_page_params;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment