diff --git a/.vscode/settings.json b/.vscode/settings.json index 54466c7..d208abf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -51,6 +51,7 @@ "precaching", "privatekeys", "readonly", + "serialisable", "serialised", "singleline", "submenu", diff --git a/src/pages/blocks/BlocksTable.tsx b/src/pages/blocks/BlocksTable.tsx index 7dee3bb..7de362e 100644 --- a/src/pages/blocks/BlocksTable.tsx +++ b/src/pages/blocks/BlocksTable.tsx @@ -9,8 +9,7 @@ import { KristBlock } from "@api/types"; import { lookupBlocks, LookupBlocksOptions, LookupBlocksResponse } from "@api/lookup"; -import { useMalleablePagination } from "@utils/table"; -import { useIntegerSetting } from "@utils/settings"; +import { useMalleablePagination, useTableHistory } from "@utils/table"; import { ContextualAddress } from "@comp/addresses/ContextualAddress"; import { BlockHash } from "./BlockHash"; @@ -32,13 +31,9 @@ export function BlocksTable({ refreshingID, lowest, setError, setPagination }: Props): JSX.Element { const { t } = useTranslation(); - const defaultPageSize = useIntegerSetting("defaultPageSize"); - const [loading, setLoading] = useState(true); const [res, setRes] = useState(); - const [options, setOptions] = useState({ - limit: defaultPageSize, - offset: 0, + const { options, setOptions } = useTableHistory({ orderBy: lowest ? "hash" : "height", order: lowest ? "ASC" : "DESC" }); diff --git a/src/pages/names/NamesTable.tsx b/src/pages/names/NamesTable.tsx index fd722d9..97ef21b 100644 --- a/src/pages/names/NamesTable.tsx +++ b/src/pages/names/NamesTable.tsx @@ -8,8 +8,7 @@ import { KristName } from "@api/types"; import { lookupNames, LookupNamesOptions, LookupNamesResponse } from "@api/lookup"; -import { useMalleablePagination } from "@utils/table"; -import { useIntegerSetting } from "@utils/settings"; +import { useMalleablePagination, useTableHistory } from "@utils/table"; import { KristNameLink } from "@comp/names/KristNameLink"; import { ContextualAddress } from "@comp/addresses/ContextualAddress"; @@ -34,13 +33,9 @@ export function NamesTable({ refreshingID, sortNew, addresses, setError, setPagination }: Props): JSX.Element { const { t } = useTranslation(); - const defaultPageSize = useIntegerSetting("defaultPageSize"); - const [loading, setLoading] = useState(true); const [res, setRes] = useState(); - const [options, setOptions] = useState({ - limit: defaultPageSize, - offset: 0, + const { options, setOptions } = useTableHistory({ orderBy: sortNew ? "registered" : "name", order: sortNew ? "DESC" : "ASC" }); diff --git a/src/pages/transactions/TransactionPage.tsx b/src/pages/transactions/TransactionPage.tsx index a17a650..3b45866 100644 --- a/src/pages/transactions/TransactionPage.tsx +++ b/src/pages/transactions/TransactionPage.tsx @@ -153,7 +153,7 @@ siteTitle: t("transaction.siteTitleTransaction", { id: kristTransaction.id }), subTitle: t("transaction.subTitleTransaction", { id: kristTransaction.id }) } - : { siteTitleKey: "transaction.siteTransaction" }; + : { siteTitleKey: "transaction.siteTitle" }; return { - debug("looking up transactions (type: %d mapped: %d) for %s", listingType, LISTING_TYPE_MAP[listingType], name || (addresses ? addresses.join(",") : "network")); + debug( + "looking up transactions (type: %d mapped: %d) for %s", + listingType, + LISTING_TYPE_MAP[listingType], + name || (addresses ? addresses.join(",") : "network"), + options + ); setLoading(true); const lookupQuery = query diff --git a/src/utils/index.ts b/src/utils/index.ts index 090ba07..741b797 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,7 +1,12 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under GPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import { EffectCallback, useEffect } from "react"; +import { EffectCallback, useEffect, useState } from "react"; + +import { useHistory, useLocation } from "react-router-dom"; + +import Debug from "debug"; +const debug = Debug("kristweb:utils"); export const toHex = (input: ArrayBufferLike | Uint8Array): string => [...(input instanceof Uint8Array ? input : new Uint8Array(input))] @@ -98,3 +103,50 @@ * method I could find online (with an admittedly non-exhaustive search) */ export const ctrl = /mac/i.test(navigator.platform) ? "\u2318" : "Ctrl"; + +/** + * Wrapper for useState that saves its value in the browser history stack + * as location state. Note that this doesn't yet support computed state. + * + * The state's value must be serialisable, and less than 2 MiB. + * + * @param initialState - The initial value of the state. + * @param stateKey - The key by which to store the state's value in the history + * stack. + */ +export function useHistoryState( + initialState: S, + stateKey: string +): [S, (s: S) => void] { + const history = useHistory(); + const location = useLocation>>(); + + const [state, setState] = useState( + location?.state?.[stateKey] ?? initialState + ); + + // Wraps setState to update the stored state value and replace the entry on + // the history stack (via `updateLocation`). + function wrappedSetState(newState: S): void { + debug("useHistoryState: setting state %s to %o", stateKey, newState); + updateLocation(newState); + setState(newState); + } + + // Merge the new state into the location state (using stateKey) and replace + // the entry on the history stack. + function updateLocation(newState: S) { + const updatedLocation = { + ...location, + state: { + ...location?.state, + [stateKey]: newState + } + }; + + debug("useHistoryState: replacing updated location:", updatedLocation); + history.replace(updatedLocation); + } + + return [state, wrappedSetState]; +} diff --git a/src/utils/table.tsx b/src/utils/table.tsx index 8b056f2..7dc1788 100644 --- a/src/utils/table.tsx +++ b/src/utils/table.tsx @@ -100,11 +100,21 @@ } { const { t } = useTranslation(); + // The currentPageSize and currentPage may be provided by the useTableHistory + // hook, which gets the values from the browser state const defaultPageSize = useIntegerSetting("defaultPageSize"); + const currentPageSize = options.limit ?? defaultPageSize; + const currentPage = options.offset + ? Math.max(Math.floor(options.offset / currentPageSize) + 1, 1) + : 1; // All this is done to allow putting the pagination in the page header - const [paginationPos, setPaginationPos] = useState({}); - const paginationConfig = getTablePaginationConfig(t, res, totalKey, defaultPageSize, paginationPos); + const [paginationPos, setPaginationPos] = useState({ + current: currentPage, + pageSize: currentPageSize + }); + const paginationConfig = getTablePaginationConfig(t, res, totalKey, currentPageSize, paginationPos); + debug(defaultPageSize, currentPageSize, currentPage, paginationPos, paginationConfig); const [mergedPagination] = usePagination( results?.length || 0, paginationConfig, @@ -174,31 +184,71 @@ return qs; } +/** Wraps the setOptions for a table, providing a sane default page size, + * and storing state changes in the history stack. When the page is returned to, + * the history stack is checked and location state is used as defaults. */ export function useTableHistory< - OptionsT extends LookupFilterOptionsBase + OptionsT extends LookupFilterOptionsBase, + ExtraStateT = any, >( - defaults: Partial & Pick + defaults: Partial & Pick, + defaultExtraState?: Partial ): { options: OptionsT; setOptions: (opts: OptionsT) => void; + extraState?: ExtraStateT; + setExtraState: (extra: ExtraStateT) => void; } { // Used to get/set the browser history state const history = useHistory(); - const location = useLocation(); + const location = useLocation & { extra?: ExtraStateT }>(); + const { state } = location; const defaultPageSize = useIntegerSetting("defaultPageSize"); + // The table filter parameters const [options, setOptions] = useState({ - limit: defaults.limit ?? defaultPageSize, - offset: defaults.offset ?? 0, - orderBy: defaults.orderBy, - order: defaults.order + limit: state?.limit ?? defaults.limit ?? defaultPageSize, + offset: state?.offset ?? defaults.offset ?? 0, + orderBy: state?.orderBy ?? defaults.orderBy, + order: state?.order ?? defaults.order } as OptionsT); function wrappedSetOptions(opts: OptionsT) { debug("table calling setOptions:", opts); + updateLocation(opts); return setOptions(opts); } - return { options, setOptions: wrappedSetOptions }; + // Extra state parameters (e.g. "include mined transactions") + const [extraState, setExtraState] = useState( + (location?.state?.extra ?? defaultExtraState) as ExtraStateT | undefined + ); + + function wrappedSetExtraState(extra: ExtraStateT) { + debug("table calling setExtraState:", extra); + updateLocation(undefined, extra); + return setExtraState(extra); + } + + // Merge the options and extra state into the location state and replace the + // entry on the history stack. + function updateLocation(opts?: OptionsT, extra?: ExtraStateT) { + const updatedLocation = { + ...location, + state: { + ...location?.state, + ...(opts ?? {}), + ...(extra ?? {}) + } + }; + + debug("replacing updated location:", updatedLocation); + history.replace(updatedLocation); + } + + return { + options, setOptions: wrappedSetOptions, + extraState, setExtraState: wrappedSetExtraState + }; }