From b88fda787c9ce89ce7b7d15e7c7603c22a885e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Thu, 21 Jul 2022 15:25:37 +0200 Subject: [PATCH] Feature/373 pagination of orders (#809) * feat(#373): switch orders to infinite scroll mode * feat(#373): fix orders tests * feat(#218): improve typing in order-list * feat(#373): add generic getRows for infinite rowModelType --- .../src/integration/trading-orders.cy.ts | 4 +- .../src/support/mocks/generate-orders.ts | 37 +++- libs/fills/src/lib/fills-manager.tsx | 47 ++--- libs/fills/src/lib/fills-table.stories.tsx | 84 ++------- .../components/__generated__/OrderFields.ts | 121 ------------ .../lib/components/__generated__/OrderSub.ts | 132 ------------- .../lib/components/__generated__/Orders.ts | 144 -------------- libs/orders/src/lib/components/index.ts | 2 - .../lib/components/mocks/generate-orders.ts | 177 ++++++++---------- .../__generated__/Orders.ts | 45 ++++- .../__generated__/index.ts | 0 .../components/order-data-provider/index.ts | 1 + .../order-data-provider.spec.ts | 135 ++++++------- .../order-data-provider.ts | 102 ++++++---- .../order-list-manager.spec.tsx | 18 +- .../order-list-manager/order-list-manager.tsx | 143 +++++++++----- .../components/order-list/order-list.spec.tsx | 9 +- .../order-list/order-list.stories.tsx | 4 +- .../lib/components/order-list/order-list.tsx | 123 ++++++++---- libs/react-helpers/src/index.ts | 3 +- libs/react-helpers/src/lib/pagination.ts | 53 ++++++ libs/trades/src/lib/trades-container.tsx | 48 ++--- 22 files changed, 562 insertions(+), 870 deletions(-) delete mode 100644 libs/orders/src/lib/components/__generated__/OrderFields.ts delete mode 100644 libs/orders/src/lib/components/__generated__/OrderSub.ts delete mode 100644 libs/orders/src/lib/components/__generated__/Orders.ts rename libs/orders/src/lib/components/{ => order-data-provider}/__generated__/index.ts (100%) create mode 100644 libs/react-helpers/src/lib/pagination.ts diff --git a/apps/trading-e2e/src/integration/trading-orders.cy.ts b/apps/trading-e2e/src/integration/trading-orders.cy.ts index 00fae1633..06aaf46f6 100644 --- a/apps/trading-e2e/src/integration/trading-orders.cy.ts +++ b/apps/trading-e2e/src/integration/trading-orders.cy.ts @@ -70,10 +70,10 @@ describe('orders', () => { it('orders are sorted by most recent order', () => { const expectedOrderList = [ - 'UNIDAI.MF21', + 'AAVEDAI.MF21', 'TSLA.QM21', 'BTCUSD.MF21', - 'AAVEDAI.MF21', + 'UNIDAI.MF21', ]; cy.getByTestId('tab-orders') diff --git a/apps/trading-e2e/src/support/mocks/generate-orders.ts b/apps/trading-e2e/src/support/mocks/generate-orders.ts index 5e2c2c814..1340f7f11 100644 --- a/apps/trading-e2e/src/support/mocks/generate-orders.ts +++ b/apps/trading-e2e/src/support/mocks/generate-orders.ts @@ -1,6 +1,9 @@ import merge from 'lodash/merge'; import type { PartialDeep } from 'type-fest'; -import type { Orders, Orders_party_orders } from '@vegaprotocol/orders'; +import type { + Orders, + Orders_party_ordersConnection_edges_node, +} from '@vegaprotocol/orders'; import { OrderStatus, OrderTimeInForce, @@ -9,7 +12,7 @@ import { } from '@vegaprotocol/types'; export const generateOrders = (override?: PartialDeep): Orders => { - const orders: Orders_party_orders[] = [ + const orders: Orders_party_ordersConnection_edges_node[] = [ { __typename: 'Order', id: '066468C06549101DAF7BC51099E1412A0067DC08C246B7D8013C9D0CBF1E8EE7', @@ -34,7 +37,7 @@ export const generateOrders = (override?: PartialDeep): Orders => { remaining: '0', price: '20000000', timeInForce: OrderTimeInForce.GTC, - createdAt: new Date(2020, 1, 1).toISOString(), + createdAt: new Date(2020, 1, 30).toISOString(), updatedAt: null, expiresAt: null, rejectionReason: null, @@ -63,7 +66,7 @@ export const generateOrders = (override?: PartialDeep): Orders => { remaining: '0', price: '100', timeInForce: OrderTimeInForce.GTC, - createdAt: new Date().toISOString(), + createdAt: new Date(2020, 1, 29).toISOString(), updatedAt: null, expiresAt: null, rejectionReason: null, @@ -92,7 +95,7 @@ export const generateOrders = (override?: PartialDeep): Orders => { remaining: '0', price: '20000', timeInForce: OrderTimeInForce.GTC, - createdAt: new Date(2022, 5, 10).toISOString(), + createdAt: new Date(2020, 1, 28).toISOString(), updatedAt: null, expiresAt: null, rejectionReason: null, @@ -121,17 +124,35 @@ export const generateOrders = (override?: PartialDeep): Orders => { remaining: '0', price: '100000', timeInForce: OrderTimeInForce.GTC, - createdAt: new Date(2022, 7, 15).toISOString(), + createdAt: new Date(2020, 1, 27).toISOString(), updatedAt: null, expiresAt: null, rejectionReason: null, }, ]; - const defaultResult = { + const defaultResult: Orders = { party: { id: Cypress.env('VEGA_PUBLIC_KEY'), - orders, + ordersConnection: { + __typename: 'OrderConnection', + edges: orders.map((f) => { + return { + __typename: 'OrderEdge', + node: f, + cursor: f.id, + }; + }), + pageInfo: { + __typename: 'PageInfo', + startCursor: + '066468C06549101DAF7BC51099E1412A0067DC08C246B7D8013C9D0CBF1E8EE7', + endCursor: + '94737d2bafafa4bc3b80a56ef084ae52a983b91aa067c31e243c61a0f962a836', + hasNextPage: false, + hasPreviousPage: false, + }, + }, __typename: 'Party', }, }; diff --git a/libs/fills/src/lib/fills-manager.tsx b/libs/fills/src/lib/fills-manager.tsx index 789016114..4f4493a19 100644 --- a/libs/fills/src/lib/fills-manager.tsx +++ b/libs/fills/src/lib/fills-manager.tsx @@ -1,13 +1,12 @@ import type { AgGridReact } from 'ag-grid-react'; import { useCallback, useRef, useMemo } from 'react'; -import { useDataProvider } from '@vegaprotocol/react-helpers'; +import { + useDataProvider, + makeInfiniteScrollGetRows, +} from '@vegaprotocol/react-helpers'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { FillsTable } from './fills-table'; -import type { - IGetRowsParams, - BodyScrollEvent, - BodyScrollEndEvent, -} from 'ag-grid-community'; +import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community'; import { fillsDataProvider as dataProvider } from './fills-data-provider'; import type { Fills_party_tradesConnection_edges } from './__generated__/Fills'; @@ -90,36 +89,12 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => { 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 getRows = makeInfiniteScrollGetRows( + newRows, + dataRef, + totalCountRef, + load + ); const onBodyScrollEnd = (event: BodyScrollEndEvent) => { if (event.top === 0) { diff --git a/libs/fills/src/lib/fills-table.stories.tsx b/libs/fills/src/lib/fills-table.stories.tsx index 8ae2d9ce1..cad2de52d 100644 --- a/libs/fills/src/lib/fills-table.stories.tsx +++ b/libs/fills/src/lib/fills-table.stories.tsx @@ -3,15 +3,12 @@ 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 { makeInfiniteScrollGetRows } from '@vegaprotocol/react-helpers'; import { FillsTable } from './fills-table'; 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'; +import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community'; export default { component: FillsTable, @@ -73,7 +70,7 @@ const useDataProvider = ({ const insertionData = getData(start, end); data.splice(start, end - start, ...insertionData); insert({ data, totalCount, insertionData }); - return Promise.resolve(); + return Promise.resolve(insertionData); }, totalCount, }; @@ -152,39 +149,12 @@ const PaginationManager = ({ pagination }: PaginationManagerProps) => { 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 getRows = makeInfiniteScrollGetRows( + newRows, + dataRef, + totalCountRef, + load + ); const onBodyScrollEnd = (event: BodyScrollEndEvent) => { if (event.top === 0) { @@ -313,36 +283,12 @@ const InfiniteScrollManager = () => { 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 getRows = makeInfiniteScrollGetRows( + newRows, + dataRef, + totalCountRef, + load + ); const onBodyScrollEnd = (event: BodyScrollEndEvent) => { if (event.top === 0) { diff --git a/libs/orders/src/lib/components/__generated__/OrderFields.ts b/libs/orders/src/lib/components/__generated__/OrderFields.ts deleted file mode 100644 index cac0eff56..000000000 --- a/libs/orders/src/lib/components/__generated__/OrderFields.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @generated -// This file was automatically generated and should not be edited. - -import { OrderType, Side, OrderStatus, OrderRejectionReason, OrderTimeInForce } from "@vegaprotocol/types"; - -// ==================================================== -// GraphQL fragment: OrderFields -// ==================================================== - -export interface OrderFields_market_tradableInstrument_instrument { - __typename: "Instrument"; - /** - * A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string) - */ - code: string; -} - -export interface OrderFields_market_tradableInstrument { - __typename: "TradableInstrument"; - /** - * An instance of or reference to a fully specified instrument. - */ - instrument: OrderFields_market_tradableInstrument_instrument; -} - -export interface OrderFields_market { - __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; - /** - * positionDecimalPlaces indicated the number of decimal places that an integer must be shifted in order to get a correct size (uint64). - * i.e. 0 means there are no fractional orders for the market, and order sizes are always whole sizes. - * 2 means sizes given as 10^2 * desired size, e.g. a desired size of 1.23 is represented as 123 in this market. - */ - positionDecimalPlaces: number; - /** - * An instance of or reference to a tradable instrument. - */ - tradableInstrument: OrderFields_market_tradableInstrument; -} - -export interface OrderFields { - __typename: "Order"; - /** - * Hash of the order data - */ - id: string; - /** - * The market the order is trading on (probably stored internally as a hash of the market details) - */ - market: OrderFields_market | null; - /** - * Type the order type (defaults to PARTY) - */ - type: OrderType | null; - /** - * Whether the order is to buy or sell - */ - side: Side; - /** - * Total number of contracts that may be bought or sold (immutable) (uint64) - */ - size: string; - /** - * The status of an order, for example 'Active' - */ - status: OrderStatus; - /** - * Reason for the order to be rejected - */ - rejectionReason: OrderRejectionReason | null; - /** - * The worst price the order will trade at (e.g. buy for price or less, sell for price or more) (uint64) - */ - price: string; - /** - * The timeInForce of order (determines how and if it executes, and whether it persists on the book) - */ - timeInForce: OrderTimeInForce; - /** - * Number of contracts remaining of the total that have not yet been bought or sold (uint64) - */ - remaining: string; - /** - * Expiration time of this order (ISO-8601 RFC3339+Nano formatted date) - */ - expiresAt: string | null; - /** - * RFC3339Nano formatted date and time for when the order was created (timestamp) - */ - createdAt: string; - /** - * RFC3339Nano time the order was altered - */ - updatedAt: string | null; -} diff --git a/libs/orders/src/lib/components/__generated__/OrderSub.ts b/libs/orders/src/lib/components/__generated__/OrderSub.ts deleted file mode 100644 index 99cdb2aae..000000000 --- a/libs/orders/src/lib/components/__generated__/OrderSub.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @generated -// This file was automatically generated and should not be edited. - -import { OrderType, Side, OrderStatus, OrderRejectionReason, OrderTimeInForce } from "@vegaprotocol/types"; - -// ==================================================== -// GraphQL subscription operation: OrderSub -// ==================================================== - -export interface OrderSub_orders_market_tradableInstrument_instrument { - __typename: "Instrument"; - /** - * A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string) - */ - code: string; -} - -export interface OrderSub_orders_market_tradableInstrument { - __typename: "TradableInstrument"; - /** - * An instance of or reference to a fully specified instrument. - */ - instrument: OrderSub_orders_market_tradableInstrument_instrument; -} - -export interface OrderSub_orders_market { - __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; - /** - * positionDecimalPlaces indicated the number of decimal places that an integer must be shifted in order to get a correct size (uint64). - * i.e. 0 means there are no fractional orders for the market, and order sizes are always whole sizes. - * 2 means sizes given as 10^2 * desired size, e.g. a desired size of 1.23 is represented as 123 in this market. - */ - positionDecimalPlaces: number; - /** - * An instance of or reference to a tradable instrument. - */ - tradableInstrument: OrderSub_orders_market_tradableInstrument; -} - -export interface OrderSub_orders { - __typename: "Order"; - /** - * Hash of the order data - */ - id: string; - /** - * The market the order is trading on (probably stored internally as a hash of the market details) - */ - market: OrderSub_orders_market | null; - /** - * Type the order type (defaults to PARTY) - */ - type: OrderType | null; - /** - * Whether the order is to buy or sell - */ - side: Side; - /** - * Total number of contracts that may be bought or sold (immutable) (uint64) - */ - size: string; - /** - * The status of an order, for example 'Active' - */ - status: OrderStatus; - /** - * Reason for the order to be rejected - */ - rejectionReason: OrderRejectionReason | null; - /** - * The worst price the order will trade at (e.g. buy for price or less, sell for price or more) (uint64) - */ - price: string; - /** - * The timeInForce of order (determines how and if it executes, and whether it persists on the book) - */ - timeInForce: OrderTimeInForce; - /** - * Number of contracts remaining of the total that have not yet been bought or sold (uint64) - */ - remaining: string; - /** - * Expiration time of this order (ISO-8601 RFC3339+Nano formatted date) - */ - expiresAt: string | null; - /** - * RFC3339Nano formatted date and time for when the order was created (timestamp) - */ - createdAt: string; - /** - * RFC3339Nano time the order was altered - */ - updatedAt: string | null; -} - -export interface OrderSub { - /** - * Subscribe to orders updates - */ - orders: OrderSub_orders[] | null; -} - -export interface OrderSubVariables { - partyId: string; -} diff --git a/libs/orders/src/lib/components/__generated__/Orders.ts b/libs/orders/src/lib/components/__generated__/Orders.ts deleted file mode 100644 index 47a88ce9e..000000000 --- a/libs/orders/src/lib/components/__generated__/Orders.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @generated -// This file was automatically generated and should not be edited. - -import { OrderType, Side, OrderStatus, OrderRejectionReason, OrderTimeInForce } from "@vegaprotocol/types"; - -// ==================================================== -// GraphQL query operation: Orders -// ==================================================== - -export interface Orders_party_orders_market_tradableInstrument_instrument { - __typename: "Instrument"; - /** - * A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string) - */ - code: string; -} - -export interface Orders_party_orders_market_tradableInstrument { - __typename: "TradableInstrument"; - /** - * An instance of or reference to a fully specified instrument. - */ - instrument: Orders_party_orders_market_tradableInstrument_instrument; -} - -export interface Orders_party_orders_market { - __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; - /** - * positionDecimalPlaces indicated the number of decimal places that an integer must be shifted in order to get a correct size (uint64). - * i.e. 0 means there are no fractional orders for the market, and order sizes are always whole sizes. - * 2 means sizes given as 10^2 * desired size, e.g. a desired size of 1.23 is represented as 123 in this market. - */ - positionDecimalPlaces: number; - /** - * An instance of or reference to a tradable instrument. - */ - tradableInstrument: Orders_party_orders_market_tradableInstrument; -} - -export interface Orders_party_orders { - __typename: "Order"; - /** - * Hash of the order data - */ - id: string; - /** - * The market the order is trading on (probably stored internally as a hash of the market details) - */ - market: Orders_party_orders_market | null; - /** - * Type the order type (defaults to PARTY) - */ - type: OrderType | null; - /** - * Whether the order is to buy or sell - */ - side: Side; - /** - * Total number of contracts that may be bought or sold (immutable) (uint64) - */ - size: string; - /** - * The status of an order, for example 'Active' - */ - status: OrderStatus; - /** - * Reason for the order to be rejected - */ - rejectionReason: OrderRejectionReason | null; - /** - * The worst price the order will trade at (e.g. buy for price or less, sell for price or more) (uint64) - */ - price: string; - /** - * The timeInForce of order (determines how and if it executes, and whether it persists on the book) - */ - timeInForce: OrderTimeInForce; - /** - * Number of contracts remaining of the total that have not yet been bought or sold (uint64) - */ - remaining: string; - /** - * Expiration time of this order (ISO-8601 RFC3339+Nano formatted date) - */ - expiresAt: string | null; - /** - * RFC3339Nano formatted date and time for when the order was created (timestamp) - */ - createdAt: string; - /** - * RFC3339Nano time the order was altered - */ - updatedAt: string | null; -} - -export interface Orders_party { - __typename: "Party"; - /** - * Party identifier - */ - id: string; - /** - * Orders relating to a party - */ - orders: Orders_party_orders[] | null; -} - -export interface Orders { - /** - * An entity that is trading on the VEGA network - */ - party: Orders_party | null; -} - -export interface OrdersVariables { - partyId: string; -} diff --git a/libs/orders/src/lib/components/index.ts b/libs/orders/src/lib/components/index.ts index 03805b4c3..dff9b29da 100644 --- a/libs/orders/src/lib/components/index.ts +++ b/libs/orders/src/lib/components/index.ts @@ -1,5 +1,3 @@ -export * from './__generated__'; -export * from './mocks'; export * from './order-data-provider'; export * from './order-list'; export * from './order-list-manager'; diff --git a/libs/orders/src/lib/components/mocks/generate-orders.ts b/libs/orders/src/lib/components/mocks/generate-orders.ts index 992ae97c5..b28b5eb40 100644 --- a/libs/orders/src/lib/components/mocks/generate-orders.ts +++ b/libs/orders/src/lib/components/mocks/generate-orders.ts @@ -1,28 +1,15 @@ import merge from 'lodash/merge'; -import type { PartialDeep } from 'type-fest'; - import { OrderStatus, OrderTimeInForce, OrderType, Side, } from '@vegaprotocol/types'; -import type { Orders, Orders_party_orders } from '../__generated__/Orders'; +import type { Orders_party_ordersConnection_edges_node } from '../'; -export const generateOrders = (override?: PartialDeep): Orders => { - const orders: Orders_party_orders[] = generateOrdersArray(); - - const defaultResult = { - party: { - id: 'party-id', - orders, - __typename: 'Party', - }, - }; - return merge(defaultResult, override); -}; - -export const generateOrder = (partialOrder?: Partial) => +export const generateOrder = ( + partialOrder: Partial +) => merge( { __typename: 'Order', @@ -52,7 +39,7 @@ export const generateOrder = (partialOrder?: Partial) => updatedAt: null, expiresAt: null, rejectionReason: null, - } as Orders_party_orders, + } as Orders_party_ordersConnection_edges_node, partialOrder ); @@ -71,86 +58,88 @@ export const marketOrder = generateOrder({ status: OrderStatus.Active, }); -export const generateMockOrders = (): Orders_party_orders[] => { - return [ - generateOrder({ - id: '066468C06549101DAF7BC51099E1412A0067DC08C246B7D8013C9D0CBF1E8EE7', - market: { - __typename: 'Market', - id: 'c9f5acd348796011c075077e4d58d9b7f1689b7c1c8e030a5e886b83aa96923d', - name: 'AAVEDAI Monthly (30 Jun 2022)', - decimalPlaces: 5, - positionDecimalPlaces: 0, - tradableInstrument: { - __typename: 'TradableInstrument', - instrument: { - __typename: 'Instrument', - code: 'AAVEDAI.MF21', +export const generateMockOrders = + (): Orders_party_ordersConnection_edges_node[] => { + return [ + generateOrder({ + id: '066468C06549101DAF7BC51099E1412A0067DC08C246B7D8013C9D0CBF1E8EE7', + market: { + __typename: 'Market', + id: 'c9f5acd348796011c075077e4d58d9b7f1689b7c1c8e030a5e886b83aa96923d', + name: 'AAVEDAI Monthly (30 Jun 2022)', + decimalPlaces: 5, + positionDecimalPlaces: 0, + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + code: 'AAVEDAI.MF21', + }, }, }, - }, - size: '10', - type: OrderType.Limit, - status: OrderStatus.Filled, - side: Side.Buy, - remaining: '0', - price: '20000000', - timeInForce: OrderTimeInForce.GTC, - createdAt: new Date(2020, 1, 1).toISOString(), - }), - generateOrder({ - id: '48DB6767E4E4E0F649C5A13ABFADE39F8451C27DA828DAF14B7A1E8E5EBDAD99', - market: { - __typename: 'Market', - id: '5a4b0b9e9c0629f0315ec56fcb7bd444b0c6e4da5ec7677719d502626658a376', - name: 'Tesla Quarterly (30 Jun 2022)', - decimalPlaces: 5, - positionDecimalPlaces: 0, - tradableInstrument: { - __typename: 'TradableInstrument', - instrument: { - __typename: 'Instrument', - code: 'TSLA.QM21', + size: '10', + type: OrderType.Limit, + status: OrderStatus.Filled, + side: Side.Buy, + remaining: '0', + price: '20000000', + timeInForce: OrderTimeInForce.GTC, + createdAt: new Date(2020, 1, 1).toISOString(), + }), + generateOrder({ + id: '48DB6767E4E4E0F649C5A13ABFADE39F8451C27DA828DAF14B7A1E8E5EBDAD99', + market: { + __typename: 'Market', + id: '5a4b0b9e9c0629f0315ec56fcb7bd444b0c6e4da5ec7677719d502626658a376', + name: 'Tesla Quarterly (30 Jun 2022)', + decimalPlaces: 5, + positionDecimalPlaces: 0, + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + code: 'TSLA.QM21', + }, }, }, - }, - size: '1', - type: OrderType.Limit, - status: OrderStatus.Filled, - side: Side.Buy, - remaining: '0', - price: '100', - timeInForce: OrderTimeInForce.GTC, - createdAt: new Date().toISOString(), - }), - generateOrder({ - id: '4e93702990712c41f6995fcbbd94f60bb372ad12d64dfa7d96d205c49f790336', - market: { - __typename: 'Market', - id: 'c6f4337b31ed57a961969c3ba10297b369d01b9e75a4cbb96db4fc62886444e6', - name: 'BTCUSD Monthly (30 Jun 2022)', - decimalPlaces: 5, - positionDecimalPlaces: 0, - tradableInstrument: { - __typename: 'TradableInstrument', - instrument: { - __typename: 'Instrument', - code: 'BTCUSD.MF21', + size: '1', + type: OrderType.Limit, + status: OrderStatus.Filled, + side: Side.Buy, + remaining: '0', + price: '100', + timeInForce: OrderTimeInForce.GTC, + createdAt: new Date().toISOString(), + }), + generateOrder({ + id: '4e93702990712c41f6995fcbbd94f60bb372ad12d64dfa7d96d205c49f790336', + market: { + __typename: 'Market', + id: 'c6f4337b31ed57a961969c3ba10297b369d01b9e75a4cbb96db4fc62886444e6', + name: 'BTCUSD Monthly (30 Jun 2022)', + decimalPlaces: 5, + positionDecimalPlaces: 0, + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + code: 'BTCUSD.MF21', + }, }, }, - }, - size: '1', - type: OrderType.Limit, - status: OrderStatus.Filled, - side: Side.Buy, - remaining: '0', - price: '20000', - timeInForce: OrderTimeInForce.GTC, - createdAt: new Date(2022, 5, 10).toISOString(), - }), - ]; -}; + size: '1', + type: OrderType.Limit, + status: OrderStatus.Filled, + side: Side.Buy, + remaining: '0', + price: '20000', + timeInForce: OrderTimeInForce.GTC, + createdAt: new Date(2022, 5, 10).toISOString(), + }), + ]; + }; -export const generateOrdersArray = (): Orders_party_orders[] => { - return [marketOrder, limitOrder, ...generateMockOrders()]; -}; +export const generateOrdersArray = + (): Orders_party_ordersConnection_edges_node[] => { + return [marketOrder, limitOrder, ...generateMockOrders()]; + }; diff --git a/libs/orders/src/lib/components/order-data-provider/__generated__/Orders.ts b/libs/orders/src/lib/components/order-data-provider/__generated__/Orders.ts index ab2ca9e60..7136bc536 100644 --- a/libs/orders/src/lib/components/order-data-provider/__generated__/Orders.ts +++ b/libs/orders/src/lib/components/order-data-provider/__generated__/Orders.ts @@ -3,13 +3,13 @@ // @generated // This file was automatically generated and should not be edited. -import { OrderType, Side, OrderStatus, OrderRejectionReason, OrderTimeInForce } from "@vegaprotocol/types"; +import { Pagination, OrderType, Side, OrderStatus, OrderRejectionReason, OrderTimeInForce } from "@vegaprotocol/types"; // ==================================================== // GraphQL query operation: Orders // ==================================================== -export interface Orders_party_orders_market_tradableInstrument_instrument { +export interface Orders_party_ordersConnection_edges_node_market_tradableInstrument_instrument { __typename: "Instrument"; /** * A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string) @@ -17,15 +17,15 @@ export interface Orders_party_orders_market_tradableInstrument_instrument { code: string; } -export interface Orders_party_orders_market_tradableInstrument { +export interface Orders_party_ordersConnection_edges_node_market_tradableInstrument { __typename: "TradableInstrument"; /** * An instance of or reference to a fully specified instrument. */ - instrument: Orders_party_orders_market_tradableInstrument_instrument; + instrument: Orders_party_ordersConnection_edges_node_market_tradableInstrument_instrument; } -export interface Orders_party_orders_market { +export interface Orders_party_ordersConnection_edges_node_market { __typename: "Market"; /** * Market ID @@ -61,10 +61,10 @@ export interface Orders_party_orders_market { /** * An instance of or reference to a tradable instrument. */ - tradableInstrument: Orders_party_orders_market_tradableInstrument; + tradableInstrument: Orders_party_ordersConnection_edges_node_market_tradableInstrument; } -export interface Orders_party_orders { +export interface Orders_party_ordersConnection_edges_node { __typename: "Order"; /** * Hash of the order data @@ -73,7 +73,7 @@ export interface Orders_party_orders { /** * The market the order is trading on (probably stored internally as a hash of the market details) */ - market: Orders_party_orders_market | null; + market: Orders_party_ordersConnection_edges_node_market | null; /** * Type the order type (defaults to PARTY) */ @@ -120,6 +120,32 @@ export interface Orders_party_orders { updatedAt: string | null; } +export interface Orders_party_ordersConnection_edges { + __typename: "OrderEdge"; + node: Orders_party_ordersConnection_edges_node; + cursor: string | null; +} + +export interface Orders_party_ordersConnection_pageInfo { + __typename: "PageInfo"; + startCursor: string; + endCursor: string; + hasNextPage: boolean; + hasPreviousPage: boolean; +} + +export interface Orders_party_ordersConnection { + __typename: "OrderConnection"; + /** + * The orders in this connection + */ + edges: Orders_party_ordersConnection_edges[] | null; + /** + * The pagination information + */ + pageInfo: Orders_party_ordersConnection_pageInfo | null; +} + export interface Orders_party { __typename: "Party"; /** @@ -129,7 +155,7 @@ export interface Orders_party { /** * Orders relating to a party */ - orders: Orders_party_orders[] | null; + ordersConnection: Orders_party_ordersConnection; } export interface Orders { @@ -141,4 +167,5 @@ export interface Orders { export interface OrdersVariables { partyId: string; + pagination?: Pagination | null; } diff --git a/libs/orders/src/lib/components/__generated__/index.ts b/libs/orders/src/lib/components/order-data-provider/__generated__/index.ts similarity index 100% rename from libs/orders/src/lib/components/__generated__/index.ts rename to libs/orders/src/lib/components/order-data-provider/__generated__/index.ts diff --git a/libs/orders/src/lib/components/order-data-provider/index.ts b/libs/orders/src/lib/components/order-data-provider/index.ts index 04954cd93..d154614ff 100644 --- a/libs/orders/src/lib/components/order-data-provider/index.ts +++ b/libs/orders/src/lib/components/order-data-provider/index.ts @@ -1 +1,2 @@ +export * from './__generated__'; export * from './order-data-provider'; diff --git a/libs/orders/src/lib/components/order-data-provider/order-data-provider.spec.ts b/libs/orders/src/lib/components/order-data-provider/order-data-provider.spec.ts index 292ba08d8..dbe048205 100644 --- a/libs/orders/src/lib/components/order-data-provider/order-data-provider.spec.ts +++ b/libs/orders/src/lib/components/order-data-provider/order-data-provider.spec.ts @@ -1,88 +1,59 @@ -import { - OrderType, - OrderStatus, - Side, - OrderTimeInForce, -} from '@vegaprotocol/types'; -import type { Orders_party_orders } from '../__generated__/Orders'; -import { sortOrders } from './order-data-provider'; - -const marketOrder: Orders_party_orders = { - __typename: 'Order', - id: 'order-id', - market: { - __typename: 'Market', - id: 'market-id', - name: 'market-name', - decimalPlaces: 2, - tradableInstrument: { - __typename: 'TradableInstrument', - instrument: { - __typename: 'Instrument', - code: 'instrument-code', +import { update } from './order-data-provider'; +import type { OrderSub_orders, Orders_party_ordersConnection_edges } from '../'; +describe('order data provider', () => { + it('puts incoming data in proper place', () => { + const data = [ + { + node: { + id: '1', + updatedAt: new Date('2022-01-31').toISOString(), + createdAt: new Date('2022-01-29').toISOString(), + }, }, - }, - }, - size: '10', - type: OrderType.Market, - status: OrderStatus.Active, - side: Side.Buy, - remaining: '5', - price: '', - timeInForce: OrderTimeInForce.IOC, - createdAt: new Date('2022-2-3').toISOString(), - updatedAt: null, - expiresAt: null, - rejectionReason: null, -}; - -const limitOrder: Orders_party_orders = { - __typename: 'Order', - id: 'order-id', - market: { - __typename: 'Market', - id: 'market-id', - name: 'market-name', - decimalPlaces: 2, - tradableInstrument: { - __typename: 'TradableInstrument', - instrument: { - __typename: 'Instrument', - code: 'instrument-code', + { + node: { + id: '2', + createdAt: new Date('2022-01-30').toISOString(), + }, }, - }, - }, - size: '10', - type: OrderType.Limit, - status: OrderStatus.Active, - side: Side.Sell, - remaining: '5', - price: '12345', - timeInForce: OrderTimeInForce.GTT, - createdAt: new Date('2022-3-3').toISOString(), - expiresAt: new Date('2022-3-5').toISOString(), - updatedAt: null, - rejectionReason: null, -}; + ] as Orders_party_ordersConnection_edges[]; -describe('OrderDataProvider', () => { - const orders = [marketOrder, limitOrder]; + const delta = [ + // this one should be dropped because id don't exits and it's older than newest + { + id: '0', + createdAt: new Date('2022-01-30').toISOString(), + }, + // this one should be dropped because new newer below + { + id: '1', + updatedAt: new Date('2022-02-01').toISOString(), + createdAt: new Date('2022-01-29').toISOString(), + }, + { + id: '1', + updatedAt: new Date('2022-02-02').toISOString(), + createdAt: new Date('2022-01-29').toISOString(), + }, + // this should be added + { + id: '4', + createdAt: new Date('2022-02-04').toISOString(), + }, + // this should be move to top + { + id: '2', + updatedAt: new Date('2022-02-03').toISOString(), + createdAt: new Date('2022-01-29').toISOString(), + }, + ] as OrderSub_orders[]; - describe('sortOrders', () => { - it('should sort the orders from the most recent placed to the oldest', () => { - expect(sortOrders(orders)).toStrictEqual([limitOrder, marketOrder]); - }); - - it('should sort the orders from the most recent updated to the oldest', () => { - const updatedOrder = { - ...limitOrder, - updatedAt: new Date('2022-3-4').toISOString(), - }; - expect(sortOrders([...orders, updatedOrder])).toStrictEqual([ - updatedOrder, - limitOrder, - marketOrder, - ]); - }); + const updatedData = update(data, delta); + expect(updatedData.findIndex((edge) => edge.node === delta[0])).toEqual(-1); + expect(updatedData[2].node.id).toEqual(delta[2].id); + expect(updatedData[2].node.updatedAt).toEqual(delta[2].updatedAt); + expect(updatedData[0].node).toEqual(delta[3]); + expect(updatedData[1].node.id).toEqual(delta[4].id); + expect(updatedData[1].node.updatedAt).toEqual(delta[4].updatedAt); }); }); diff --git a/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts b/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts index 334f835ab..6c82ff2bf 100644 --- a/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts +++ b/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts @@ -1,11 +1,18 @@ import produce from 'immer'; import { gql } from '@apollo/client'; -import { makeDataProvider } from '@vegaprotocol/react-helpers'; -import type { OrderFields } from '../__generated__/OrderFields'; -import type { Orders, Orders_party_orders } from '../__generated__/Orders'; -import type { OrderSub } from '../__generated__/OrderSub'; import orderBy from 'lodash/orderBy'; import uniqBy from 'lodash/uniqBy'; +import { + makeDataProvider, + defaultAppend as append, +} from '@vegaprotocol/react-helpers'; +import type { PageInfo } from '@vegaprotocol/react-helpers'; +import type { + Orders, + Orders_party_ordersConnection_edges, + OrderSub, + OrderFields, +} from '../'; const ORDER_FRAGMENT = gql` fragment OrderFields on Order { @@ -37,11 +44,22 @@ const ORDER_FRAGMENT = gql` export const ORDERS_QUERY = gql` ${ORDER_FRAGMENT} - query Orders($partyId: ID!) { + query Orders($partyId: ID!, $pagination: Pagination) { party(id: $partyId) { id - orders { - ...OrderFields + ordersConnection(pagination: $pagination) { + edges { + node { + ...OrderFields + } + cursor + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } } } } @@ -56,53 +74,55 @@ export const ORDERS_SUB = gql` } `; -// A single update can contain the same order with multiple updates, so we need to find -// the latest version of the order and only update using that -export const sortOrders = (orders: OrderFields[]) => { - return orderBy( - orders, - (o) => { - if (!o.updatedAt) return new Date(o.createdAt).getTime(); - return new Date(o.updatedAt).getTime(); - }, - 'desc' - ); -}; - -export const uniqOrders = (orders: OrderFields[]) => { - return uniqBy(orders, 'id'); -}; - -export const prepareIncomingOrders = (delta: OrderFields[]) => { - const sortedOrders = sortOrders(delta); - const incoming = uniqOrders(sortedOrders); - return incoming; -}; - -const update = (data: OrderFields[], delta: OrderFields[]) => { +export const update = ( + data: Orders_party_ordersConnection_edges[], + delta: OrderFields[] +) => { return produce(data, (draft) => { - const incoming = prepareIncomingOrders(delta); + // A single update can contain the same order with multiple updates, so we need to find + // the latest version of the order and only update using that + const incoming = uniqBy( + orderBy(delta, (order) => order.updatedAt || order.createdAt, 'desc'), + 'id' + ); // Add or update incoming orders - incoming.forEach((order) => { - const index = draft.findIndex((o) => o.id === order.id); - if (index === -1) { - draft.unshift(order); - } else { - draft[index] = order; + incoming.reverse().forEach((node) => { + const index = draft.findIndex((edge) => edge.node.id === node.id); + const newer = + (node.updatedAt || node.createdAt) >= + (draft[0].node.updatedAt || draft[0].node.createdAt); + if (index !== -1) { + Object.assign(draft[index].node, node); + if (newer) { + draft.unshift(...draft.splice(index, 1)); + } + } else if (newer) { + draft.unshift({ node, cursor: '', __typename: 'OrderEdge' }); } }); }); }; -const getData = (responseData: Orders): Orders_party_orders[] | null => - responseData?.party?.orders || null; +const getData = ( + responseData: Orders +): Orders_party_ordersConnection_edges[] | null => + responseData?.party?.ordersConnection.edges || null; + const getDelta = (subscriptionData: OrderSub) => subscriptionData.orders || []; +const getPageInfo = (responseData: Orders): PageInfo | null => + responseData.party?.ordersConnection.pageInfo || null; + export const ordersDataProvider = makeDataProvider( ORDERS_QUERY, ORDERS_SUB, update, getData, - getDelta + getDelta, + { + getPageInfo, + append, + first: 100, + } ); diff --git a/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx b/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx index 44d5fa244..545d1d5bd 100644 --- a/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx +++ b/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx @@ -1,8 +1,8 @@ import { render, screen } from '@testing-library/react'; import { OrderListManager } from './order-list-manager'; import * as useDataProviderHook from '@vegaprotocol/react-helpers'; -import type { Orders_party_orders } from '../__generated__/Orders'; -import * as orderListMock from '../order-list'; +import type { Orders_party_ordersConnection_edges_node } from '../'; +import * as orderListMock from '../'; import { forwardRef } from 'react'; jest.mock('./order-list'); @@ -12,6 +12,10 @@ it('Renders a loading state while awaiting orders', () => { data: [], loading: true, error: undefined, + flush: jest.fn(), + reload: jest.fn(), + load: jest.fn(), + totalCount: 0, }); render(); expect(screen.getByText('Loading...')).toBeInTheDocument(); @@ -23,6 +27,10 @@ it('Renders an error state', () => { data: [], loading: false, error: new Error(errorMsg), + flush: jest.fn(), + reload: jest.fn(), + load: jest.fn(), + totalCount: undefined, }); render(); expect( @@ -35,9 +43,13 @@ it('Renders the order list if orders provided', async () => { // avoid warnings about padding refs orderListMock.OrderList = forwardRef(() =>
OrderList
); jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({ - data: [{ id: '1' } as Orders_party_orders], + data: [{ id: '1' } as Orders_party_ordersConnection_edges_node], loading: false, error: undefined, + flush: jest.fn(), + reload: jest.fn(), + load: jest.fn(), + totalCount: undefined, }); render(); expect(await screen.findByText('OrderList')).toBeInTheDocument(); diff --git a/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx b/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx index af4607285..18e38f7e7 100644 --- a/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx +++ b/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx @@ -1,16 +1,14 @@ import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; -import { OrderList } from '../order-list'; -import type { OrderFields } from '../__generated__/OrderFields'; -import { useDataProvider } from '@vegaprotocol/react-helpers'; import { - ordersDataProvider as dataProvider, - prepareIncomingOrders, - sortOrders, -} from '../order-data-provider'; + useDataProvider, + makeInfiniteScrollGetRows, +} from '@vegaprotocol/react-helpers'; import { useCallback, useMemo, useRef } from 'react'; +import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community'; import type { AgGridReact } from 'ag-grid-react'; -import type { OrderSub_orders } from '../__generated__/OrderSub'; -import isEqual from 'lodash/isEqual'; + +import { OrderList, ordersDataProvider as dataProvider } from '../'; +import type { OrderFields, Orders_party_ordersConnection_edges } from '../'; interface OrderListManagerProps { partyId: string; @@ -18,62 +16,105 @@ interface OrderListManagerProps { export const OrderListManager = ({ partyId }: OrderListManagerProps) => { const gridRef = useRef(null); + const dataRef = useRef<(Orders_party_ordersConnection_edges | null)[] | null>( + null + ); + const totalCountRef = useRef(undefined); + const newRows = useRef(0); + const scrolledToTop = useRef(true); const variables = useMemo(() => ({ partyId }), [partyId]); - // Apply updates to the table - const update = useCallback(({ delta }: { delta: OrderSub_orders[] }) => { - if (!gridRef.current) { - return false; + const addNewRows = useCallback(() => { + if (newRows.current === 0) { + return; } - const incoming = prepareIncomingOrders(delta); - - const updateRows: OrderFields[] = []; - const add: OrderFields[] = []; - - incoming.forEach((d) => { - if (!gridRef.current?.api) { - return; - } - - const rowNode = gridRef.current.api.getRowNode(d.id); - - if (rowNode) { - if (!isEqual(d, rowNode.data)) { - updateRows.push(d); - } - } else { - add.push(d); - } - }); - - if (updateRows.length || add.length) { - gridRef.current.api.applyTransactionAsync({ - update: updateRows, - add, - addIndex: 0, - }); + if (totalCountRef.current !== undefined) { + totalCountRef.current += newRows.current; } - - return true; + newRows.current = 0; + if (!gridRef.current?.api) { + return; + } + gridRef.current.api.refreshInfiniteCache(); }, []); - const { data, error, loading } = useDataProvider({ + const update = useCallback( + ({ + data, + delta, + }: { + data: (Orders_party_ordersConnection_edges | null)[]; + delta: OrderFields[]; + }) => { + 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: Orders_party_ordersConnection_edges[]; + 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 orders = useMemo(() => { - if (!data) { - return null; + const getRows = + makeInfiniteScrollGetRows( + newRows, + dataRef, + totalCountRef, + load + ); + + const onBodyScrollEnd = (event: BodyScrollEndEvent) => { + if (event.top === 0) { + addNewRows(); } - return sortOrders(data); - }, [data]); + }; + + const onBodyScroll = (event: BodyScrollEvent) => { + scrolledToTop.current = event.top <= 0; + }; - // We can set - + + ); }; diff --git a/libs/orders/src/lib/components/order-list/order-list.spec.tsx b/libs/orders/src/lib/components/order-list/order-list.spec.tsx index 4ab543b4f..709a19985 100644 --- a/libs/orders/src/lib/components/order-list/order-list.spec.tsx +++ b/libs/orders/src/lib/components/order-list/order-list.spec.tsx @@ -4,24 +4,25 @@ import { formatLabel, getDateTimeFormat, } from '@vegaprotocol/react-helpers'; -import type { Orders_party_orders } from '../__generated__/Orders'; import { OrderStatus, OrderRejectionReason } from '@vegaprotocol/types'; -import { OrderListTable } from './order-list'; import type { PartialDeep } from 'type-fest'; import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; import { VegaWalletContext } from '@vegaprotocol/wallet'; import { MockedProvider } from '@apollo/client/testing'; + +import { OrderListTable } from '../'; +import type { Orders_party_ordersConnection_edges_node } from '../'; import { limitOrder, marketOrder } from '../mocks/generate-orders'; const generateJsx = ( - orders: Orders_party_orders[] | null, + orders: Orders_party_ordersConnection_edges_node[] | null, context: PartialDeep = { keypair: { pub: '0x123' } } ) => { return ( { return (
{ return; @@ -55,7 +55,7 @@ const Template2: Story = (args) => { <>
{ return; diff --git a/libs/orders/src/lib/components/order-list/order-list.tsx b/libs/orders/src/lib/components/order-list/order-list.tsx index 6cc16a331..2593872ec 100644 --- a/libs/orders/src/lib/components/order-list/order-list.tsx +++ b/libs/orders/src/lib/components/order-list/order-list.tsx @@ -1,5 +1,4 @@ import { OrderTimeInForce, OrderStatus, Side } from '@vegaprotocol/types'; -import type { Orders_party_orders } from '../__generated__/Orders'; import { addDecimal, formatLabel, @@ -11,27 +10,29 @@ import type { ICellRendererParams, ValueFormatterParams, } from 'ag-grid-community'; -import type { AgGridReact } from 'ag-grid-react'; +import type { + AgGridReact, + AgGridReactProps, + AgReactUiProps, +} from 'ag-grid-react'; import { AgGridColumn } from 'ag-grid-react'; import { forwardRef, useState } from 'react'; +import type { Orders_party_ordersConnection_edges_node } from '../'; import BigNumber from 'bignumber.js'; + import { useOrderCancel } from '../../order-hooks/use-order-cancel'; import { VegaTransactionDialog } from '@vegaprotocol/wallet'; import { useOrderEdit } from '../../order-hooks/use-order-edit'; import { OrderEditDialog } from './order-edit-dialog'; -interface OrderListProps { - data: Orders_party_orders[] | null; - showCancelled?: boolean; -} +type OrderListProps = AgGridReactProps | AgReactUiProps; export const OrderList = forwardRef( - ({ data, showCancelled = true }, ref) => { + (props, ref) => { const [cancelOrderDialogOpen, setCancelOrderDialogOpen] = useState(false); const [editOrderDialogOpen, setEditOrderDialogOpen] = useState(false); - const [editOrder, setEditOrder] = useState( - null - ); + const [editOrder, setEditOrder] = + useState(null); const { transaction, updatedOrder, reset, cancel } = useOrderCancel(); const { @@ -40,9 +41,6 @@ export const OrderList = forwardRef( reset: resetEdit, edit, } = useOrderEdit(); - const ordersData = showCancelled - ? data - : data?.filter((o) => o.status !== OrderStatus.Cancelled) || null; const getCancelDialogTitle = (status?: string) => { switch (status) { case OrderStatus.Cancelled: @@ -70,7 +68,7 @@ export const OrderList = forwardRef( return ( <> ( } ); -interface OrderListTableProps { - data: Orders_party_orders[] | null; +type OrderListTableValueFormatterParams = Omit< + ValueFormatterParams, + 'data' | 'value' +> & { + data: Orders_party_ordersConnection_edges_node | null; +}; + +type OrderListTableProps = (AgGridReactProps | AgReactUiProps) & { cancel: (body?: unknown) => Promise; setEditOrderDialogOpen: (value: boolean) => void; - setEditOrder: (order: Orders_party_orders | null) => void; -} + setEditOrder: ( + order: Orders_party_ordersConnection_edges_node | null + ) => void; +}; export const OrderListTable = forwardRef( - ({ data, cancel, setEditOrderDialogOpen, setEditOrder }, ref) => { + ({ cancel, setEditOrderDialogOpen, setEditOrder, ...props }, ref) => { return ( data.id} rowHeight={40} + {...props} > ( headerName={t('Amount')} field="size" cellClass="font-mono" - valueFormatter={({ value, data }: ValueFormatterParams) => { + valueFormatter={({ + value, + data, + }: OrderListTableValueFormatterParams & { + value?: Orders_party_ordersConnection_edges_node['size']; + }) => { + if (value === undefined || !data || !data.market) { + return undefined; + } const prefix = data.side === Side.Buy ? '+' : '-'; return ( prefix + addDecimal(value, data.market.positionDecimalPlaces) @@ -142,11 +156,20 @@ export const OrderListTable = forwardRef( { - if (value === OrderStatus.Rejected) { - return `${value}: ${formatLabel(data.rejectionReason)}`; + valueFormatter={({ + value, + data, + }: OrderListTableValueFormatterParams & { + value?: Orders_party_ordersConnection_edges_node['status']; + }) => { + if (value === undefined || !data || !data.market) { + return undefined; + } + if (value === OrderStatus.Rejected) { + return `${value}: ${ + data.rejectionReason && formatLabel(data.rejectionReason) + }`; } - return value; }} /> @@ -154,10 +177,18 @@ export const OrderListTable = forwardRef( headerName={t('Filled')} field="remaining" cellClass="font-mono" - valueFormatter={({ data }: ValueFormatterParams) => { + valueFormatter={({ + data, + value, + }: OrderListTableValueFormatterParams & { + value?: Orders_party_ordersConnection_edges_node['remaining']; + }) => { + if (value === undefined || !data || !data.market) { + return undefined; + } const dps = data.market.positionDecimalPlaces; const size = new BigNumber(data.size); - const remaining = new BigNumber(data.remaining); + const remaining = new BigNumber(value); const fills = size.minus(remaining); return `${addDecimal(fills.toString(), dps)}/${addDecimal( size.toString(), @@ -168,8 +199,18 @@ export const OrderListTable = forwardRef( { - if (data.type === 'Market') { + valueFormatter={({ + value, + data, + }: OrderListTableValueFormatterParams & { + value?: Orders_party_ordersConnection_edges_node['price']; + }) => { + if ( + value === undefined || + !data || + !data.market || + data.type === 'Market' + ) { return '-'; } return addDecimal(value, data.market.decimalPlaces); @@ -177,7 +218,15 @@ export const OrderListTable = forwardRef( /> { + valueFormatter={({ + value, + data, + }: OrderListTableValueFormatterParams & { + value?: Orders_party_ordersConnection_edges_node['timeInForce']; + }) => { + if (value === undefined || !data || !data.market) { + return undefined; + } if (value === OrderTimeInForce.GTT && data.expiresAt) { const expiry = getDateTimeFormat().format( new Date(data.expiresAt) @@ -190,13 +239,21 @@ export const OrderListTable = forwardRef( /> { - return getDateTimeFormat().format(new Date(value)); + valueFormatter={({ + value, + }: OrderListTableValueFormatterParams & { + value?: Orders_party_ordersConnection_edges_node['createdAt']; + }) => { + return value ? getDateTimeFormat().format(new Date(value)) : value; }} /> { + valueFormatter={({ + value, + }: OrderListTableValueFormatterParams & { + value?: Orders_party_ordersConnection_edges_node['updatedAt']; + }) => { return value ? getDateTimeFormat().format(new Date(value)) : '-'; }} /> diff --git a/libs/react-helpers/src/index.ts b/libs/react-helpers/src/index.ts index 87c27d7b9..e98c785b4 100644 --- a/libs/react-helpers/src/index.ts +++ b/libs/react-helpers/src/index.ts @@ -1,10 +1,11 @@ export * from './hooks'; export * from './lib/context'; +export * from './lib/determine-id'; export * from './lib/format'; export * from './lib/generic-data-provider'; export * from './lib/grid'; export * from './lib/i18n'; +export * from './lib/pagination'; export * from './lib/remove-0x'; export * from './lib/storage'; export * from './lib/validate'; -export * from './lib/determine-id'; diff --git a/libs/react-helpers/src/lib/pagination.ts b/libs/react-helpers/src/lib/pagination.ts new file mode 100644 index 000000000..0ecb403bf --- /dev/null +++ b/libs/react-helpers/src/lib/pagination.ts @@ -0,0 +1,53 @@ +import type { IGetRowsParams } from 'ag-grid-community'; +import type { Load } from './generic-data-provider'; +import type { MutableRefObject } from 'react'; + +const getLastRow = ( + startRow: number, + endRow: number, + blockLength: number, + totalCount?: number +) => { + let lastRow = -1; + if (totalCount !== undefined) { + if (!totalCount) { + lastRow = 0; + } else if (totalCount <= endRow) { + lastRow = totalCount; + } + } else if (blockLength < endRow - startRow) { + lastRow = blockLength; + } + return lastRow; +}; + +export const makeInfiniteScrollGetRows = + ( // eslint-disable-line @typescript-eslint/no-explicit-any + newRows: MutableRefObject, + data: MutableRefObject<(T | null)[] | null>, + totalCount: MutableRefObject, + load: Load<(T | null)[]> + ) => + async ({ + successCallback, + failCallback, + startRow, + endRow, + }: IGetRowsParams) => { + startRow += newRows.current; + endRow += newRows.current; + try { + if (data.current && data.current.indexOf(null) < endRow) { + await load(); + } + const rowsThisBlock = data.current + ? data.current.slice(startRow, endRow).map((edge) => edge?.node) + : []; + successCallback( + rowsThisBlock, + getLastRow(startRow, endRow, rowsThisBlock.length, totalCount.current) + ); + } catch (e) { + failCallback(); + } + }; diff --git a/libs/trades/src/lib/trades-container.tsx b/libs/trades/src/lib/trades-container.tsx index 0a49b59c9..274c4efa9 100644 --- a/libs/trades/src/lib/trades-container.tsx +++ b/libs/trades/src/lib/trades-container.tsx @@ -1,12 +1,11 @@ -import { useDataProvider } from '@vegaprotocol/react-helpers'; +import { + useDataProvider, + makeInfiniteScrollGetRows, +} from '@vegaprotocol/react-helpers'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import type { AgGridReact } from 'ag-grid-react'; import { useCallback, useMemo, useRef } from 'react'; -import type { - IGetRowsParams, - BodyScrollEvent, - BodyScrollEndEvent, -} from 'ag-grid-community'; +import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community'; import { MAX_TRADES, tradesDataProvider as dataProvider, @@ -100,36 +99,13 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => { 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 getRows = + makeInfiniteScrollGetRows( + newRows, + dataRef, + totalCountRef, + load + ); const onBodyScrollEnd = (event: BodyScrollEndEvent) => { if (event.top === 0) {