diff --git a/apps/trading-e2e/src/support/mocks/generate-fills.ts b/apps/trading-e2e/src/support/mocks/generate-fills.ts index 6093e032e..94b1d1abc 100644 --- a/apps/trading-e2e/src/support/mocks/generate-fills.ts +++ b/apps/trading-e2e/src/support/mocks/generate-fills.ts @@ -51,7 +51,6 @@ export const generateFills = (override?: PartialDeep): Fills => { id: 'buyer-id', tradesConnection: { __typename: 'TradeConnection', - totalCount: 1, edges: fills.map((f) => { return { __typename: 'TradeEdge', @@ -63,6 +62,8 @@ export const generateFills = (override?: PartialDeep): Fills => { __typename: 'PageInfo', startCursor: '1', endCursor: '2', + hasNextPage: false, + hasPreviousPage: false, }, }, __typename: 'Party', diff --git a/apps/trading-e2e/src/support/mocks/generate-trades.ts b/apps/trading-e2e/src/support/mocks/generate-trades.ts index 0031ec090..b8546e1f5 100644 --- a/apps/trading-e2e/src/support/mocks/generate-trades.ts +++ b/apps/trading-e2e/src/support/mocks/generate-trades.ts @@ -1,9 +1,12 @@ import merge from 'lodash/merge'; import type { PartialDeep } from 'type-fest'; -import type { Trades, Trades_market_trades } from '@vegaprotocol/trades'; +import type { + Trades, + Trades_market_tradesConnection_edges_node, +} from '@vegaprotocol/trades'; export const generateTrades = (override?: PartialDeep): Trades => { - const trades: Trades_market_trades[] = [ + const trades: Trades_market_tradesConnection_edges_node[] = [ { id: 'FFFFBC80005C517A10ACF481F7E6893769471098E696D0CC407F18134044CB16', price: '17116898', @@ -44,10 +47,26 @@ export const generateTrades = (override?: PartialDeep): Trades => { __typename: 'Trade', }, ]; - const defaultResult = { + const defaultResult: Trades = { market: { id: 'market-0', - trades, + tradesConnection: { + __typename: 'TradeConnection', + edges: trades.map((node, i) => { + return { + __typename: 'TradeEdge', + node, + cursor: (i + 1).toString(), + }; + }), + pageInfo: { + __typename: 'PageInfo', + startCursor: '0', + endCursor: trades.length.toString(), + hasNextPage: false, + hasPreviousPage: false, + }, + }, __typename: 'Market', }, }; diff --git a/libs/fills/src/lib/__generated__/Fills.ts b/libs/fills/src/lib/__generated__/Fills.ts index 2d42f6d7b..a1d40b511 100644 --- a/libs/fills/src/lib/__generated__/Fills.ts +++ b/libs/fills/src/lib/__generated__/Fills.ts @@ -206,14 +206,12 @@ export interface Fills_party_tradesConnection_pageInfo { __typename: "PageInfo"; startCursor: string; endCursor: string; + hasNextPage: boolean; + hasPreviousPage: boolean; } export interface Fills_party_tradesConnection { __typename: "TradeConnection"; - /** - * The total number of trades in this connection - */ - totalCount: number; /** * The trade in this connection */ diff --git a/libs/fills/src/lib/fills-data-provider.ts b/libs/fills/src/lib/fills-data-provider.ts index 3c580e5ee..8219e36dd 100644 --- a/libs/fills/src/lib/fills-data-provider.ts +++ b/libs/fills/src/lib/fills-data-provider.ts @@ -1,11 +1,16 @@ import produce from 'immer'; +import orderBy from 'lodash/orderBy'; import { gql } from '@apollo/client'; -import { makeDataProvider } from '@vegaprotocol/react-helpers'; -import type { PageInfo, Pagination } from '@vegaprotocol/react-helpers'; +import { + makeDataProvider, + defaultAppend as append, +} from '@vegaprotocol/react-helpers'; +import type { PageInfo } from '@vegaprotocol/react-helpers'; import type { FillFields } from './__generated__/FillFields'; import type { Fills, Fills_party_tradesConnection_edges, + Fills_party_tradesConnection_edges_node, } from './__generated__/Fills'; import type { FillsSub } from './__generated__/FillsSub'; @@ -64,7 +69,6 @@ export const FILLS_QUERY = gql` party(id: $partyId) { id tradesConnection(marketId: $marketId, pagination: $pagination) { - totalCount edges { node { ...FillFields @@ -74,6 +78,8 @@ export const FILLS_QUERY = gql` pageInfo { startCursor endCursor + hasNextPage + hasPreviousPage } } } @@ -90,16 +96,24 @@ export const FILLS_SUB = gql` `; const update = ( - data: Fills_party_tradesConnection_edges[], + data: (Fills_party_tradesConnection_edges | null)[], delta: FillFields[] ) => { return produce(data, (draft) => { - delta.forEach((node) => { - const index = draft.findIndex((edge) => edge.node.id === node.id); + orderBy(delta, 'createdAt').forEach((node) => { + const index = draft.findIndex((edge) => edge?.node.id === node.id); if (index !== -1) { - Object.assign(draft[index].node, node); + if (draft[index]?.node) { + Object.assign( + draft[index]?.node as Fills_party_tradesConnection_edges_node, + node + ); + } } else { - draft.unshift({ node, cursor: '', __typename: 'TradeEdge' }); + const firstNode = draft[0]?.node; + if (firstNode && node.createdAt >= firstNode.createdAt) { + draft.unshift({ node, cursor: '', __typename: 'TradeEdge' }); + } } }); }); @@ -113,40 +127,8 @@ const getData = ( const getPageInfo = (responseData: Fills): PageInfo | null => responseData.party?.tradesConnection.pageInfo || null; -const getTotalCount = (responseData: Fills): number | undefined => - responseData.party?.tradesConnection.totalCount; - const getDelta = (subscriptionData: FillsSub) => subscriptionData.trades || []; -const append = ( - data: Fills_party_tradesConnection_edges[] | null, - pageInfo: PageInfo, - insertionData: Fills_party_tradesConnection_edges[] | null, - insertionPageInfo: PageInfo | null, - pagination?: Pagination -) => { - if (data && insertionData && insertionPageInfo) { - if (pagination?.after) { - if (data[data.length - 1].cursor === pagination.after) { - return { - data: [...data, ...insertionData], - pageInfo: { ...pageInfo, endCursor: insertionPageInfo.endCursor }, - }; - } else { - const cursors = data.map((item) => item.cursor); - const startIndex = cursors.lastIndexOf(pagination.after); - if (startIndex !== -1) { - return { - data: [...data.slice(0, startIndex), ...insertionData], - pageInfo: { ...pageInfo, endCursor: insertionPageInfo.endCursor }, - }; - } - } - } - } - return { data, pageInfo }; -}; - export const fillsDataProvider = makeDataProvider( FILLS_QUERY, FILLS_SUB, @@ -155,7 +137,6 @@ export const fillsDataProvider = makeDataProvider( getDelta, { getPageInfo, - getTotalCount, append, first: 100, } diff --git a/libs/fills/src/lib/fills-manager.tsx b/libs/fills/src/lib/fills-manager.tsx index 380a51194..789016114 100644 --- a/libs/fills/src/lib/fills-manager.tsx +++ b/libs/fills/src/lib/fills-manager.tsx @@ -3,7 +3,11 @@ import { useCallback, useRef, useMemo } from 'react'; import { useDataProvider } from '@vegaprotocol/react-helpers'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { FillsTable } from './fills-table'; -import type { IGetRowsParams } from 'ag-grid-community'; +import type { + IGetRowsParams, + BodyScrollEvent, + BodyScrollEndEvent, +} from 'ag-grid-community'; import { fillsDataProvider as dataProvider } from './fills-data-provider'; import type { Fills_party_tradesConnection_edges } from './__generated__/Fills'; @@ -15,14 +19,46 @@ interface FillsManagerProps { export const FillsManager = ({ partyId }: FillsManagerProps) => { const gridRef = useRef(null); - const dataRef = useRef(null); + const dataRef = useRef<(Fills_party_tradesConnection_edges | null)[] | null>( + null + ); const totalCountRef = useRef(undefined); + const newRows = useRef(0); + const scrolledToTop = useRef(true); + + const addNewRows = useCallback(() => { + if (newRows.current === 0) { + return; + } + if (totalCountRef.current !== undefined) { + totalCountRef.current += newRows.current; + } + newRows.current = 0; + if (!gridRef.current?.api) { + return; + } + gridRef.current.api.refreshInfiniteCache(); + }, []); const update = useCallback( - ({ data }: { data: Fills_party_tradesConnection_edges[] }) => { + ({ + data, + delta, + }: { + data: (Fills_party_tradesConnection_edges | null)[]; + delta: FillsSub_trades[]; + }) => { if (!gridRef.current?.api) { return false; } + if (!scrolledToTop.current) { + const createdAt = dataRef.current?.[0]?.node.createdAt; + if (createdAt) { + newRows.current += delta.filter( + (trade) => trade.createdAt > createdAt + ).length; + } + } dataRef.current = data; gridRef.current.api.refreshInfiniteCache(); return true; @@ -35,7 +71,7 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => { data, totalCount, }: { - data: Fills_party_tradesConnection_edges[]; + data: (Fills_party_tradesConnection_edges | null)[]; totalCount?: number; }) => { dataRef.current = data; @@ -48,7 +84,7 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => { const variables = useMemo(() => ({ partyId }), [partyId]); const { data, error, loading, load, totalCount } = useDataProvider< - Fills_party_tradesConnection_edges[], + (Fills_party_tradesConnection_edges | null)[], FillsSub_trades[] >({ dataProvider, update, insert, variables }); totalCountRef.current = totalCount; @@ -60,15 +96,14 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => { startRow, endRow, }: IGetRowsParams) => { + startRow += newRows.current; + endRow += newRows.current; try { - if (dataRef.current && dataRef.current.length < endRow) { - await load({ - first: endRow - startRow, - after: dataRef.current[dataRef.current.length - 1].cursor, - }); + if (dataRef.current && dataRef.current.indexOf(null) < endRow) { + await load(); } const rowsThisBlock = dataRef.current - ? dataRef.current.slice(startRow, endRow).map((edge) => edge.node) + ? dataRef.current.slice(startRow, endRow).map((edge) => edge?.node) : []; let lastRow = -1; if (totalCountRef.current !== undefined) { @@ -77,6 +112,8 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => { } else if (totalCountRef.current <= endRow) { lastRow = totalCountRef.current; } + } else if (rowsThisBlock.length < endRow - startRow) { + lastRow = rowsThisBlock.length; } successCallback(rowsThisBlock, lastRow); } catch (e) { @@ -84,9 +121,26 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => { } }; + const onBodyScrollEnd = (event: BodyScrollEndEvent) => { + if (event.top === 0) { + addNewRows(); + } + }; + + const onBodyScroll = (event: BodyScrollEvent) => { + scrolledToTop.current = event.top <= 0; + }; + return ( - + ); }; diff --git a/libs/fills/src/lib/fills-table.spec.tsx b/libs/fills/src/lib/fills-table.spec.tsx index 1b00c2a34..e7c66df27 100644 --- a/libs/fills/src/lib/fills-table.spec.tsx +++ b/libs/fills/src/lib/fills-table.spec.tsx @@ -4,7 +4,7 @@ import { Side } from '@vegaprotocol/types'; import type { PartialDeep } from 'type-fest'; import { FillsTable } from './fills-table'; -import { generateFill, makeGetRows } from './test-helpers'; +import { generateFill } from './test-helpers'; import type { FillFields } from './__generated__/FillFields'; const waitForGridToBeInTheDOM = () => { @@ -47,12 +47,7 @@ describe('FillsTable', () => { }); it('correct columns are rendered', async () => { - render( - - ); + render(); await waitForGridToBeInTheDOM(); await waitForDataToHaveLoaded(); @@ -84,12 +79,7 @@ describe('FillsTable', () => { }, }); - render( - - ); + render(); await waitForGridToBeInTheDOM(); await waitForDataToHaveLoaded(); @@ -126,12 +116,7 @@ describe('FillsTable', () => { }, }); - render( - - ); + render(); await waitForGridToBeInTheDOM(); await waitForDataToHaveLoaded(); @@ -163,10 +148,7 @@ describe('FillsTable', () => { }); const { rerender } = render( - + ); await waitForGridToBeInTheDOM(); await waitForDataToHaveLoaded(); @@ -184,12 +166,7 @@ describe('FillsTable', () => { aggressor: Side.Buy, }); - rerender( - - ); + rerender(); await waitForGridToBeInTheDOM(); await waitForDataToHaveLoaded(); diff --git a/libs/fills/src/lib/fills-table.stories.tsx b/libs/fills/src/lib/fills-table.stories.tsx index 49f637d26..8ae2d9ce1 100644 --- a/libs/fills/src/lib/fills-table.stories.tsx +++ b/libs/fills/src/lib/fills-table.stories.tsx @@ -1,22 +1,397 @@ import type { Story, Meta } from '@storybook/react'; -import type { FillsTableProps } from './fills-table'; +import type { Props } from './fills-table'; +import type { AgGridReact } from 'ag-grid-react'; +import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit'; +import { useCallback, useRef } from 'react'; import { FillsTable } from './fills-table'; -import { generateFills, makeGetRows } from './test-helpers'; +import { generateFills, generateFill } from './test-helpers'; +import type { Fills_party_tradesConnection_edges } from './__generated__/Fills'; +import type { FillsSub_trades } from './__generated__/FillsSub'; +import type { + IGetRowsParams, + BodyScrollEvent, + BodyScrollEndEvent, +} from 'ag-grid-community'; export default { component: FillsTable, title: 'FillsTable', } as Meta; -const Template: Story = (args) => ; - +const Template: Story = (args) => ; export const Default = Template.bind({}); + +const createdAt = new Date('2005-04-02 21:37:00').getTime(); const fills = generateFills(); Default.args = { partyId: 'party-id', - datasource: { - getRows: makeGetRows( - fills.party?.tradesConnection.edges.map((e) => e.node) || [] - ), - }, + rowData: fills.party?.tradesConnection.edges.map((e) => e.node) || [], }; + +const getData = ( + start: number, + end: number +): Fills_party_tradesConnection_edges[] => + new Array(end - start).fill(null).map((v, i) => ({ + __typename: 'TradeEdge', + node: generateFill({ + id: (start + i).toString(), + createdAt: new Date(createdAt - 1000 * (start + i)).toISOString(), + }), + cursor: (start + i).toString(), + })); + +const totalCount = 550; +const partyId = 'partyId'; + +const useDataProvider = ({ + insert, +}: { + insert: ({ + insertionData, + data, + totalCount, + }: { + insertionData: Fills_party_tradesConnection_edges[]; + data: Fills_party_tradesConnection_edges[]; + totalCount?: number; + }) => boolean; +}) => { + const data = [...getData(0, 100), ...new Array(totalCount - 100).fill(null)]; + return { + data, + error: null, + loading: false, + load: (start?: number, end?: number) => { + if (start === undefined) { + start = data.findIndex((v) => !v); + } + if (end === undefined) { + end = start + 100; + } + end = Math.min(end, totalCount); + const insertionData = getData(start, end); + data.splice(start, end - start, ...insertionData); + insert({ data, totalCount, insertionData }); + return Promise.resolve(); + }, + totalCount, + }; +}; + +interface PaginationManagerProps { + pagination: boolean; +} + +const PaginationManager = ({ pagination }: PaginationManagerProps) => { + const gridRef = useRef(null); + const dataRef = useRef(null); + const totalCountRef = useRef(undefined); + const newRows = useRef(0); + const scrolledToTop = useRef(true); + + const addNewRows = useCallback(() => { + if (newRows.current === 0) { + return; + } + if (totalCountRef.current !== undefined) { + totalCountRef.current += newRows.current; + } + newRows.current = 0; + if (!gridRef.current?.api) { + return; + } + gridRef.current.api.refreshInfiniteCache(); + }, []); + + const update = useCallback( + ({ + data, + delta, + }: { + data: Fills_party_tradesConnection_edges[]; + delta: FillsSub_trades[]; + }) => { + if (!gridRef.current?.api) { + return false; + } + if (!scrolledToTop.current) { + const createdAt = dataRef.current?.[0].node.createdAt; + if (createdAt) { + newRows.current += delta.filter( + (trade) => trade.createdAt > createdAt + ).length; + } + } + dataRef.current = data; + gridRef.current.api.refreshInfiniteCache(); + return true; + }, + [] + ); + + const insert = useCallback( + ({ + data, + totalCount, + }: { + data: Fills_party_tradesConnection_edges[]; + totalCount?: number; + }) => { + dataRef.current = data; + totalCountRef.current = totalCount; + return true; + }, + [] + ); + + const { data, error, loading, load, totalCount } = useDataProvider({ + insert, + }); + + totalCountRef.current = totalCount; + dataRef.current = data; + + const getRows = async ({ + successCallback, + failCallback, + startRow, + endRow, + }: IGetRowsParams) => { + startRow += newRows.current; + endRow += newRows.current; + try { + if ( + dataRef.current && + dataRef.current.slice(startRow, endRow).some((i) => !i) + ) { + await load(startRow, endRow); + } + const rowsThisBlock = dataRef.current + ? dataRef.current.slice(startRow, endRow).map((edge) => edge.node) + : []; + let lastRow = -1; + if (totalCountRef.current !== undefined) { + if (!totalCountRef.current) { + lastRow = 0; + } else { + lastRow = totalCountRef.current; + } + } else if (rowsThisBlock.length < endRow - startRow) { + lastRow = rowsThisBlock.length; + } + successCallback(rowsThisBlock, lastRow); + } catch (e) { + failCallback(); + } + }; + + const onBodyScrollEnd = (event: BodyScrollEndEvent) => { + if (event.top === 0) { + addNewRows(); + } + }; + + const onBodyScroll = (event: BodyScrollEvent) => { + scrolledToTop.current = event.top <= 0; + }; + + // id and onclick is needed only for mocked data + let id = 0; + const onClick = () => { + if (!dataRef.current) { + return; + } + const node = generateFill({ + id: (--id).toString(), + createdAt: new Date(createdAt - 1000 * id).toISOString(), + }); + update({ + data: [ + { cursor: '0', node, __typename: 'TradeEdge' }, + ...dataRef.current, + ], + delta: [node], + }); + }; + + return ( + <> + + + + + + ); +}; + +const PaginationTemplate: Story = (args) => ( + +); + +export const Pagination = PaginationTemplate.bind({}); +Pagination.args = { pagination: true }; + +export const PaginationScroll = PaginationTemplate.bind({}); +PaginationScroll.args = { pagination: false }; + +const InfiniteScrollManager = () => { + const gridRef = useRef(null); + const dataRef = useRef<(Fills_party_tradesConnection_edges | null)[] | null>( + null + ); + const totalCountRef = useRef(undefined); + const newRows = useRef(0); + const scrolledToTop = useRef(true); + + const addNewRows = useCallback(() => { + if (newRows.current === 0) { + return; + } + if (totalCountRef.current !== undefined) { + totalCountRef.current += newRows.current; + } + newRows.current = 0; + if (!gridRef.current?.api) { + return; + } + gridRef.current.api.refreshInfiniteCache(); + }, []); + + const update = useCallback( + ({ + data, + delta, + }: { + data: (Fills_party_tradesConnection_edges | null)[]; + delta: FillsSub_trades[]; + }) => { + if (!gridRef.current?.api) { + return false; + } + if (!scrolledToTop.current) { + const createdAt = dataRef.current?.[0]?.node.createdAt; + if (createdAt) { + newRows.current += delta.filter( + (trade) => trade.createdAt > createdAt + ).length; + } + } + dataRef.current = data; + gridRef.current.api.refreshInfiniteCache(); + return true; + }, + [] + ); + + const insert = useCallback( + ({ + data, + totalCount, + }: { + data: Fills_party_tradesConnection_edges[]; + totalCount?: number; + }) => { + dataRef.current = data; + totalCountRef.current = totalCount; + return true; + }, + [] + ); + + const { data, error, loading, load, totalCount } = useDataProvider({ + insert, + }); + totalCountRef.current = totalCount; + dataRef.current = data; + + const getRows = async ({ + successCallback, + failCallback, + startRow, + endRow, + }: IGetRowsParams) => { + startRow += newRows.current; + endRow += newRows.current; + try { + if (dataRef.current && dataRef.current.indexOf(null) < endRow) { + await load(); + } + const rowsThisBlock = dataRef.current + ? dataRef.current.slice(startRow, endRow).map((edge) => edge?.node) + : []; + let lastRow = -1; + if (totalCountRef.current !== undefined) { + if (!totalCountRef.current) { + lastRow = 0; + } else if (totalCountRef.current <= endRow) { + lastRow = totalCountRef.current; + } + } else if (rowsThisBlock.length < endRow - startRow) { + lastRow = rowsThisBlock.length; + } + successCallback(rowsThisBlock, lastRow); + } catch (e) { + failCallback(); + } + }; + + const onBodyScrollEnd = (event: BodyScrollEndEvent) => { + if (event.top === 0) { + addNewRows(); + } + }; + + const onBodyScroll = (event: BodyScrollEvent) => { + scrolledToTop.current = event.top <= 0; + }; + + // id and onclick is needed only for mocked data + let id = 0; + const onClick = () => { + if (!dataRef.current) { + return; + } + const node = generateFill({ + id: (--id).toString(), + createdAt: new Date(createdAt - 1000 * id).toISOString(), + }); + update({ + data: [ + { cursor: '0', node, __typename: 'TradeEdge' }, + ...dataRef.current, + ], + delta: [node], + }); + }; + + return ( + <> + + + + + + ); +}; + +const InfiniteScrollTemplate: Story> = () => ( + +); + +export const InfiniteScroll = InfiniteScrollTemplate.bind({}); diff --git a/libs/fills/src/lib/fills-table.tsx b/libs/fills/src/lib/fills-table.tsx index 52ad384a2..1f88da586 100644 --- a/libs/fills/src/lib/fills-table.tsx +++ b/libs/fills/src/lib/fills-table.tsx @@ -6,21 +6,32 @@ import { getDateTimeFormat, t, } from '@vegaprotocol/react-helpers'; +import { Side } from '@vegaprotocol/types'; import { AgGridColumn } from 'ag-grid-react'; import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; import { forwardRef } from 'react'; -import type { FillFields } from './__generated__/FillFields'; -import type { ValueFormatterParams, IDatasource } from 'ag-grid-community'; +import type { ValueFormatterParams } from 'ag-grid-community'; import BigNumber from 'bignumber.js'; -import { Side } from '@vegaprotocol/types'; +import type { AgGridReactProps, AgReactUiProps } from 'ag-grid-react'; +import type { + FillFields, + FillFields_market_tradableInstrument_instrument_product, +} from './__generated__/FillFields'; +import type { Fills_party_tradesConnection_edges_node } from './__generated__/Fills'; -export interface FillsTableProps { +export type Props = (AgGridReactProps | AgReactUiProps) & { partyId: string; - datasource: IDatasource; -} +}; -export const FillsTable = forwardRef( - ({ partyId, datasource }, ref) => { +type AccountsTableValueFormatterParams = Omit< + ValueFormatterParams, + 'data' | 'value' +> & { + data: Fills_party_tradesConnection_edges_node | null; +}; + +export const FillsTable = forwardRef( + ({ partyId, ...props }, ref) => { return ( ( defaultColDef={{ flex: 1, resizable: true }} style={{ width: '100%', height: '100%' }} getRowId={({ data }) => data?.id} - rowModelType="infinite" - datasource={datasource} + {...props} > ( { + valueFormatter={({ + value, + }: AccountsTableValueFormatterParams & { + value: Fills_party_tradesConnection_edges_node['createdAt']; + }) => { if (value === undefined) { return value; } @@ -81,9 +95,14 @@ export const FillsTable = forwardRef( } ); -const formatPrice = ({ value, data }: ValueFormatterParams) => { - if (value === undefined) { - return value; +const formatPrice = ({ + value, + data, +}: AccountsTableValueFormatterParams & { + value?: Fills_party_tradesConnection_edges_node['price']; +}) => { + if (value === undefined || !data) { + return undefined; } const asset = data?.market.tradableInstrument.instrument.product.settlementAsset.symbol; @@ -95,9 +114,14 @@ const formatPrice = ({ value, data }: ValueFormatterParams) => { }; const formatSize = (partyId: string) => { - return ({ value, data }: ValueFormatterParams) => { - if (value === undefined) { - return value; + return ({ + value, + data, + }: AccountsTableValueFormatterParams & { + value?: Fills_party_tradesConnection_edges_node['size']; + }) => { + if (value === undefined || !data) { + return undefined; } let prefix; if (data?.buyer.id === partyId) { @@ -114,9 +138,14 @@ const formatSize = (partyId: string) => { }; }; -const formatTotal = ({ value, data }: ValueFormatterParams) => { - if (value === undefined) { - return value; +const formatTotal = ({ + value, + data, +}: AccountsTableValueFormatterParams & { + value?: Fills_party_tradesConnection_edges_node['price']; +}) => { + if (value === undefined || !data) { + return undefined; } const asset = data?.market.tradableInstrument.instrument.product.settlementAsset.symbol; @@ -131,7 +160,12 @@ const formatTotal = ({ value, data }: ValueFormatterParams) => { }; const formatRole = (partyId: string) => { - return ({ value, data }: ValueFormatterParams) => { + return ({ + value, + data, + }: AccountsTableValueFormatterParams & { + value?: Fills_party_tradesConnection_edges_node['aggressor']; + }) => { if (value === undefined) { return value; } @@ -156,7 +190,12 @@ const formatRole = (partyId: string) => { }; const formatFee = (partyId: string) => { - return ({ value, data }: ValueFormatterParams) => { + return ({ + value, + data, + }: AccountsTableValueFormatterParams & { + value?: FillFields_market_tradableInstrument_instrument_product; + }) => { if (value === undefined) { return value; } diff --git a/libs/fills/src/lib/test-helpers.ts b/libs/fills/src/lib/test-helpers.ts index bafd16a9f..3862fcb9a 100644 --- a/libs/fills/src/lib/test-helpers.ts +++ b/libs/fills/src/lib/test-helpers.ts @@ -1,5 +1,4 @@ import merge from 'lodash/merge'; -import type { IGetRowsParams } from 'ag-grid-community'; import type { PartialDeep } from 'type-fest'; import type { Fills, @@ -52,7 +51,6 @@ export const generateFills = (override?: PartialDeep): Fills => { id: 'buyer-id', tradesConnection: { __typename: 'TradeConnection', - totalCount: 1, edges: fills.map((f) => { return { __typename: 'TradeEdge', @@ -64,6 +62,8 @@ export const generateFills = (override?: PartialDeep): Fills => { __typename: 'PageInfo', startCursor: '1', endCursor: '2', + hasNextPage: false, + hasPreviousPage: false, }, }, __typename: 'Party', @@ -79,7 +79,7 @@ export const generateFill = ( const defaultFill: Fills_party_tradesConnection_edges_node = { __typename: 'Trade', id: '0', - createdAt: new Date().toISOString(), + createdAt: '2005-04-02T19:37:00.000Z', price: '10000000', size: '50000', buyOrder: 'buy-order', @@ -133,9 +133,3 @@ export const generateFill = ( return merge(defaultFill, override); }; - -export const makeGetRows = - (data: Fills_party_tradesConnection_edges_node[]) => - ({ successCallback }: IGetRowsParams) => { - successCallback(data, data.length); - }; diff --git a/libs/market-list/src/lib/components/__generated__/MarketDataFields.ts b/libs/market-list/src/lib/components/__generated__/MarketDataFields.ts deleted file mode 100644 index 16906d33b..000000000 --- a/libs/market-list/src/lib/components/__generated__/MarketDataFields.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @generated -// This file was automatically generated and should not be edited. - -import { MarketState, MarketTradingMode } from "@vegaprotocol/types"; - -// ==================================================== -// GraphQL fragment: MarketDataFields -// ==================================================== - -export interface MarketDataFields_market { - __typename: "Market"; - /** - * Market ID - */ - id: string; - /** - * Current state of the market - */ - state: MarketState; - /** - * Current mode of execution of the market - */ - tradingMode: MarketTradingMode; -} - -export interface MarketDataFields { - __typename: "MarketData"; - /** - * market id of the associated mark price - */ - market: MarketDataFields_market; - /** - * the highest price level on an order book for buy orders. - */ - bestBidPrice: string; - /** - * the lowest price level on an order book for offer orders. - */ - bestOfferPrice: string; - /** - * the mark price (actually an unsigned int) - */ - markPrice: string; -} diff --git a/libs/market-list/src/lib/components/__generated__/MarketDataSub.ts b/libs/market-list/src/lib/components/__generated__/MarketDataSub.ts deleted file mode 100644 index a74ce2159..000000000 --- a/libs/market-list/src/lib/components/__generated__/MarketDataSub.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @generated -// This file was automatically generated and should not be edited. - -import { MarketState, MarketTradingMode } from "@vegaprotocol/types"; - -// ==================================================== -// GraphQL subscription operation: MarketDataSub -// ==================================================== - -export interface MarketDataSub_marketData_market { - __typename: "Market"; - /** - * Market ID - */ - id: string; - /** - * Current state of the market - */ - state: MarketState; - /** - * Current mode of execution of the market - */ - tradingMode: MarketTradingMode; -} - -export interface MarketDataSub_marketData { - __typename: "MarketData"; - /** - * market id of the associated mark price - */ - market: MarketDataSub_marketData_market; - /** - * the highest price level on an order book for buy orders. - */ - bestBidPrice: string; - /** - * the lowest price level on an order book for offer orders. - */ - bestOfferPrice: string; - /** - * the mark price (actually an unsigned int) - */ - markPrice: string; -} - -export interface MarketDataSub { - /** - * Subscribe to the mark price changes - */ - marketData: MarketDataSub_marketData; -} diff --git a/libs/market-list/src/lib/components/__generated__/Markets.ts b/libs/market-list/src/lib/components/__generated__/Markets.ts deleted file mode 100644 index 37cc01771..000000000 --- a/libs/market-list/src/lib/components/__generated__/Markets.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @generated -// This file was automatically generated and should not be edited. - -import { MarketState, MarketTradingMode } from "@vegaprotocol/types"; - -// ==================================================== -// GraphQL query operation: Markets -// ==================================================== - -export interface Markets_markets_data_market { - __typename: "Market"; - /** - * Market ID - */ - id: string; - /** - * Current state of the market - */ - state: MarketState; - /** - * Current mode of execution of the market - */ - tradingMode: MarketTradingMode; -} - -export interface Markets_markets_data { - __typename: "MarketData"; - /** - * market id of the associated mark price - */ - market: Markets_markets_data_market; - /** - * the highest price level on an order book for buy orders. - */ - bestBidPrice: string; - /** - * the lowest price level on an order book for offer orders. - */ - bestOfferPrice: string; - /** - * the mark price (actually an unsigned int) - */ - markPrice: string; -} - -export interface Markets_markets_tradableInstrument_instrument_product_settlementAsset { - __typename: "Asset"; - /** - * The symbol of the asset (e.g: GBP) - */ - symbol: string; -} - -export interface Markets_markets_tradableInstrument_instrument_product { - __typename: "Future"; - /** - * The name of the asset (string) - */ - settlementAsset: Markets_markets_tradableInstrument_instrument_product_settlementAsset; -} - -export interface Markets_markets_tradableInstrument_instrument { - __typename: "Instrument"; - /** - * A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string) - */ - code: string; - /** - * A reference to or instance of a fully specified product, including all required product parameters for that product (Product union) - */ - product: Markets_markets_tradableInstrument_instrument_product; -} - -export interface Markets_markets_tradableInstrument { - __typename: "TradableInstrument"; - /** - * An instance of or reference to a fully specified instrument. - */ - instrument: Markets_markets_tradableInstrument_instrument; -} - -export interface Markets_markets { - __typename: "Market"; - /** - * Market ID - */ - id: string; - /** - * Market full name - */ - name: string; - /** - * decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct - * number denominated in the currency of the Market. (uint64) - * - * Examples: - * Currency Balance decimalPlaces Real Balance - * GBP 100 0 GBP 100 - * GBP 100 2 GBP 1.00 - * GBP 100 4 GBP 0.01 - * GBP 1 4 GBP 0.0001 ( 0.01p ) - * - * GBX (pence) 100 0 GBP 1.00 (100p ) - * GBX (pence) 100 2 GBP 0.01 ( 1p ) - * GBX (pence) 100 4 GBP 0.0001 ( 0.01p ) - * GBX (pence) 1 4 GBP 0.000001 ( 0.0001p) - */ - decimalPlaces: number; - /** - * marketData for the given market - */ - data: Markets_markets_data | null; - /** - * An instance of or reference to a tradable instrument. - */ - tradableInstrument: Markets_markets_tradableInstrument; -} - -export interface Markets { - /** - * One or more instruments that are trading on the VEGA network - */ - markets: Markets_markets[] | null; -} diff --git a/libs/market-list/src/lib/components/index.ts b/libs/market-list/src/lib/components/index.ts index 4fe60a8ed..b8ae120b2 100644 --- a/libs/market-list/src/lib/components/index.ts +++ b/libs/market-list/src/lib/components/index.ts @@ -1,3 +1,2 @@ -export * from './__generated__'; export * from './landing'; export * from './markets-container'; diff --git a/libs/market-list/src/lib/components/__generated__/index.ts b/libs/market-list/src/lib/components/markets-container/__generated__/index.ts similarity index 76% rename from libs/market-list/src/lib/components/__generated__/index.ts rename to libs/market-list/src/lib/components/markets-container/__generated__/index.ts index b169fc0c4..807f552b1 100644 --- a/libs/market-list/src/lib/components/__generated__/index.ts +++ b/libs/market-list/src/lib/components/markets-container/__generated__/index.ts @@ -1,3 +1,4 @@ export * from './MarketDataFields'; export * from './MarketDataSub'; +export * from './MarketList'; export * from './Markets'; diff --git a/libs/market-list/src/lib/components/markets-container/index.ts b/libs/market-list/src/lib/components/markets-container/index.ts index 8750972e5..a73a3fc05 100644 --- a/libs/market-list/src/lib/components/markets-container/index.ts +++ b/libs/market-list/src/lib/components/markets-container/index.ts @@ -2,4 +2,4 @@ export * from './market-list-table'; export * from './markets-container'; export * from './markets-data-provider'; export * from './summary-cell'; -export * from './__generated__/MarketList'; +export * from './__generated__'; diff --git a/libs/market-list/src/lib/components/markets-container/market-list-table.tsx b/libs/market-list/src/lib/components/markets-container/market-list-table.tsx index 45cd91a7d..7c3f7b032 100644 --- a/libs/market-list/src/lib/components/markets-container/market-list-table.tsx +++ b/libs/market-list/src/lib/components/markets-container/market-list-table.tsx @@ -1,5 +1,5 @@ import { forwardRef } from 'react'; -import type { IDatasource, ValueFormatterParams } from 'ag-grid-community'; +import type { ValueFormatterParams } from 'ag-grid-community'; import { PriceFlashCell, addDecimalsFormatNumber, @@ -8,95 +8,119 @@ import { } from '@vegaprotocol/react-helpers'; import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; import { AgGridColumn } from 'ag-grid-react'; -import type { AgGridReact } from 'ag-grid-react'; -import type { Markets_markets } from '../__generated__/Markets'; +import type { + AgGridReact, + AgGridReactProps, + AgReactUiProps, +} from 'ag-grid-react'; import { MarketTradingMode, AuctionTrigger } from '@vegaprotocol/types'; +import type { + Markets_markets, + Markets_markets_data, +} from './__generated__/Markets'; -interface MarketListTableProps { - datasource: IDatasource; - onRowClicked: (marketId: string) => void; -} +type Props = AgGridReactProps | AgReactUiProps; -export const MarketListTable = forwardRef( - ({ datasource, onRowClicked }, ref) => { - return ( - data?.id} - ref={ref} - defaultColDef={{ - flex: 1, - resizable: true, +type MarketListTableValueFormatterParams = Omit< + ValueFormatterParams, + 'data' | 'value' +> & { + data: Markets_markets; +}; + +export const MarketListTable = forwardRef((props, ref) => { + return ( + data?.id} + ref={ref} + defaultColDef={{ + flex: 1, + resizable: true, + }} + suppressCellFocus={true} + components={{ PriceFlashCell }} + {...props} + > + + + { + if (!value) return value; + const { market, trigger } = value; + return market && + market.tradingMode === MarketTradingMode.MonitoringAuction && + trigger && + trigger !== AuctionTrigger.Unspecified + ? `${formatLabel(market.tradingMode)} - ${trigger.toLowerCase()}` + : formatLabel(market?.tradingMode); }} - suppressCellFocus={true} - onRowClicked={({ data }: { data: Markets_markets }) => - onRowClicked(data.id) + /> + + value === undefined + ? value + : addDecimalsFormatNumber(value, data.decimalPlaces) } - components={{ PriceFlashCell }} - > - - - { - if (!value) return value; - const { market, trigger } = value; - return market && - market.tradingMode === MarketTradingMode.MonitoringAuction && - trigger && - trigger !== AuctionTrigger.Unspecified - ? `${formatLabel(market.tradingMode)} - ${trigger.toLowerCase()}` - : formatLabel(market?.tradingMode); - }} - /> - - value === undefined - ? value - : addDecimalsFormatNumber(value, data.decimalPlaces) - } - /> - - value === undefined - ? value - : addDecimalsFormatNumber(value, data.decimalPlaces) - } - cellRenderer="PriceFlashCell" - /> - - value === undefined - ? value - : addDecimalsFormatNumber(value, data.decimalPlaces) - } - /> - - - ); - } -); + /> + + value === undefined + ? value + : addDecimalsFormatNumber(value, data.decimalPlaces) + } + cellRenderer="PriceFlashCell" + /> + + value === undefined + ? value + : addDecimalsFormatNumber(value, data.decimalPlaces) + } + /> + + + ); +}); export default MarketListTable; diff --git a/libs/market-list/src/lib/components/markets-container/markets-container.tsx b/libs/market-list/src/lib/components/markets-container/markets-container.tsx index d7f73d8ea..25ae64792 100644 --- a/libs/market-list/src/lib/components/markets-container/markets-container.tsx +++ b/libs/market-list/src/lib/components/markets-container/markets-container.tsx @@ -8,7 +8,7 @@ import type { IGetRowsParams } from 'ag-grid-community'; import type { Markets_markets, Markets_markets_data, -} from '../../components/__generated__/Markets'; +} from './__generated__/Markets'; import { marketsDataProvider as dataProvider } from './markets-data-provider'; import { MarketState } from '@vegaprotocol/types'; @@ -42,13 +42,15 @@ export const MarketsContainer = () => { const lastRow = dataRef.current?.length ?? -1; successCallback(rowsThisBlock, lastRow); }; - return ( push(`/markets/${id}`)} + onRowClicked={({ data }: { data: Markets_markets }) => + push(`/markets/${data.id}`) + } /> ); diff --git a/libs/market-list/src/lib/components/markets-container/markets-data-provider.ts b/libs/market-list/src/lib/components/markets-container/markets-data-provider.ts index 1f699308c..32121c091 100644 --- a/libs/market-list/src/lib/components/markets-container/markets-data-provider.ts +++ b/libs/market-list/src/lib/components/markets-container/markets-data-provider.ts @@ -3,13 +3,10 @@ import { gql } from '@apollo/client'; import type { Markets, Markets_markets, -} from '../../components/__generated__/Markets'; -import { makeDataProvider } from '@vegaprotocol/react-helpers'; - -import type { MarketDataSub, MarketDataSub_marketData, -} from '../../components/__generated__/MarketDataSub'; +} from './'; +import { makeDataProvider } from '@vegaprotocol/react-helpers'; const MARKET_DATA_FRAGMENT = gql` fragment MarketDataFields on MarketData { diff --git a/libs/react-helpers/src/hooks/use-data-provider.ts b/libs/react-helpers/src/hooks/use-data-provider.ts index 5e525ec05..236180fbc 100644 --- a/libs/react-helpers/src/hooks/use-data-provider.ts +++ b/libs/react-helpers/src/hooks/use-data-provider.ts @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { useApolloClient } from '@apollo/client'; import type { OperationVariables } from '@apollo/client'; -import type { Subscribe, Pagination, Load } from '../lib/generic-data-provider'; +import type { Subscribe, Load } from '../lib/generic-data-provider'; /** * @@ -48,9 +48,9 @@ export function useDataProvider({ reloadRef.current(force); } }, []); - const load = useCallback((pagination: Pagination) => { + const load = useCallback>((...args) => { if (loadRef.current) { - return loadRef.current(pagination); + return loadRef.current(...args); } return Promise.reject(); }, []); diff --git a/libs/react-helpers/src/lib/generic-data-provider.spec.ts b/libs/react-helpers/src/lib/generic-data-provider.spec.ts new file mode 100644 index 000000000..0edddc797 --- /dev/null +++ b/libs/react-helpers/src/lib/generic-data-provider.spec.ts @@ -0,0 +1,445 @@ +import { makeDataProvider, defaultAppend } from './generic-data-provider'; +import type { + Query, + UpdateCallback, + Update, + PageInfo, +} from './generic-data-provider'; +import type { + ApolloClient, + FetchResult, + SubscriptionOptions, + OperationVariables, + ApolloQueryResult, + QueryOptions, +} from '@apollo/client'; +import type { Subscription, Observable } from 'zen-observable-ts'; + +type Item = { + cursor: string; + node: { + id: string; + }; +}; +type Data = Item[]; +type QueryData = { + data: Data; + pageInfo?: PageInfo; + totalCount?: number; +}; + +type SubscriptionData = QueryData; +type Delta = Data; + +describe('data provider', () => { + const update = jest.fn< + ReturnType>, + Parameters> + >(); + + const callback = jest.fn< + ReturnType>, + Parameters> + >(); + + const query: Query = { + kind: 'Document', + definitions: [], + }; + const subscriptionQuery: Query = query; + + const subscribe = makeDataProvider( + query, + subscriptionQuery, + update, + (r) => r.data, + (r) => r.data + ); + + const first = 100; + const paginatedSubscribe = makeDataProvider< + QueryData, + Data, + SubscriptionData, + Delta + >( + query, + subscriptionQuery, + update, + (r) => r.data, + (r) => r.data, + { + first, + append: defaultAppend, + getPageInfo: (r) => r?.pageInfo ?? null, + getTotalCount: (r) => r?.totalCount, + } + ); + + const generateData = (start = 0, size = first) => { + return new Array(size).fill(null).map((v, i) => ({ + cursor: (i + start + 1).toString(), + node: { + id: (i + start + 1).toString(), + }, + })); + }; + + const clientSubscribeUnsubscribe = jest.fn(); + const clientSubscribeSubscribe = jest.fn< + Subscription, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [(value: FetchResult) => void, (error: any) => void] + >(() => ({ + unsubscribe: clientSubscribeUnsubscribe, + closed: false, + })); + + const clientSubscribe = jest.fn< + Observable>, + [SubscriptionOptions] + >( + () => + ({ + subscribe: clientSubscribeSubscribe, + } as unknown as Observable>) + ); + + const clientQueryPromise: { + resolve?: ( + value: + | ApolloQueryResult + | PromiseLike> + ) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reject?: (reason?: any) => void; + } = {}; + + const clientQuery = jest.fn< + Promise>, + [QueryOptions] + >(() => { + return new Promise((resolve, reject) => { + clientQueryPromise.resolve = resolve; + clientQueryPromise.reject = reject; + }); + }); + + const client = { + query: clientQuery, + subscribe: clientSubscribe, + } as unknown as ApolloClient; + + const resolveQuery = async (data: QueryData) => { + if (clientQueryPromise.resolve) { + await clientQueryPromise.resolve({ + data, + loading: false, + networkStatus: 8, + }); + } + }; + + it('memoize instance and unsubscribe if no subscribers', () => { + const subscription1 = subscribe(jest.fn(), client); + const subscription2 = subscribe(jest.fn(), client); + expect(clientSubscribeSubscribe.mock.calls.length).toEqual(1); + subscription1.unsubscribe(); + expect(clientSubscribeUnsubscribe.mock.calls.length).toEqual(0); + subscription2.unsubscribe(); + expect(clientSubscribeUnsubscribe.mock.calls.length).toEqual(1); + }); + + it('calls callback before and after initial fetch', async () => { + callback.mockClear(); + const data: Item[] = []; + const subscription = subscribe(callback, client); + expect(callback.mock.calls.length).toBe(1); + expect(callback.mock.calls[0][0].data).toBe(null); + expect(callback.mock.calls[0][0].loading).toBe(true); + await resolveQuery({ data }); + expect(callback.mock.calls.length).toBe(2); + expect(callback.mock.calls[1][0].data).toBe(data); + expect(callback.mock.calls[1][0].loading).toBe(false); + subscription.unsubscribe(); + }); + + it('calls update and callback on each update', async () => { + const data: Item[] = []; + const subscription = subscribe(callback, client); + await resolveQuery({ data }); + const delta: Item[] = []; + update.mockImplementationOnce((data, delta) => [...data, ...delta]); + // calling onNext from client.subscribe({ query }).subscribe(onNext) + await clientSubscribeSubscribe.mock.calls[ + clientSubscribeSubscribe.mock.calls.length - 1 + ][0]({ data: { data: delta } }); + expect(update.mock.calls[update.mock.calls.length - 1][0]).toBe(data); + expect(update.mock.calls[update.mock.calls.length - 1][1]).toBe(delta); + expect(callback.mock.calls[callback.mock.calls.length - 1][0].delta).toBe( + delta + ); + subscription.unsubscribe(); + }); + + it("don't calls callback on update if data doesn't", async () => { + callback.mockClear(); + const data: Item[] = []; + const subscription = subscribe(callback, client); + await resolveQuery({ data }); + const delta: Item[] = []; + update.mockImplementationOnce((data, delta) => data); + const callbackCallsLength = callback.mock.calls.length; + // calling onNext from client.subscribe({ query }).subscribe(onNext) + await clientSubscribeSubscribe.mock.calls[ + clientSubscribeSubscribe.mock.calls.length - 1 + ][0]({ data: { data: delta } }); + expect(update.mock.calls[update.mock.calls.length - 1][0]).toBe(data); + expect(update.mock.calls[update.mock.calls.length - 1][1]).toBe(delta); + expect(callback.mock.calls.length).toBe(callbackCallsLength); + subscription.unsubscribe(); + }); + + it('refetch data on reload', async () => { + clientQuery.mockClear(); + clientSubscribeUnsubscribe.mockClear(); + clientSubscribeSubscribe.mockClear(); + const data: Item[] = []; + const subscription = subscribe(callback, client); + await resolveQuery({ data }); + subscription.reload(); + await resolveQuery({ data }); + expect(clientQuery.mock.calls.length).toBe(2); + expect(clientSubscribeSubscribe.mock.calls.length).toBe(1); + expect(clientSubscribeUnsubscribe.mock.calls.length).toBe(0); + subscription.unsubscribe(); + }); + + it('refetch data and restart subscription on reload with force', async () => { + clientQuery.mockClear(); + clientSubscribeUnsubscribe.mockClear(); + clientSubscribeSubscribe.mockClear(); + const data: Item[] = []; + const subscription = subscribe(callback, client); + await resolveQuery({ data }); + subscription.reload(true); + await resolveQuery({ data }); + expect(clientQuery.mock.calls.length).toBe(2); + expect(clientSubscribeSubscribe.mock.calls.length).toBe(2); + expect(clientSubscribeUnsubscribe.mock.calls.length).toBe(1); + subscription.unsubscribe(); + }); + + it('calls callback on flush', async () => { + callback.mockClear(); + const data: Item[] = []; + const subscription = subscribe(callback, client); + await resolveQuery({ data }); + const callbackCallsLength = callback.mock.calls.length; + subscription.flush(); + expect(callback.mock.calls.length).toBe(callbackCallsLength + 1); + subscription.unsubscribe(); + }); + + it('fills data with nulls if paginaton is enabled', async () => { + callback.mockClear(); + const totalCount = 1000; + const data: Item[] = new Array(first).fill(null).map((v, i) => ({ + cursor: i.toString(), + node: { + id: i.toString(), + }, + })); + const subscription = paginatedSubscribe(callback, client); + await resolveQuery({ + data, + totalCount, + pageInfo: { + hasNextPage: true, + }, + }); + expect(callback.mock.calls[1][0].data?.length).toBe(totalCount); + subscription.unsubscribe(); + }); + + it('loads requested data blocks and inserts data with total count', async () => { + callback.mockClear(); + const totalCount = 1000; + const subscription = paginatedSubscribe(callback, client); + await resolveQuery({ + data: generateData(), + totalCount, + pageInfo: { + hasNextPage: true, + endCursor: '100', + }, + }); + + // load next page + subscription.load(); + let lastQueryArgs = + clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; + expect(lastQueryArgs?.variables?.pagination).toEqual({ + after: '100', + first, + }); + await resolveQuery({ + data: generateData(100), + pageInfo: { + hasNextPage: true, + endCursor: '200', + }, + }); + + // load page with skip + subscription.load(500, 600); + lastQueryArgs = + clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; + expect(lastQueryArgs?.variables?.pagination).toEqual({ + after: '200', + first, + skip: 300, + }); + await resolveQuery({ + data: generateData(500), + pageInfo: { + hasNextPage: true, + endCursor: '600', + }, + }); + + // load in the gap + subscription.load(400, 500); + lastQueryArgs = + clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; + expect(lastQueryArgs?.variables?.pagination).toEqual({ + after: '200', + first, + skip: 200, + }); + await resolveQuery({ + data: generateData(400), + pageInfo: { + hasNextPage: true, + endCursor: '500', + }, + }); + + // load page after last block + subscription.load(700, 800); + lastQueryArgs = + clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; + expect(lastQueryArgs?.variables?.pagination).toEqual({ + after: '600', + first, + skip: 100, + }); + await resolveQuery({ + data: generateData(700), + pageInfo: { + hasNextPage: true, + endCursor: '800', + }, + }); + + // load last page shorter than expected + subscription.load(950, 1050); + lastQueryArgs = + clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; + expect(lastQueryArgs?.variables?.pagination).toEqual({ + after: '800', + first, + skip: 150, + }); + await resolveQuery({ + data: generateData(950, 20), + pageInfo: { + hasNextPage: false, + endCursor: '970', + }, + }); + let lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1]; + expect(lastCallbackArgs[0].totalCount).toBe(970); + + // load next page when pageInfo.hasNextPage === false + const clientQueryCallsLength = clientQuery.mock.calls.length; + subscription.load(); + expect(clientQuery.mock.calls.length).toBe(clientQueryCallsLength); + + // load last page longer than expected + subscription.load(960, 1000); + lastQueryArgs = + clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; + expect(lastQueryArgs?.variables?.pagination).toEqual({ + after: '960', + first, + }); + await resolveQuery({ + data: generateData(960, 40), + pageInfo: { + hasNextPage: true, + endCursor: '1000', + }, + }); + lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1]; + expect(lastCallbackArgs[0].totalCount).toBe(1000); + + subscription.unsubscribe(); + }); + + it('loads requested data blocks and inserts data without totalCount', async () => { + callback.mockClear(); + const totalCount = undefined; + const subscription = paginatedSubscribe(callback, client); + await resolveQuery({ + data: generateData(), + totalCount, + pageInfo: { + hasNextPage: true, + endCursor: '100', + }, + }); + let lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1]; + expect(lastCallbackArgs[0].totalCount).toBe(undefined); + + // load next page + subscription.load(); + await resolveQuery({ + data: generateData(100), + pageInfo: { + hasNextPage: true, + endCursor: '200', + }, + }); + lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1]; + expect(lastCallbackArgs[0].totalCount).toBe(undefined); + + // load last page + subscription.load(); + await resolveQuery({ + data: generateData(200, 50), + pageInfo: { + hasNextPage: false, + endCursor: '250', + }, + }); + lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1]; + expect(lastCallbackArgs[0].totalCount).toBe(250); + subscription.unsubscribe(); + }); + + it('sets total count when first page has no next page', async () => { + const subscription = paginatedSubscribe(callback, client); + await resolveQuery({ + data: generateData(), + pageInfo: { + hasNextPage: false, + endCursor: '100', + }, + }); + const lastCallbackArgs = + callback.mock.calls[callback.mock.calls.length - 1]; + expect(lastCallbackArgs[0].totalCount).toBe(100); + subscription.unsubscribe(); + }); +}); diff --git a/libs/react-helpers/src/lib/generic-data-provider.ts b/libs/react-helpers/src/lib/generic-data-provider.ts index d7601e257..0ea74dce1 100644 --- a/libs/react-helpers/src/lib/generic-data-provider.ts +++ b/libs/react-helpers/src/lib/generic-data-provider.ts @@ -7,6 +7,7 @@ import type { } from '@apollo/client'; import type { Subscription } from 'zen-observable-ts'; import isEqual from 'lodash/isEqual'; +import type { Pagination as PaginationWithoutSkip } from '@vegaprotocol/types'; export interface UpdateCallback { (arg: { @@ -21,15 +22,12 @@ export interface UpdateCallback { } export interface Load { - (pagination: Pagination): Promise; + (start?: number, end?: number): Promise; } -export interface Pagination { - first?: number; - after?: string; - last?: number; - before?: string; -} +type Pagination = PaginationWithoutSkip & { + skip?: number; +}; export interface PageInfo { startCursor?: string; @@ -51,7 +49,7 @@ export interface Subscribe { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -type Query = DocumentNode | TypedDocumentNode; +export type Query = DocumentNode | TypedDocumentNode; export interface Update { (data: Data, delta: Delta, reload: (forceReset?: boolean) => void): Data; @@ -60,13 +58,13 @@ export interface Update { export interface Append { ( data: Data | null, - pageInfo: PageInfo, insertionData: Data | null, insertionPageInfo: PageInfo | null, - pagination?: Pagination + pagination?: Pagination, + totalCount?: number ): { data: Data | null; - pageInfo: PageInfo; + totalCount?: number; }; } @@ -86,6 +84,46 @@ interface GetDelta { (subscriptionData: SubscriptionData): Delta; } +export function defaultAppend( + data: Data | null, + insertionData: Data | null, + insertionPageInfo: PageInfo | null, + pagination?: Pagination, + totalCount?: number +) { + if (data && insertionData && insertionPageInfo) { + if (!(data instanceof Array) || !(insertionData instanceof Array)) { + throw new Error( + 'data needs to be instance of { cursor: string }[] when using pagination' + ); + } + if (pagination?.after) { + const cursors = data.map((item) => item && item.cursor); + const startIndex = cursors.lastIndexOf(pagination.after); + if (startIndex !== -1) { + const start = startIndex + 1 + (pagination.skip ?? 0); + const end = start + insertionData.length; + let updatedData = [ + ...data.slice(0, start), + ...insertionData, + ...data.slice(end), + ]; + if (!insertionPageInfo.hasNextPage && end !== (totalCount ?? 0)) { + // adjust totalCount if last page is shorter or longer than expected + totalCount = end; + updatedData = updatedData.slice(0, end); + } + return { + data: updatedData, + // increase totalCount if last page is longer than expected + totalCount: totalCount && Math.max(updatedData.length, totalCount), + }; + } + } + } + return { data, totalCount }; +} + /** * @param subscriptionQuery query that will be used for subscription * @param update function that will be execued on each onNext, it should update data base on delta, it can reload data provider @@ -102,7 +140,7 @@ function makeDataProviderInternal( getDelta: GetDelta, pagination?: { getPageInfo: GetPageInfo; - getTotalCount: GetTotalCount; + getTotalCount?: GetTotalCount; append: Append; first: number; }, @@ -125,7 +163,7 @@ function makeDataProviderInternal( // notify single callback about current state, delta is passes optionally only if notify was invoked onNext const notify = ( callback: UpdateCallback, - dataUpdate?: { delta?: Delta; insertionData?: Data | null } + updateData?: { delta?: Delta; insertionData?: Data | null } ) => { callback({ data, @@ -133,26 +171,50 @@ function makeDataProviderInternal( loading, pageInfo, totalCount, - ...dataUpdate, + ...updateData, }); }; // notify all callbacks - const notifyAll = (dataUpdate?: { + const notifyAll = (updateData?: { delta?: Delta; insertionData?: Data | null; }) => { - callbacks.forEach((callback) => notify(callback, dataUpdate)); + callbacks.forEach((callback) => notify(callback, updateData)); }; - const load = async (params?: Pagination) => { - if (!client || !pagination || !pageInfo) { + const load = async (start?: number, end?: number) => { + if (!client || !pagination || !pageInfo || !(data instanceof Array)) { return Promise.reject(); } - const paginationVariables: Pagination = params ?? { + const paginationVariables: Pagination = { first: pagination.first, after: pageInfo.endCursor, }; + if (start !== undefined) { + if (!start) { + paginationVariables.after = undefined; + } else if (data && data[start - 1]) { + paginationVariables.after = ( + data[start - 1] as { cursor: string } + ).cursor; + } else { + let skip = 1; + while (!data[start - 1 - skip] && skip <= start) { + skip += 1; + } + paginationVariables.skip = skip; + if (skip === start) { + paginationVariables.after = undefined; + } else { + paginationVariables.after = ( + data[start - 1 - skip] as { cursor: string } + ).cursor; + } + } + } else if (!pageInfo.hasNextPage) { + return null; + } const res = await client.query({ query, variables: { @@ -162,15 +224,18 @@ function makeDataProviderInternal( fetchPolicy, }); const insertionData = getData(res.data); - const insertionDataPageInfo = pagination.getPageInfo(res.data); - ({ data, pageInfo } = pagination.append( + const insertionPageInfo = pagination.getPageInfo(res.data); + ({ data, totalCount } = pagination.append( data, - pageInfo, insertionData, - insertionDataPageInfo, - paginationVariables + insertionPageInfo, + paginationVariables, + totalCount )); - totalCount = pagination.getTotalCount(res.data); + pageInfo = insertionPageInfo; + totalCount = + (pagination.getTotalCount && pagination.getTotalCount(res.data)) ?? + totalCount; notifyAll({ insertionData }); return insertionData; }; @@ -188,9 +253,23 @@ function makeDataProviderInternal( fetchPolicy, }); data = getData(res.data); - if (pagination) { + if (data && pagination) { + if (!(data instanceof Array)) { + throw new Error( + 'data needs to be instance of { cursor: string }[] when using pagination' + ); + } pageInfo = pagination.getPageInfo(res.data); - totalCount = pagination.getTotalCount(res.data); + if (pageInfo && !pageInfo.hasNextPage) { + totalCount = data.length; + } else { + totalCount = + pagination.getTotalCount && pagination.getTotalCount(res.data); + } + + if (data && totalCount && data.length < totalCount) { + data.push(...new Array(totalCount - data.length).fill(null)); + } } // if there was some updates received from subscription during initial query loading apply them on just received data if (data && updateQueue && updateQueue.length > 0) { @@ -255,11 +334,11 @@ function makeDataProviderInternal( if (loading || !data) { updateQueue.push(delta); } else { - const newData = update(data, delta, reload); - if (newData === data) { + const updatedData = update(data, delta, reload); + if (updatedData === data) { return; } - data = newData; + data = updatedData; notifyAll({ delta }); } }, @@ -361,7 +440,7 @@ export function makeDataProvider( getDelta: GetDelta, pagination?: { getPageInfo: GetPageInfo; - getTotalCount: GetTotalCount; + getTotalCount?: GetTotalCount; append: Append; first: number; }, diff --git a/libs/trades/src/lib/__generated__/Trades.ts b/libs/trades/src/lib/__generated__/Trades.ts index e7bd4e8a8..b20eb7243 100644 --- a/libs/trades/src/lib/__generated__/Trades.ts +++ b/libs/trades/src/lib/__generated__/Trades.ts @@ -3,11 +3,13 @@ // @generated // This file was automatically generated and should not be edited. +import { Pagination } from "@vegaprotocol/types"; + // ==================================================== // GraphQL query operation: Trades // ==================================================== -export interface Trades_market_trades_market { +export interface Trades_market_tradesConnection_edges_node_market { __typename: "Market"; /** * Market ID @@ -38,7 +40,7 @@ export interface Trades_market_trades_market { positionDecimalPlaces: number; } -export interface Trades_market_trades { +export interface Trades_market_tradesConnection_edges_node { __typename: "Trade"; /** * The hash of the trade data @@ -59,7 +61,33 @@ export interface Trades_market_trades { /** * The market the trade occurred on */ - market: Trades_market_trades_market; + market: Trades_market_tradesConnection_edges_node_market; +} + +export interface Trades_market_tradesConnection_edges { + __typename: "TradeEdge"; + node: Trades_market_tradesConnection_edges_node; + cursor: string; +} + +export interface Trades_market_tradesConnection_pageInfo { + __typename: "PageInfo"; + startCursor: string; + endCursor: string; + hasNextPage: boolean; + hasPreviousPage: boolean; +} + +export interface Trades_market_tradesConnection { + __typename: "TradeConnection"; + /** + * The trade in this connection + */ + edges: Trades_market_tradesConnection_edges[]; + /** + * The pagination information + */ + pageInfo: Trades_market_tradesConnection_pageInfo; } export interface Trades_market { @@ -68,10 +96,7 @@ export interface Trades_market { * Market ID */ id: string; - /** - * Trades on a market - */ - trades: Trades_market_trades[] | null; + tradesConnection: Trades_market_tradesConnection; } export interface Trades { @@ -83,5 +108,5 @@ export interface Trades { export interface TradesVariables { marketId: string; - maxTrades: number; + pagination?: Pagination | null; } diff --git a/libs/trades/src/lib/trades-container.tsx b/libs/trades/src/lib/trades-container.tsx index 83d0a2da1..0a49b59c9 100644 --- a/libs/trades/src/lib/trades-container.tsx +++ b/libs/trades/src/lib/trades-container.tsx @@ -1,16 +1,22 @@ import { useDataProvider } from '@vegaprotocol/react-helpers'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; -import type { GridApi } from 'ag-grid-community'; import type { AgGridReact } from 'ag-grid-react'; import { useCallback, useMemo, useRef } from 'react'; +import type { + IGetRowsParams, + BodyScrollEvent, + BodyScrollEndEvent, +} from 'ag-grid-community'; import { MAX_TRADES, - sortTrades, tradesDataProvider as dataProvider, } from './trades-data-provider'; import { TradesTable } from './trades-table'; import type { TradeFields } from './__generated__/TradeFields'; -import type { TradesVariables } from './__generated__/Trades'; +import type { + TradesVariables, + Trades_market_tradesConnection_edges, +} from './__generated__/Trades'; interface TradesContainerProps { marketId: string; @@ -18,51 +24,132 @@ interface TradesContainerProps { export const TradesContainer = ({ marketId }: TradesContainerProps) => { const gridRef = useRef(null); + const dataRef = useRef< + (Trades_market_tradesConnection_edges | null)[] | null + >(null); + const totalCountRef = useRef(undefined); + const newRows = useRef(0); + const scrolledToTop = useRef(true); + const variables = useMemo( () => ({ marketId, maxTrades: MAX_TRADES }), [marketId] ); - const update = useCallback(({ delta }: { delta: TradeFields[] }) => { - if (!gridRef.current?.api) { - return false; + + const addNewRows = useCallback(() => { + if (newRows.current === 0) { + return; } - - const incoming = sortTrades(delta); - const currentRows = getAllRows(gridRef.current.api); - // Create array of trades whose index is now greater than the max so we - // can remove them from the grid - const outgoing = [...incoming, ...currentRows].filter( - (r, i) => i > MAX_TRADES - 1 - ); - - gridRef.current.api.applyTransactionAsync({ - add: incoming, - remove: outgoing, - addIndex: 0, - }); - - return true; + if (totalCountRef.current !== undefined) { + totalCountRef.current += newRows.current; + } + newRows.current = 0; + if (!gridRef.current?.api) { + return; + } + gridRef.current.api.refreshInfiniteCache(); }, []); - const { data, error, loading } = useDataProvider({ + + const update = useCallback( + ({ + data, + delta, + }: { + data: (Trades_market_tradesConnection_edges | null)[]; + delta: TradeFields[]; + }) => { + if (!gridRef.current?.api) { + return false; + } + if (!scrolledToTop.current) { + const createdAt = dataRef.current?.[0]?.node.createdAt; + if (createdAt) { + newRows.current += delta.filter( + (trade) => trade.createdAt > createdAt + ).length; + } + } + dataRef.current = data; + gridRef.current.api.refreshInfiniteCache(); + return true; + }, + [] + ); + + const insert = useCallback( + ({ + data, + totalCount, + }: { + data: (Trades_market_tradesConnection_edges | null)[]; + totalCount?: number; + }) => { + dataRef.current = data; + totalCountRef.current = totalCount; + return true; + }, + [] + ); + + const { data, error, loading, load, totalCount } = useDataProvider({ dataProvider, update, + insert, variables, }); + totalCountRef.current = totalCount; + dataRef.current = data; + + const getRows = async ({ + successCallback, + failCallback, + startRow, + endRow, + }: IGetRowsParams) => { + startRow += newRows.current; + endRow += newRows.current; + try { + if (dataRef.current && dataRef.current.indexOf(null) < endRow) { + await load(); + } + const rowsThisBlock = dataRef.current + ? dataRef.current.slice(startRow, endRow).map((edge) => edge?.node) + : []; + let lastRow = -1; + if (totalCountRef.current !== undefined) { + if (!totalCountRef.current) { + lastRow = 0; + } else if (totalCountRef.current <= endRow) { + lastRow = totalCountRef.current; + } + } else if (rowsThisBlock.length < endRow - startRow) { + lastRow = rowsThisBlock.length; + } + successCallback(rowsThisBlock, lastRow); + } catch (e) { + failCallback(); + } + }; + + const onBodyScrollEnd = (event: BodyScrollEndEvent) => { + if (event.top === 0) { + addNewRows(); + } + }; + + const onBodyScroll = (event: BodyScrollEvent) => { + scrolledToTop.current = event.top <= 0; + }; return ( - } - /> + + + ); }; - -const getAllRows = (api: GridApi) => { - const rows: TradeFields[] = []; - api.forEachNode((node) => { - rows.push(node.data); - }); - return rows; -}; diff --git a/libs/trades/src/lib/trades-data-provider.ts b/libs/trades/src/lib/trades-data-provider.ts index a2f684165..8e6e5f116 100644 --- a/libs/trades/src/lib/trades-data-provider.ts +++ b/libs/trades/src/lib/trades-data-provider.ts @@ -1,7 +1,15 @@ import { gql } from '@apollo/client'; -import { makeDataProvider } from '@vegaprotocol/react-helpers'; +import { + makeDataProvider, + defaultAppend as append, +} from '@vegaprotocol/react-helpers'; +import type { PageInfo } from '@vegaprotocol/react-helpers'; import type { TradeFields } from './__generated__/TradeFields'; -import type { Trades } from './__generated__/Trades'; +import type { + Trades, + Trades_market_tradesConnection_edges, + Trades_market_tradesConnection_edges_node, +} from './__generated__/Trades'; import type { TradesSub } from './__generated__/TradesSub'; import orderBy from 'lodash/orderBy'; import produce from 'immer'; @@ -24,11 +32,22 @@ const TRADES_FRAGMENT = gql` export const TRADES_QUERY = gql` ${TRADES_FRAGMENT} - query Trades($marketId: ID!, $maxTrades: Int!) { + query Trades($marketId: ID!, $pagination: Pagination) { market(id: $marketId) { id - trades(last: $maxTrades) { - ...TradeFields + tradesConnection(pagination: $pagination) { + edges { + node { + ...TradeFields + } + cursor + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } } } } @@ -43,40 +62,50 @@ export const TRADES_SUB = gql` } `; -export const sortTrades = (trades: TradeFields[]) => { - return orderBy( - trades, - (t) => { - return new Date(t.createdAt).getTime(); - }, - 'desc' - ); -}; - -const update = (data: TradeFields[], delta: TradeFields[]) => { +const update = ( + data: (Trades_market_tradesConnection_edges | null)[], + delta: TradeFields[] +) => { return produce(data, (draft) => { - const incoming = sortTrades(delta); - - // Add new trades to the top - draft.unshift(...incoming); - - // Remove old trades from the bottom - if (draft.length > MAX_TRADES) { - draft.splice(MAX_TRADES, draft.length - MAX_TRADES); - } + orderBy(delta, 'createdAt', 'desc').forEach((node) => { + const index = draft.findIndex((edge) => edge?.node.id === node.id); + if (index !== -1) { + if (draft[index]?.node) { + Object.assign( + draft[index]?.node as Trades_market_tradesConnection_edges_node, + node + ); + } + } else { + const firstNode = draft[0]?.node; + if (firstNode && node.createdAt >= firstNode.createdAt) { + draft.unshift({ node, cursor: '', __typename: 'TradeEdge' }); + } + } + }); }); }; -const getData = (responseData: Trades): TradeFields[] | null => - responseData.market ? responseData.market.trades : null; +const getData = ( + responseData: Trades +): Trades_market_tradesConnection_edges[] | null => + responseData.market ? responseData.market.tradesConnection.edges : null; const getDelta = (subscriptionData: TradesSub): TradeFields[] => subscriptionData?.trades || []; +const getPageInfo = (responseData: Trades): PageInfo | null => + responseData.market?.tradesConnection.pageInfo || null; + export const tradesDataProvider = makeDataProvider( TRADES_QUERY, TRADES_SUB, update, getData, - getDelta + getDelta, + { + getPageInfo, + append, + first: 100, + } ); diff --git a/libs/trades/src/lib/trades-table.spec.tsx b/libs/trades/src/lib/trades-table.spec.tsx index e4f566179..f27f20593 100644 --- a/libs/trades/src/lib/trades-table.spec.tsx +++ b/libs/trades/src/lib/trades-table.spec.tsx @@ -19,7 +19,7 @@ const trade: TradeFields = { it('Correct columns are rendered', async () => { await act(async () => { - render(); + render(); }); const expectedHeaders = ['Price', 'Size', 'Created at']; const headers = screen.getAllByRole('columnheader'); @@ -29,7 +29,7 @@ it('Correct columns are rendered', async () => { it('Number and data columns are formatted', async () => { await act(async () => { - render(); + render(); }); const cells = screen.getAllByRole('gridcell'); @@ -51,7 +51,7 @@ it('Price and size columns are formatted', async () => { size: (Number(trade.size) - 10).toString(), }; await act(async () => { - render(); + render(); }); const cells = screen.getAllByRole('gridcell'); diff --git a/libs/trades/src/lib/trades-table.tsx b/libs/trades/src/lib/trades-table.tsx index 24942f798..af9dbda1f 100644 --- a/libs/trades/src/lib/trades-table.tsx +++ b/libs/trades/src/lib/trades-table.tsx @@ -1,8 +1,7 @@ import type { AgGridReact } from 'ag-grid-react'; import { AgGridColumn } from 'ag-grid-react'; -import { forwardRef, useMemo } from 'react'; +import { forwardRef } from 'react'; import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; -import type { TradeFields } from './__generated__/TradeFields'; import { addDecimal, addDecimalsFormatNumber, @@ -10,8 +9,9 @@ import { t, } from '@vegaprotocol/react-helpers'; import type { CellClassParams, ValueFormatterParams } from 'ag-grid-community'; +import type { AgGridReactProps, AgReactUiProps } from 'ag-grid-react'; +import type { Trades_market_tradesConnection_edges_node } from './__generated__/Trades'; import BigNumber from 'bignumber.js'; -import { sortTrades } from './trades-data-provider'; export const UP_CLASS = 'text-vega-green'; export const DOWN_CLASS = 'text-vega-red'; @@ -37,58 +37,72 @@ const changeCellClass = return ['font-mono', colorClass].join(' '); }; -interface TradesTableProps { - data: TradeFields[] | null; -} +type Props = AgGridReactProps | AgReactUiProps; +type TradesTableValueFormatterParams = Omit< + ValueFormatterParams, + 'data' | 'value' +> & { + data: Trades_market_tradesConnection_edges_node | null; +}; -export const TradesTable = forwardRef( - ({ data }, ref) => { - // Sort initial trades - const trades = useMemo(() => { - if (!data) { - return null; - } - return sortTrades(data); - }, [data]); - - return ( - data.id} - ref={ref} - defaultColDef={{ - resizable: true, +export const TradesTable = forwardRef((props, ref) => { + return ( + data.id} + ref={ref} + defaultColDef={{ + resizable: true, + }} + {...props} + > + { + if (!data?.market) { + return null; + } + return addDecimalsFormatNumber(value, data.market.decimalPlaces); }} - > - { - return addDecimalsFormatNumber(value, data.market.decimalPlaces); - }} - /> - { - return addDecimal(value, data.market.positionDecimalPlaces); - }} - cellClass={changeCellClass('size')} - /> - { - return getDateTimeFormat().format(new Date(value)); - }} - /> - - ); - } -); + /> + { + if (!data?.market) { + return null; + } + return addDecimal(value, data.market.positionDecimalPlaces); + }} + cellClass={changeCellClass('size')} + /> + { + return value && getDateTimeFormat().format(new Date(value)); + }} + /> + + ); +}); diff --git a/package.json b/package.json index 3655e6b7a..adec06216 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "start": "nx serve", "build": "nx build", "test": "nx test", - "postinstall": "husky install && yarn tsc -b tools/executors/**" + "postinstall": "husky install && yarn tsc -b tools/executors/next && yarn tsc -b tools/executors/webpack" }, "engines": { "node": ">=16.14.0"