diff --git a/components/TableContainer/TableContainerGetter.tsx b/components/TableContainer/TableContainerGetter.tsx index 978c73a66..c2b4e8e6c 100644 --- a/components/TableContainer/TableContainerGetter.tsx +++ b/components/TableContainer/TableContainerGetter.tsx @@ -35,9 +35,11 @@ const TableContainer = ({ columns, dataGetter, opts, ssr, tableRefreshCount, emp const [pageCount, setPageCount] = useState(0) const [loading, setLoading] = useState(true) const emptyMessageDisplay = emptyMessage ?? DEFAULT_EMPTY_TABLE_MESSAGE + const [hiddenColumns, setHiddenColumns] = useState({}) const triggerSort = (column: any): void => { - if (column.disableSortBy === true) return + if (column.disableSortBy === true || hiddenColumns[column.id]) return + const id = column.id if (sortColumn === id) { setSortDesc(!sortDesc) @@ -47,6 +49,10 @@ const TableContainer = ({ columns, dataGetter, opts, ssr, tableRefreshCount, emp } gotoPage(0) } + + const toggleColumn = (id: any): void => { + setHiddenColumns((prev) => ({ ...prev, [id]: !prev[id]})) + } const { getTableProps, @@ -114,8 +120,15 @@ const TableContainer = ({ columns, dataGetter, opts, ssr, tableRefreshCount, emp {headerGroup.headers.map((column: any) => ( { triggerSort(column) }}> +
{column.render('Header')} - {generateSortingIndicator(column)} + {column.shrinkable && ( + toggleColumn(column.id)} style={{ cursor: 'pointer' }}> + {hiddenColumns[column.id] ?
:
} + + )} + {!column.shrinkable && generateSortingIndicator(column)} +
))} @@ -129,9 +142,9 @@ const TableContainer = ({ columns, dataGetter, opts, ssr, tableRefreshCount, emp prepareRow(row) return ( - {row.cells.map((cell: any) => { - return {cell.render('Cell')} - })} + {row.cells.map((cell: any) => + hiddenColumns[cell.column.id] ? : {cell.render('Cell')} + )} ) }) diff --git a/components/Transaction/AddressTransactions.tsx b/components/Transaction/PaybuttonTransactions.tsx similarity index 62% rename from components/Transaction/AddressTransactions.tsx rename to components/Transaction/PaybuttonTransactions.tsx index 6f169fc06..db6fa90b5 100644 --- a/components/Transaction/AddressTransactions.tsx +++ b/components/Transaction/PaybuttonTransactions.tsx @@ -1,12 +1,11 @@ import React, { useMemo } from 'react' -import style from './transaction.module.css' import Image from 'next/image' import XECIcon from 'assets/xec-logo.png' import BCHIcon from 'assets/bch-logo.png' import EyeIcon from 'assets/eye-icon.png' import CheckIcon from 'assets/check-icon.png' import XIcon from 'assets/x-icon.png' -import TableContainerGetter from '../../components/TableContainer/TableContainerGetter' +import TableContainerGetter from '../TableContainer/TableContainerGetter' import { compareNumericString } from 'utils/index' import moment from 'moment-timezone' import { XEC_TX_EXPLORER_URL, BCH_TX_EXPLORER_URL } from 'constants/index' @@ -15,26 +14,29 @@ interface IProps { addressSyncing: { [address: string]: boolean } + paybuttonId: string tableRefreshCount: number timezone: string } -function getGetterForAddress (addressString: string): Function { +function fetchTransactionsByPaybuttonId (paybuttonId: string): Function { return async (page: number, pageSize: number, orderBy: string, orderDesc: boolean) => { - const ok = await fetch(`/api/address/transactions/${addressString}?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}&orderDesc=${String(orderDesc)}`, { + const response = await fetch(`/api/paybutton/transactions/${paybuttonId}?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}&orderDesc=${String(orderDesc)}`, { headers: { Timezone: moment.tz.guess() } }) - const ok2 = await fetch(`/api/address/transactions/count/${addressString}`) + const responseCount = await fetch(`/api/paybutton/transactions/count/${paybuttonId}`) + const transactions = await response.json() + const count = await responseCount.json() return { - data: await ok.json(), - totalCount: await ok2.json() + data: transactions.transactions, + totalCount: count } } } -export default ({ addressSyncing, tableRefreshCount, timezone = moment.tz.guess() }: IProps): JSX.Element => { +export default ({ paybuttonId, addressSyncing, tableRefreshCount, timezone = moment.tz.guess() }: IProps): JSX.Element => { const columns = useMemo( () => [ { @@ -80,17 +82,31 @@ export default ({ addressSyncing, tableRefreshCount, timezone = moment.tz.guess( } }, { - Header: 'TX', + Header: () => (
TX
), accessor: 'hash', disableSortBy: true, Cell: (cellProps) => { const url = cellProps.cell.row.values['address.networkId'] === 1 ? XEC_TX_EXPLORER_URL : BCH_TX_EXPLORER_URL return ( - -
- View on explorer -
-
+
+ +
+ View on explorer +
+
+
+ ) + } + }, + { + Header: () => (
Address
), + accessor: 'address.address', + shrinkable: true, + Cell: (cellProps) => { + return ( +
+ {cellProps.cell.value} +
) } } @@ -99,19 +115,7 @@ export default ({ addressSyncing, tableRefreshCount, timezone = moment.tz.guess( ) return ( <> - {Object.keys(addressSyncing).map(transactionAddress => ( -
-
-
{transactionAddress}
- -
- View on explorer -
-
-
- -
- ))} + ) } diff --git a/components/Transaction/index.tsx b/components/Transaction/index.tsx index 2cab8a8e0..193bb5bec 100644 --- a/components/Transaction/index.tsx +++ b/components/Transaction/index.tsx @@ -1,5 +1,5 @@ -import AddressTransactions from './AddressTransactions' +import PaybuttonTransactions from './PaybuttonTransactions' export { - AddressTransactions + PaybuttonTransactions } diff --git a/pages/api/paybutton/transactions/[id].ts b/pages/api/paybutton/transactions/[id].ts index 3a7dbd046..10c62ff6b 100644 --- a/pages/api/paybutton/transactions/[id].ts +++ b/pages/api/paybutton/transactions/[id].ts @@ -1,5 +1,5 @@ -import { RESPONSE_MESSAGES } from 'constants/index' -import { fetchTransactionsByPaybuttonId } from 'services/transactionService' +import { RESPONSE_MESSAGES, TX_PAGE_SIZE_LIMIT } from 'constants/index' +import { fetchTransactionsByPaybuttonIdWithPagination } from 'services/transactionService' import * as paybuttonService from 'services/paybuttonService' import { setSession } from 'utils/setSession' import { parseError } from 'utils/validators' @@ -9,6 +9,17 @@ export default async (req: any, res: any): Promise => { await setSession(req, res) const userId = req.session.userId const paybuttonId = req.query.id as string + const page = (req.query.page === '' || req.query.page === undefined) ? 0 : Number(req.query.page) + const pageSize = (req.query.pageSize === '' || req.query.pageSize === undefined) ? DEFAULT_TX_PAGE_SIZE : Number(req.query.pageSize) + const orderBy = (req.query.orderBy === '' || req.query.orderBy === undefined) ? undefined : req.query.orderBy as string + const orderDesc: boolean = !!(req.query.orderDesc === '' || req.query.orderDesc === undefined || req.query.orderDesc === 'true') + + if (isNaN(page) || isNaN(pageSize)) { + throw new Error(RESPONSE_MESSAGES.PAGE_SIZE_AND_PAGE_SHOULD_BE_NUMBERS_400.message) + } + if (pageSize > TX_PAGE_SIZE_LIMIT) { + throw new Error(RESPONSE_MESSAGES.PAGE_SIZE_LIMIT_EXCEEDED_400.message) + } try { const paybutton = await paybuttonService.fetchPaybuttonById(paybuttonId) @@ -16,7 +27,7 @@ export default async (req: any, res: any): Promise => { throw new Error(RESPONSE_MESSAGES.RESOURCE_DOES_NOT_BELONG_TO_USER_400.message) } - const transactions = await fetchTransactionsByPaybuttonId(paybuttonId) + const transactions = await fetchTransactionsByPaybuttonIdWithPagination(paybuttonId, page, pageSize, orderDesc, orderBy) res.status(200).json({ transactions }) } catch (err: any) { diff --git a/pages/button/[id].tsx b/pages/button/[id].tsx index 0bb50cbc2..b1729ac41 100644 --- a/pages/button/[id].tsx +++ b/pages/button/[id].tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react' import Page from 'components/Page' import { PaybuttonDetail } from 'components/Paybutton' import { PaybuttonWithAddresses } from 'services/paybuttonService' -import { AddressTransactions } from 'components/Transaction' +import { PaybuttonTransactions } from 'components/Transaction' import supertokensNode from 'supertokens-node' import * as SuperTokensConfig from '../../config/backendConfig' import Session from 'supertokens-node/recipe/session' @@ -101,7 +101,7 @@ export default function PayButton (props: PaybuttonProps): React.ReactElement { }) socket.on(SOCKET_MESSAGES.INCOMING_TXS, (broadcastedData: BroadcastTxData) => { - setTableRefreshCount(tableRefreshCount + 1) + setTableRefreshCount(tableRefreshCountCurrent => tableRefreshCountCurrent + 1) updateIsSyncing([broadcastedData.address]) }) } @@ -207,7 +207,7 @@ export default function PayButton (props: PaybuttonProps): React.ReactElement {
- + ) diff --git a/services/transactionService.ts b/services/transactionService.ts index 155c4c7ee..aa910e0c3 100644 --- a/services/transactionService.ts +++ b/services/transactionService.ts @@ -145,6 +145,57 @@ export async function fetchTransactionsByAddressList ( }) } +export async function fetchTransactionsByAddressListWithPagination ( + addressIdList: string[], + page: number, + pageSize: number, + orderBy?: string, + orderDesc = true, + networkIdsListFilter?: number[], +): Promise { + + const orderDescString: Prisma.SortOrder = orderDesc ? 'desc' : 'asc' + + // Get query for orderBy that works with nested properties (e.g. `address.networkId`) + let orderByQuery + if (orderBy !== undefined && orderBy !== '') { + if (orderBy.includes('.')) { + const [relation, property] = orderBy.split('.') + orderByQuery = { + [relation]: { + [property]: orderDescString + } + } + } else { + orderByQuery = { + [orderBy]: orderDescString + } + } + } else { + // Default orderBy + orderByQuery = { + timestamp: orderDescString + } + } + + return await prisma.transaction.findMany({ + where: { + addressId: { + in: addressIdList + }, + address: { + networkId: { + in: networkIdsListFilter ?? Object.values(NETWORK_IDS) + } + } + }, + include: includePaybuttonsAndPrices, + orderBy: orderByQuery, + skip: page * pageSize, + take: pageSize, + }) +} + export async function fetchTxCountByAddressString (addressString: string): Promise { return await prisma.transaction.count({ where: { @@ -513,6 +564,29 @@ export async function fetchTransactionsByPaybuttonId (paybuttonId: string, netwo return transactions } +export async function fetchTransactionsByPaybuttonIdWithPagination ( + paybuttonId: string, + page: number, + pageSize: number, + orderDesc: boolean, + orderBy?: string, + networkIds?: number[]): Promise { + const addressIdList = await fetchAddressesByPaybuttonId(paybuttonId) + const transactions = await fetchTransactionsByAddressListWithPagination( + addressIdList, + page, + pageSize, + orderBy, + orderDesc, + networkIds); + + if (transactions.length === 0) { + throw new Error(RESPONSE_MESSAGES.NO_TRANSACTION_FOUND_404.message) + } + + return transactions +} + export const getTransactionValueInCurrency = (transaction: TransactionWithAddressAndPrices, currency: SupportedQuotesType): number => { const { prices, @@ -661,7 +735,7 @@ export async function fetchAllPaymentsByUserIdWithPagination ( userId, page, pageSize, orderDesc, buttonIds ) } - + // Get query for orderBy that works with nested properties (e.g. `address.networkId`) let orderByQuery if (orderBy !== undefined && orderBy !== '') { if (orderBy === 'values') { diff --git a/styles/global.css b/styles/global.css index 8bd3dae12..4ed7ce91e 100644 --- a/styles/global.css +++ b/styles/global.css @@ -161,6 +161,20 @@ button:enabled:hover { background: #f4f4f4; } +.table-arrow-right { + width: 0; + height: 0; + border-style: solid; + border-width: 4px 5px 4px 0; + border-color: transparent var(--primary-text-color) transparent transparent; + position: absolute; + right: 0; + top: 0; + bottom: 0; + margin: auto; + opacity: 0.5; +} + .table-sort-arrow-down, .table-sort-arrow-up { width: 0; @@ -219,12 +233,13 @@ button:enabled:hover { } .table-eye-ctn { - text-align: right; + text-align: center; display: flex; align-items: center; - justify-content: flex-end; + justify-content: center; } + .table-eye { width: 20px; opacity: 0.6;