diff --git a/public/locales/en.json b/public/locales/en.json index 64d053e..2cf6c5f 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -42,6 +42,7 @@ "sendLong": "Send Krist", "request": "Request", "requestLong": "Request Krist", + "sort": "Sort results", "settings": "Settings", "more": "More" @@ -1119,5 +1120,24 @@ "modalContent": "If you forgot your master password for KristWeb v1, then you will not be able to migrate your wallets. You will never be asked again. Are you sure you want to skip migration?", "buttonSkip": "Skip" } + }, + + "sortModal": { + "title": "Sort results", + + "sortBy": "Sort by", + "sortOrder": "Sort order", + "sortAscending": "Ascending", + "sortDescending": "Descending", + + "buttonReset": "Reset", + + "options": { + "transactionsFrom": "From", + "transactionsTo": "To", + "transactionsValue": "Value", + "transactionsName": "Name", + "transactionsTime": "Time" + } } } diff --git a/src/layout/nav/TopMenu.tsx b/src/layout/nav/TopMenu.tsx index cc578f3..5117300 100644 --- a/src/layout/nav/TopMenu.tsx +++ b/src/layout/nav/TopMenu.tsx @@ -4,7 +4,8 @@ import { useState, useCallback, useMemo, useContext, createContext, FC, ReactNode } from "react"; import { Menu, Dropdown } from "antd"; import { - MoreOutlined, SettingOutlined, SendOutlined, DownloadOutlined + MoreOutlined, SettingOutlined, SendOutlined, DownloadOutlined, + SortAscendingOutlined } from "@ant-design/icons"; import { useTFns } from "@utils/i18n"; @@ -12,6 +13,8 @@ import { ConditionalLink } from "@comp/ConditionalLink"; import { useBreakpoint } from "@utils/hooks"; +import { OpenSortModalFn, SetOpenSortModalFn } from "@utils/table/SortModal"; + import Debug from "debug"; const debug = Debug("kristweb:top-menu"); @@ -21,6 +24,9 @@ interface TopMenuCtxRes { options?: ReactNode; setMenuOptions?: SetMenuOptsFn; + + openSortModalFn?: OpenSortModalFn; + setOpenSortModal?: SetOpenSortModalFn; } export const TopMenuContext = createContext({}); @@ -31,6 +37,7 @@ const ctxRes = useContext(TopMenuContext); const options = ctxRes?.options; + const openSortModalFn = ctxRes?.openSortModalFn; const menu = useMemo(() => ( + + + + {tStr("sort")} + + } + {/* Settings item */} @@ -69,7 +86,7 @@ > - ), [tStr, options]); + ), [tStr, options, openSortModalFn]); // If on mobile and there are options available from the page, display them // instead of the settings button. @@ -101,18 +118,28 @@ export const TopMenuProvider: FC = ({ children }) => { const [menuOptions, setMenuOptions] = useState(); + const [openSortModalFn, setOpenSortModal] = useState(); + const res: TopMenuCtxRes = useMemo(() => ({ - options: menuOptions, setMenuOptions - }), [menuOptions, setMenuOptions]); + options: menuOptions, setMenuOptions, + openSortModalFn, setOpenSortModal + }), [menuOptions, openSortModalFn]); return {children} ; }; -export function useTopMenuOptions(): [boolean, SetMenuOptsFn, () => void] { +export type TopMenuOptionsHookRes = [ + boolean, // isMobile + SetMenuOptsFn, // set + () => void, // unset + SetOpenSortModalFn | undefined +] + +export function useTopMenuOptions(): TopMenuOptionsHookRes { const bps = useBreakpoint(); - const { setMenuOptions } = useContext(TopMenuContext); + const { setMenuOptions, setOpenSortModal } = useContext(TopMenuContext); const set = useCallback((opts: Opts) => { debug("top menu options hook set"); @@ -122,8 +149,9 @@ const unset = useCallback(() => { debug("top menu options hook destructor"); setMenuOptions?.(undefined); - }, [setMenuOptions]); + setOpenSortModal?.(undefined); + }, [setMenuOptions, setOpenSortModal]); // Return whether or not the options are being shown - return [!bps.md, set, unset]; + return [!bps.md, set, unset, setOpenSortModal]; } diff --git a/src/pages/transactions/TransactionsPage.tsx b/src/pages/transactions/TransactionsPage.tsx index 48113ef..62b3bf2 100644 --- a/src/pages/transactions/TransactionsPage.tsx +++ b/src/pages/transactions/TransactionsPage.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under AGPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import { useState, useMemo } from "react"; +import { useState, useMemo, useEffect } from "react"; import { Switch } from "antd"; import { useTranslation, TFunction } from "react-i18next"; @@ -21,6 +21,7 @@ import { useSubscription } from "@global/ws/WebsocketSubscription"; import { useBooleanSetting } from "@utils/settings"; import { useLinkedPagination } from "@utils/table/table"; +import { useTopMenuOptions } from "@layout/nav/TopMenu"; import { useHistoryState } from "@utils/hooks"; import { KristNameLink } from "@comp/names/KristNameLink"; @@ -209,6 +210,9 @@ ? getRefreshID(listingType, includeMined, nodeState, subscribedRefreshID) : 0; + const [,, unset, setOpenSortModal] = useTopMenuOptions(); + useEffect(() => unset, [unset]); + // Memoise the table so that it only updates the props (thus triggering a // re-fetch of the transactions) when something relevant changes const memoTable = useMemo(() => ( @@ -224,13 +228,14 @@ setError={setError} setPagination={setPagination} + setOpenSortModal={setOpenSortModal} /> ), [ listingType, usedAddresses, name, query, usedRefreshID, includeMined, - setError, setPagination + setError, setPagination, setOpenSortModal ]); // Alter the page titles depending on the listing type diff --git a/src/pages/transactions/TransactionsTable.tsx b/src/pages/transactions/TransactionsTable.tsx index b3a58b5..4030a25 100644 --- a/src/pages/transactions/TransactionsTable.tsx +++ b/src/pages/transactions/TransactionsTable.tsx @@ -12,11 +12,11 @@ import { KristTransaction } from "@api/types"; import { lookupTransactions, LookupTransactionsOptions, LookupTransactionsResponse, - LookupTransactionType + LookupTransactionType, SortableTransactionFields } from "@api/lookup"; import { useMalleablePagination, useTableHistory, useDateColumnWidth, useMobileList, - PaginationTableProps, RenderItem + PaginationTableProps, RenderItem, SortOptions, SetOpenSortModalFn } from "@utils/table/table"; import { ListingType } from "./TransactionsPage"; @@ -63,6 +63,7 @@ setError?: Dispatch>; setPagination?: Dispatch>; + setOpenSortModal?: SetOpenSortModalFn; } /** Map the search listing types to their API endpoint name */ @@ -157,9 +158,7 @@ title: tStr("columnName"), dataIndex: "name", key: "name", - render: name => , - - sorter: true + render: name => }, // Metadata @@ -184,20 +183,28 @@ ]; } +const sortOptions: SortOptions = [ + { sortKey: "from", i18nKey: "transactionsFrom" }, + { sortKey: "to", i18nKey: "transactionsTo" }, + { sortKey: "value", i18nKey: "transactionsValue" }, + { sortKey: "time", i18nKey: "transactionsTime" } +]; +const defaultOrderBy = "time"; // Equivalent to sorting by ID +const defaultOrder = "DESC"; + export function TransactionsTable({ listingType, refreshingID, addresses, name, query, includeMined, - setError, setPagination + setError, setPagination, setOpenSortModal }: Props): JSX.Element { const { tKey } = useTFns("transactions."); const [loading, setLoading] = useState(true); const [res, setRes] = useState(); const { options, setOptions } = useTableHistory({ - orderBy: "time", // Equivalent to sorting by ID - order: "DESC" + orderBy: defaultOrderBy, order: defaultOrder }); const { paginationTableProps, paginationChange, hotkeys } = useMalleablePagination( @@ -249,8 +256,9 @@ const { isMobile, list } = useMobileList( loading, res?.transactions || [], "id", - paginationTableProps.pagination, - paginationChange, + paginationTableProps.pagination, paginationChange, + sortOptions, defaultOrderBy, defaultOrder, + options, setOptions, setOpenSortModal, renderMobileItem ); diff --git a/src/utils/table/SortModal.tsx b/src/utils/table/SortModal.tsx new file mode 100644 index 0000000..7edd964 --- /dev/null +++ b/src/utils/table/SortModal.tsx @@ -0,0 +1,166 @@ +// Copyright (c) 2020-2021 Drew Lemmy +// This file is part of KristWeb 2 under AGPL-3.0. +// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt +import { useState, useEffect, Dispatch, SetStateAction } from "react"; +import { Modal, Form, Button, Select, Radio } from "antd"; + +import { useTFns } from "@utils/i18n"; + +import { LookupFilterOptionsBase } from "./table"; + +export interface SortConfig { + sortKey: FieldsT; + i18nKey: string; +} +export type SortOptions = SortConfig[]; + +interface FormValues { + orderBy: FieldsT; + order: "ASC" | "DESC"; +} + +interface Props { + visible: boolean; + setVisible: Dispatch>; + + sortOptions: SortOptions; + defaultOrderBy: FieldsT; + defaultOrder: "ASC" | "DESC"; + + options: LookupFilterOptionsBase; + setOptions: (opts: LookupFilterOptionsBase) => void; +} + +export function SortModal({ + visible, + setVisible, + + sortOptions, + defaultOrderBy, + defaultOrder, + + options, + setOptions +}: Props): JSX.Element { + const { t, tStr } = useTFns("sortModal."); + + const [form] = Form.useForm>(); + + async function onSubmit() { + const values = await form.validateFields(); + console.log(values); + setOptions({ + ...options, + orderBy: values.orderBy, + order: values.order + }); + closeModal(); + } + + function onReset() { + setOptions({ + ...options, + orderBy: defaultOrderBy, + order: defaultOrder + }); + closeModal(); + } + + function closeModal() { + form.resetFields(); + setVisible(false); + } + + // Update the form values if the table is sorted + useEffect(() => { + if (!form || !options) return; + form.setFieldsValue({ + orderBy: options.orderBy || defaultOrderBy, + order: options.order || defaultOrder + } as any); + }, [form, options, defaultOrderBy, defaultOrder]); + + return + {/* Reset */} + + + {/* Cancel */} + + + {/* Submit */} + + } + > + > + form={form} + initialValues={{ + sortBy: options.orderBy, + sortOrder: options.order + }} + + onFinish={onSubmit} + > + {/* Sort by */} + + {/* Present each available sort option as a field */} + + + + {/* Sort order (ascending, descending) */} + + + {tStr("sortAscending")} + {tStr("sortDescending")} + + + + ; +} + +export type OpenSortModalFn = (() => void)[]; +export type SetOpenSortModalFn = (fn: OpenSortModalFn | undefined) => void; + +export function useSortModal( + sortOptions: SortOptions, + defaultOrderBy: FieldsT, + defaultOrder: "ASC" | "DESC", + + options: LookupFilterOptionsBase, + setOptions: (opts: LookupFilterOptionsBase) => void, + setOpenSortModal?: SetOpenSortModalFn +): JSX.Element { + const [visible, setVisible] = useState(false); + useEffect(() => setOpenSortModal?.([() => setVisible(true)]), [setOpenSortModal]); + + return ; +} diff --git a/src/utils/table/mobileList.tsx b/src/utils/table/mobileList.tsx index 3900ef8..fd4c3ef 100644 --- a/src/utils/table/mobileList.tsx +++ b/src/utils/table/mobileList.tsx @@ -5,7 +5,9 @@ import { List } from "antd"; import { PaginationConfig } from "antd/lib/pagination"; -import { PaginationChangeFn } from "@utils/table/table"; +import { PaginationChangeFn, LookupFilterOptionsBase } from "@utils/table/table"; +import { useSortModal, SetOpenSortModalFn, SortOptions } from "./SortModal"; + import { useBreakpoint } from "@utils/hooks"; interface MobileListHookRes { @@ -16,18 +18,34 @@ export type RenderItem = (item: T, index: number) => ReactNode; /** Returns a mobile-specific list view if the screen is small enough. */ -export function useMobileList( +export function useMobileList( loading: boolean, res: T[], rowKey: string, + paginationConfig: Omit | false | undefined, paginationChange: PaginationChangeFn, + + sortOptions: SortOptions, + defaultOrderBy: FieldsT, + defaultOrder: "ASC" | "DESC", + + options: LookupFilterOptionsBase, + setOptions: (opts: LookupFilterOptionsBase) => void, + setOpenSortModal: SetOpenSortModalFn | undefined, + renderItem: (item: T, index: number) => ReactNode ): MobileListHookRes { const bps = useBreakpoint(); const isMobile = !bps.md; console.log(paginationConfig); + console.log(options); + + const sortModal = useSortModal( + sortOptions, defaultOrderBy, defaultOrder, + options, setOptions, setOpenSortModal + ); const pagination: PaginationConfig = useMemo(() => ({ ...paginationConfig, @@ -38,19 +56,23 @@ const list = useMemo(() => { if (!isMobile) return null; - return + ; - }, [isMobile, loading, res, rowKey, pagination, renderItem]); + renderItem={renderItem} + /> + + {sortModal} + ; + }, [isMobile, loading, res, rowKey, pagination, renderItem, sortModal]); return { isMobile, list }; } diff --git a/src/utils/table/table.tsx b/src/utils/table/table.tsx index 84e6b5a..102ffee 100644 --- a/src/utils/table/table.tsx +++ b/src/utils/table/table.tsx @@ -327,3 +327,4 @@ } export * from "./mobileList"; +export * from "./SortModal";