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
This commit is contained in:
Bartłomiej Głownia 2022-07-21 15:25:37 +02:00 committed by GitHub
parent be7690a73e
commit b88fda787c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 562 additions and 870 deletions

View File

@ -70,10 +70,10 @@ describe('orders', () => {
it('orders are sorted by most recent order', () => { it('orders are sorted by most recent order', () => {
const expectedOrderList = [ const expectedOrderList = [
'UNIDAI.MF21', 'AAVEDAI.MF21',
'TSLA.QM21', 'TSLA.QM21',
'BTCUSD.MF21', 'BTCUSD.MF21',
'AAVEDAI.MF21', 'UNIDAI.MF21',
]; ];
cy.getByTestId('tab-orders') cy.getByTestId('tab-orders')

View File

@ -1,6 +1,9 @@
import merge from 'lodash/merge'; import merge from 'lodash/merge';
import type { PartialDeep } from 'type-fest'; 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 { import {
OrderStatus, OrderStatus,
OrderTimeInForce, OrderTimeInForce,
@ -9,7 +12,7 @@ import {
} from '@vegaprotocol/types'; } from '@vegaprotocol/types';
export const generateOrders = (override?: PartialDeep<Orders>): Orders => { export const generateOrders = (override?: PartialDeep<Orders>): Orders => {
const orders: Orders_party_orders[] = [ const orders: Orders_party_ordersConnection_edges_node[] = [
{ {
__typename: 'Order', __typename: 'Order',
id: '066468C06549101DAF7BC51099E1412A0067DC08C246B7D8013C9D0CBF1E8EE7', id: '066468C06549101DAF7BC51099E1412A0067DC08C246B7D8013C9D0CBF1E8EE7',
@ -34,7 +37,7 @@ export const generateOrders = (override?: PartialDeep<Orders>): Orders => {
remaining: '0', remaining: '0',
price: '20000000', price: '20000000',
timeInForce: OrderTimeInForce.GTC, timeInForce: OrderTimeInForce.GTC,
createdAt: new Date(2020, 1, 1).toISOString(), createdAt: new Date(2020, 1, 30).toISOString(),
updatedAt: null, updatedAt: null,
expiresAt: null, expiresAt: null,
rejectionReason: null, rejectionReason: null,
@ -63,7 +66,7 @@ export const generateOrders = (override?: PartialDeep<Orders>): Orders => {
remaining: '0', remaining: '0',
price: '100', price: '100',
timeInForce: OrderTimeInForce.GTC, timeInForce: OrderTimeInForce.GTC,
createdAt: new Date().toISOString(), createdAt: new Date(2020, 1, 29).toISOString(),
updatedAt: null, updatedAt: null,
expiresAt: null, expiresAt: null,
rejectionReason: null, rejectionReason: null,
@ -92,7 +95,7 @@ export const generateOrders = (override?: PartialDeep<Orders>): Orders => {
remaining: '0', remaining: '0',
price: '20000', price: '20000',
timeInForce: OrderTimeInForce.GTC, timeInForce: OrderTimeInForce.GTC,
createdAt: new Date(2022, 5, 10).toISOString(), createdAt: new Date(2020, 1, 28).toISOString(),
updatedAt: null, updatedAt: null,
expiresAt: null, expiresAt: null,
rejectionReason: null, rejectionReason: null,
@ -121,17 +124,35 @@ export const generateOrders = (override?: PartialDeep<Orders>): Orders => {
remaining: '0', remaining: '0',
price: '100000', price: '100000',
timeInForce: OrderTimeInForce.GTC, timeInForce: OrderTimeInForce.GTC,
createdAt: new Date(2022, 7, 15).toISOString(), createdAt: new Date(2020, 1, 27).toISOString(),
updatedAt: null, updatedAt: null,
expiresAt: null, expiresAt: null,
rejectionReason: null, rejectionReason: null,
}, },
]; ];
const defaultResult = { const defaultResult: Orders = {
party: { party: {
id: Cypress.env('VEGA_PUBLIC_KEY'), 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', __typename: 'Party',
}, },
}; };

View File

@ -1,13 +1,12 @@
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { useCallback, useRef, useMemo } from '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 { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { FillsTable } from './fills-table'; import { FillsTable } from './fills-table';
import type { import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community';
IGetRowsParams,
BodyScrollEvent,
BodyScrollEndEvent,
} from 'ag-grid-community';
import { fillsDataProvider as dataProvider } from './fills-data-provider'; import { fillsDataProvider as dataProvider } from './fills-data-provider';
import type { Fills_party_tradesConnection_edges } from './__generated__/Fills'; import type { Fills_party_tradesConnection_edges } from './__generated__/Fills';
@ -90,36 +89,12 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => {
totalCountRef.current = totalCount; totalCountRef.current = totalCount;
dataRef.current = data; dataRef.current = data;
const getRows = async ({ const getRows = makeInfiniteScrollGetRows<Fills_party_tradesConnection_edges>(
successCallback, newRows,
failCallback, dataRef,
startRow, totalCountRef,
endRow, load
}: 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) => { const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
if (event.top === 0) { if (event.top === 0) {

View File

@ -3,15 +3,12 @@ import type { Props } from './fills-table';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { makeInfiniteScrollGetRows } from '@vegaprotocol/react-helpers';
import { FillsTable } from './fills-table'; import { FillsTable } from './fills-table';
import { generateFills, generateFill } from './test-helpers'; import { generateFills, generateFill } from './test-helpers';
import type { Fills_party_tradesConnection_edges } from './__generated__/Fills'; import type { Fills_party_tradesConnection_edges } from './__generated__/Fills';
import type { FillsSub_trades } from './__generated__/FillsSub'; import type { FillsSub_trades } from './__generated__/FillsSub';
import type { import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community';
IGetRowsParams,
BodyScrollEvent,
BodyScrollEndEvent,
} from 'ag-grid-community';
export default { export default {
component: FillsTable, component: FillsTable,
@ -73,7 +70,7 @@ const useDataProvider = ({
const insertionData = getData(start, end); const insertionData = getData(start, end);
data.splice(start, end - start, ...insertionData); data.splice(start, end - start, ...insertionData);
insert({ data, totalCount, insertionData }); insert({ data, totalCount, insertionData });
return Promise.resolve(); return Promise.resolve(insertionData);
}, },
totalCount, totalCount,
}; };
@ -152,39 +149,12 @@ const PaginationManager = ({ pagination }: PaginationManagerProps) => {
totalCountRef.current = totalCount; totalCountRef.current = totalCount;
dataRef.current = data; dataRef.current = data;
const getRows = async ({ const getRows = makeInfiniteScrollGetRows<Fills_party_tradesConnection_edges>(
successCallback, newRows,
failCallback, dataRef,
startRow, totalCountRef,
endRow, load
}: 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) => { const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
if (event.top === 0) { if (event.top === 0) {
@ -313,36 +283,12 @@ const InfiniteScrollManager = () => {
totalCountRef.current = totalCount; totalCountRef.current = totalCount;
dataRef.current = data; dataRef.current = data;
const getRows = async ({ const getRows = makeInfiniteScrollGetRows<Fills_party_tradesConnection_edges>(
successCallback, newRows,
failCallback, dataRef,
startRow, totalCountRef,
endRow, load
}: 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) => { const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
if (event.top === 0) { if (event.top === 0) {

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -1,5 +1,3 @@
export * from './__generated__';
export * from './mocks';
export * from './order-data-provider'; export * from './order-data-provider';
export * from './order-list'; export * from './order-list';
export * from './order-list-manager'; export * from './order-list-manager';

View File

@ -1,28 +1,15 @@
import merge from 'lodash/merge'; import merge from 'lodash/merge';
import type { PartialDeep } from 'type-fest';
import { import {
OrderStatus, OrderStatus,
OrderTimeInForce, OrderTimeInForce,
OrderType, OrderType,
Side, Side,
} from '@vegaprotocol/types'; } 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>): Orders => { export const generateOrder = (
const orders: Orders_party_orders[] = generateOrdersArray(); partialOrder: Partial<Orders_party_ordersConnection_edges_node>
) =>
const defaultResult = {
party: {
id: 'party-id',
orders,
__typename: 'Party',
},
};
return merge(defaultResult, override);
};
export const generateOrder = (partialOrder?: Partial<Orders_party_orders>) =>
merge( merge(
{ {
__typename: 'Order', __typename: 'Order',
@ -52,7 +39,7 @@ export const generateOrder = (partialOrder?: Partial<Orders_party_orders>) =>
updatedAt: null, updatedAt: null,
expiresAt: null, expiresAt: null,
rejectionReason: null, rejectionReason: null,
} as Orders_party_orders, } as Orders_party_ordersConnection_edges_node,
partialOrder partialOrder
); );
@ -71,7 +58,8 @@ export const marketOrder = generateOrder({
status: OrderStatus.Active, status: OrderStatus.Active,
}); });
export const generateMockOrders = (): Orders_party_orders[] => { export const generateMockOrders =
(): Orders_party_ordersConnection_edges_node[] => {
return [ return [
generateOrder({ generateOrder({
id: '066468C06549101DAF7BC51099E1412A0067DC08C246B7D8013C9D0CBF1E8EE7', id: '066468C06549101DAF7BC51099E1412A0067DC08C246B7D8013C9D0CBF1E8EE7',
@ -151,6 +139,7 @@ export const generateMockOrders = (): Orders_party_orders[] => {
]; ];
}; };
export const generateOrdersArray = (): Orders_party_orders[] => { export const generateOrdersArray =
(): Orders_party_ordersConnection_edges_node[] => {
return [marketOrder, limitOrder, ...generateMockOrders()]; return [marketOrder, limitOrder, ...generateMockOrders()];
}; };

View File

@ -3,13 +3,13 @@
// @generated // @generated
// This file was automatically generated and should not be edited. // 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 // GraphQL query operation: Orders
// ==================================================== // ====================================================
export interface Orders_party_orders_market_tradableInstrument_instrument { export interface Orders_party_ordersConnection_edges_node_market_tradableInstrument_instrument {
__typename: "Instrument"; __typename: "Instrument";
/** /**
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string) * 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; code: string;
} }
export interface Orders_party_orders_market_tradableInstrument { export interface Orders_party_ordersConnection_edges_node_market_tradableInstrument {
__typename: "TradableInstrument"; __typename: "TradableInstrument";
/** /**
* An instance of or reference to a fully specified instrument. * 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"; __typename: "Market";
/** /**
* Market ID * Market ID
@ -61,10 +61,10 @@ export interface Orders_party_orders_market {
/** /**
* An instance of or reference to a tradable instrument. * 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"; __typename: "Order";
/** /**
* Hash of the order data * 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) * 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) * Type the order type (defaults to PARTY)
*/ */
@ -120,6 +120,32 @@ export interface Orders_party_orders {
updatedAt: string | null; 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 { export interface Orders_party {
__typename: "Party"; __typename: "Party";
/** /**
@ -129,7 +155,7 @@ export interface Orders_party {
/** /**
* Orders relating to a party * Orders relating to a party
*/ */
orders: Orders_party_orders[] | null; ordersConnection: Orders_party_ordersConnection;
} }
export interface Orders { export interface Orders {
@ -141,4 +167,5 @@ export interface Orders {
export interface OrdersVariables { export interface OrdersVariables {
partyId: string; partyId: string;
pagination?: Pagination | null;
} }

View File

@ -1 +1,2 @@
export * from './__generated__';
export * from './order-data-provider'; export * from './order-data-provider';

View File

@ -1,88 +1,59 @@
import { import { update } from './order-data-provider';
OrderType, import type { OrderSub_orders, Orders_party_ordersConnection_edges } from '../';
OrderStatus, describe('order data provider', () => {
Side, it('puts incoming data in proper place', () => {
OrderTimeInForce, const data = [
} from '@vegaprotocol/types'; {
import type { Orders_party_orders } from '../__generated__/Orders'; node: {
import { sortOrders } from './order-data-provider'; id: '1',
updatedAt: new Date('2022-01-31').toISOString(),
createdAt: new Date('2022-01-29').toISOString(),
},
},
{
node: {
id: '2',
createdAt: new Date('2022-01-30').toISOString(),
},
},
] as Orders_party_ordersConnection_edges[];
const marketOrder: Orders_party_orders = { const delta = [
__typename: 'Order', // this one should be dropped because id don't exits and it's older than newest
id: 'order-id', {
market: { id: '0',
__typename: 'Market', createdAt: new Date('2022-01-30').toISOString(),
id: 'market-id',
name: 'market-name',
decimalPlaces: 2,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
code: 'instrument-code',
}, },
// 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(),
}, },
size: '10', // this should be added
type: OrderType.Market, {
status: OrderStatus.Active, id: '4',
side: Side.Buy, createdAt: new Date('2022-02-04').toISOString(),
remaining: '5', },
price: '', // this should be move to top
timeInForce: OrderTimeInForce.IOC, {
createdAt: new Date('2022-2-3').toISOString(), id: '2',
updatedAt: null, updatedAt: new Date('2022-02-03').toISOString(),
expiresAt: null, createdAt: new Date('2022-01-29').toISOString(),
rejectionReason: null, },
}; ] as OrderSub_orders[];
const limitOrder: Orders_party_orders = { const updatedData = update(data, delta);
__typename: 'Order', expect(updatedData.findIndex((edge) => edge.node === delta[0])).toEqual(-1);
id: 'order-id', expect(updatedData[2].node.id).toEqual(delta[2].id);
market: { expect(updatedData[2].node.updatedAt).toEqual(delta[2].updatedAt);
__typename: 'Market', expect(updatedData[0].node).toEqual(delta[3]);
id: 'market-id', expect(updatedData[1].node.id).toEqual(delta[4].id);
name: 'market-name', expect(updatedData[1].node.updatedAt).toEqual(delta[4].updatedAt);
decimalPlaces: 2,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
code: 'instrument-code',
},
},
},
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,
};
describe('OrderDataProvider', () => {
const orders = [marketOrder, limitOrder];
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,
]);
});
}); });
}); });

View File

@ -1,11 +1,18 @@
import produce from 'immer'; import produce from 'immer';
import { gql } from '@apollo/client'; 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 orderBy from 'lodash/orderBy';
import uniqBy from 'lodash/uniqBy'; 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` const ORDER_FRAGMENT = gql`
fragment OrderFields on Order { fragment OrderFields on Order {
@ -37,12 +44,23 @@ const ORDER_FRAGMENT = gql`
export const ORDERS_QUERY = gql` export const ORDERS_QUERY = gql`
${ORDER_FRAGMENT} ${ORDER_FRAGMENT}
query Orders($partyId: ID!) { query Orders($partyId: ID!, $pagination: Pagination) {
party(id: $partyId) { party(id: $partyId) {
id id
orders { ordersConnection(pagination: $pagination) {
edges {
node {
...OrderFields ...OrderFields
} }
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
} }
} }
`; `;
@ -56,53 +74,55 @@ export const ORDERS_SUB = gql`
} }
`; `;
export const update = (
data: Orders_party_ordersConnection_edges[],
delta: OrderFields[]
) => {
return produce(data, (draft) => {
// A single update can contain the same order with multiple updates, so we need to find // 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 // the latest version of the order and only update using that
export const sortOrders = (orders: OrderFields[]) => { const incoming = uniqBy(
return orderBy( orderBy(delta, (order) => order.updatedAt || order.createdAt, 'desc'),
orders, 'id'
(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[]) => {
return produce(data, (draft) => {
const incoming = prepareIncomingOrders(delta);
// Add or update incoming orders // Add or update incoming orders
incoming.forEach((order) => { incoming.reverse().forEach((node) => {
const index = draft.findIndex((o) => o.id === order.id); const index = draft.findIndex((edge) => edge.node.id === node.id);
if (index === -1) { const newer =
draft.unshift(order); (node.updatedAt || node.createdAt) >=
} else { (draft[0].node.updatedAt || draft[0].node.createdAt);
draft[index] = order; 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 => const getData = (
responseData?.party?.orders || null; responseData: Orders
): Orders_party_ordersConnection_edges[] | null =>
responseData?.party?.ordersConnection.edges || null;
const getDelta = (subscriptionData: OrderSub) => subscriptionData.orders || []; const getDelta = (subscriptionData: OrderSub) => subscriptionData.orders || [];
const getPageInfo = (responseData: Orders): PageInfo | null =>
responseData.party?.ordersConnection.pageInfo || null;
export const ordersDataProvider = makeDataProvider( export const ordersDataProvider = makeDataProvider(
ORDERS_QUERY, ORDERS_QUERY,
ORDERS_SUB, ORDERS_SUB,
update, update,
getData, getData,
getDelta getDelta,
{
getPageInfo,
append,
first: 100,
}
); );

View File

@ -1,8 +1,8 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { OrderListManager } from './order-list-manager'; import { OrderListManager } from './order-list-manager';
import * as useDataProviderHook from '@vegaprotocol/react-helpers'; import * as useDataProviderHook from '@vegaprotocol/react-helpers';
import type { Orders_party_orders } from '../__generated__/Orders'; import type { Orders_party_ordersConnection_edges_node } from '../';
import * as orderListMock from '../order-list'; import * as orderListMock from '../';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
jest.mock('./order-list'); jest.mock('./order-list');
@ -12,6 +12,10 @@ it('Renders a loading state while awaiting orders', () => {
data: [], data: [],
loading: true, loading: true,
error: undefined, error: undefined,
flush: jest.fn(),
reload: jest.fn(),
load: jest.fn(),
totalCount: 0,
}); });
render(<OrderListManager partyId="0x123" />); render(<OrderListManager partyId="0x123" />);
expect(screen.getByText('Loading...')).toBeInTheDocument(); expect(screen.getByText('Loading...')).toBeInTheDocument();
@ -23,6 +27,10 @@ it('Renders an error state', () => {
data: [], data: [],
loading: false, loading: false,
error: new Error(errorMsg), error: new Error(errorMsg),
flush: jest.fn(),
reload: jest.fn(),
load: jest.fn(),
totalCount: undefined,
}); });
render(<OrderListManager partyId="0x123" />); render(<OrderListManager partyId="0x123" />);
expect( expect(
@ -35,9 +43,13 @@ it('Renders the order list if orders provided', async () => {
// avoid warnings about padding refs // avoid warnings about padding refs
orderListMock.OrderList = forwardRef(() => <div>OrderList</div>); orderListMock.OrderList = forwardRef(() => <div>OrderList</div>);
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({ jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
data: [{ id: '1' } as Orders_party_orders], data: [{ id: '1' } as Orders_party_ordersConnection_edges_node],
loading: false, loading: false,
error: undefined, error: undefined,
flush: jest.fn(),
reload: jest.fn(),
load: jest.fn(),
totalCount: undefined,
}); });
render(<OrderListManager partyId="0x123" />); render(<OrderListManager partyId="0x123" />);
expect(await screen.findByText('OrderList')).toBeInTheDocument(); expect(await screen.findByText('OrderList')).toBeInTheDocument();

View File

@ -1,16 +1,14 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { OrderList } from '../order-list';
import type { OrderFields } from '../__generated__/OrderFields';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { import {
ordersDataProvider as dataProvider, useDataProvider,
prepareIncomingOrders, makeInfiniteScrollGetRows,
sortOrders, } from '@vegaprotocol/react-helpers';
} from '../order-data-provider';
import { useCallback, useMemo, useRef } from 'react'; import { useCallback, useMemo, useRef } from 'react';
import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community';
import type { AgGridReact } from 'ag-grid-react'; 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 { interface OrderListManagerProps {
partyId: string; partyId: string;
@ -18,62 +16,105 @@ interface OrderListManagerProps {
export const OrderListManager = ({ partyId }: OrderListManagerProps) => { export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const dataRef = useRef<(Orders_party_ordersConnection_edges | null)[] | null>(
null
);
const totalCountRef = useRef<number | undefined>(undefined);
const newRows = useRef(0);
const scrolledToTop = useRef(true);
const variables = useMemo(() => ({ partyId }), [partyId]); const variables = useMemo(() => ({ partyId }), [partyId]);
// Apply updates to the table const addNewRows = useCallback(() => {
const update = useCallback(({ delta }: { delta: OrderSub_orders[] }) => { if (newRows.current === 0) {
if (!gridRef.current) { return;
return false;
} }
const incoming = prepareIncomingOrders(delta); if (totalCountRef.current !== undefined) {
totalCountRef.current += newRows.current;
const updateRows: OrderFields[] = []; }
const add: OrderFields[] = []; newRows.current = 0;
incoming.forEach((d) => {
if (!gridRef.current?.api) { if (!gridRef.current?.api) {
return; return;
} }
gridRef.current.api.refreshInfiniteCache();
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,
});
}
return true;
}, []); }, []);
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, dataProvider,
update, update,
insert,
variables, variables,
}); });
totalCountRef.current = totalCount;
dataRef.current = data;
const orders = useMemo(() => { const getRows =
if (!data) { makeInfiniteScrollGetRows<Orders_party_ordersConnection_edges>(
return null; 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 <OrderList showCancelled={false} to hide cancelled orders
return ( return (
<AsyncRenderer loading={loading} error={error} data={orders}> <AsyncRenderer loading={loading} error={error} data={data}>
<OrderList ref={gridRef} data={orders} /> <OrderList
ref={gridRef}
rowModelType="infinite"
datasource={{ getRows }}
onBodyScrollEnd={onBodyScrollEnd}
onBodyScroll={onBodyScroll}
/>
</AsyncRenderer> </AsyncRenderer>
); );
}; };

View File

@ -4,24 +4,25 @@ import {
formatLabel, formatLabel,
getDateTimeFormat, getDateTimeFormat,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import type { Orders_party_orders } from '../__generated__/Orders';
import { OrderStatus, OrderRejectionReason } from '@vegaprotocol/types'; import { OrderStatus, OrderRejectionReason } from '@vegaprotocol/types';
import { OrderListTable } from './order-list';
import type { PartialDeep } from 'type-fest'; import type { PartialDeep } from 'type-fest';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet'; import { VegaWalletContext } from '@vegaprotocol/wallet';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { OrderListTable } from '../';
import type { Orders_party_ordersConnection_edges_node } from '../';
import { limitOrder, marketOrder } from '../mocks/generate-orders'; import { limitOrder, marketOrder } from '../mocks/generate-orders';
const generateJsx = ( const generateJsx = (
orders: Orders_party_orders[] | null, orders: Orders_party_ordersConnection_edges_node[] | null,
context: PartialDeep<VegaWalletContextShape> = { keypair: { pub: '0x123' } } context: PartialDeep<VegaWalletContextShape> = { keypair: { pub: '0x123' } }
) => { ) => {
return ( return (
<MockedProvider> <MockedProvider>
<VegaWalletContext.Provider value={context as VegaWalletContextShape}> <VegaWalletContext.Provider value={context as VegaWalletContextShape}>
<OrderListTable <OrderListTable
data={orders} rowData={orders}
cancel={jest.fn()} cancel={jest.fn()}
setEditOrderDialogOpen={jest.fn()} setEditOrderDialogOpen={jest.fn()}
setEditOrder={jest.fn()} setEditOrder={jest.fn()}

View File

@ -16,7 +16,7 @@ const Template: Story = (args) => {
return ( return (
<div style={{ height: 1000 }}> <div style={{ height: 1000 }}>
<OrderListTable <OrderListTable
data={args.data} rowData={args.data}
cancel={cancel} cancel={cancel}
setEditOrderDialogOpen={() => { setEditOrderDialogOpen={() => {
return; return;
@ -55,7 +55,7 @@ const Template2: Story = (args) => {
<> <>
<div style={{ height: 1000 }}> <div style={{ height: 1000 }}>
<OrderListTable <OrderListTable
data={args.data} rowData={args.data}
cancel={cancel} cancel={cancel}
setEditOrderDialogOpen={() => { setEditOrderDialogOpen={() => {
return; return;

View File

@ -1,5 +1,4 @@
import { OrderTimeInForce, OrderStatus, Side } from '@vegaprotocol/types'; import { OrderTimeInForce, OrderStatus, Side } from '@vegaprotocol/types';
import type { Orders_party_orders } from '../__generated__/Orders';
import { import {
addDecimal, addDecimal,
formatLabel, formatLabel,
@ -11,27 +10,29 @@ import type {
ICellRendererParams, ICellRendererParams,
ValueFormatterParams, ValueFormatterParams,
} from 'ag-grid-community'; } 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 { AgGridColumn } from 'ag-grid-react';
import { forwardRef, useState } from 'react'; import { forwardRef, useState } from 'react';
import type { Orders_party_ordersConnection_edges_node } from '../';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { useOrderCancel } from '../../order-hooks/use-order-cancel'; import { useOrderCancel } from '../../order-hooks/use-order-cancel';
import { VegaTransactionDialog } from '@vegaprotocol/wallet'; import { VegaTransactionDialog } from '@vegaprotocol/wallet';
import { useOrderEdit } from '../../order-hooks/use-order-edit'; import { useOrderEdit } from '../../order-hooks/use-order-edit';
import { OrderEditDialog } from './order-edit-dialog'; import { OrderEditDialog } from './order-edit-dialog';
interface OrderListProps { type OrderListProps = AgGridReactProps | AgReactUiProps;
data: Orders_party_orders[] | null;
showCancelled?: boolean;
}
export const OrderList = forwardRef<AgGridReact, OrderListProps>( export const OrderList = forwardRef<AgGridReact, OrderListProps>(
({ data, showCancelled = true }, ref) => { (props, ref) => {
const [cancelOrderDialogOpen, setCancelOrderDialogOpen] = useState(false); const [cancelOrderDialogOpen, setCancelOrderDialogOpen] = useState(false);
const [editOrderDialogOpen, setEditOrderDialogOpen] = useState(false); const [editOrderDialogOpen, setEditOrderDialogOpen] = useState(false);
const [editOrder, setEditOrder] = useState<Orders_party_orders | null>( const [editOrder, setEditOrder] =
null useState<Orders_party_ordersConnection_edges_node | null>(null);
);
const { transaction, updatedOrder, reset, cancel } = useOrderCancel(); const { transaction, updatedOrder, reset, cancel } = useOrderCancel();
const { const {
@ -40,9 +41,6 @@ export const OrderList = forwardRef<AgGridReact, OrderListProps>(
reset: resetEdit, reset: resetEdit,
edit, edit,
} = useOrderEdit(); } = useOrderEdit();
const ordersData = showCancelled
? data
: data?.filter((o) => o.status !== OrderStatus.Cancelled) || null;
const getCancelDialogTitle = (status?: string) => { const getCancelDialogTitle = (status?: string) => {
switch (status) { switch (status) {
case OrderStatus.Cancelled: case OrderStatus.Cancelled:
@ -70,7 +68,7 @@ export const OrderList = forwardRef<AgGridReact, OrderListProps>(
return ( return (
<> <>
<OrderListTable <OrderListTable
data={ordersData} {...props}
cancel={cancel} cancel={cancel}
ref={ref} ref={ref}
setEditOrderDialogOpen={setEditOrderDialogOpen} setEditOrderDialogOpen={setEditOrderDialogOpen}
@ -105,24 +103,32 @@ export const OrderList = forwardRef<AgGridReact, OrderListProps>(
} }
); );
interface OrderListTableProps { type OrderListTableValueFormatterParams = Omit<
data: Orders_party_orders[] | null; ValueFormatterParams,
'data' | 'value'
> & {
data: Orders_party_ordersConnection_edges_node | null;
};
type OrderListTableProps = (AgGridReactProps | AgReactUiProps) & {
cancel: (body?: unknown) => Promise<unknown>; cancel: (body?: unknown) => Promise<unknown>;
setEditOrderDialogOpen: (value: boolean) => void; setEditOrderDialogOpen: (value: boolean) => void;
setEditOrder: (order: Orders_party_orders | null) => void; setEditOrder: (
} order: Orders_party_ordersConnection_edges_node | null
) => void;
};
export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>( export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
({ data, cancel, setEditOrderDialogOpen, setEditOrder }, ref) => { ({ cancel, setEditOrderDialogOpen, setEditOrder, ...props }, ref) => {
return ( return (
<AgGrid <AgGrid
ref={ref} ref={ref}
rowData={data}
overlayNoRowsTemplate="No orders" overlayNoRowsTemplate="No orders"
defaultColDef={{ flex: 1, resizable: true }} defaultColDef={{ flex: 1, resizable: true }}
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
getRowId={({ data }) => data.id} getRowId={({ data }) => data.id}
rowHeight={40} rowHeight={40}
{...props}
> >
<AgGridColumn <AgGridColumn
headerName={t('Market')} headerName={t('Market')}
@ -132,7 +138,15 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
headerName={t('Amount')} headerName={t('Amount')}
field="size" field="size"
cellClass="font-mono" 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 ? '+' : '-'; const prefix = data.side === Side.Buy ? '+' : '-';
return ( return (
prefix + addDecimal(value, data.market.positionDecimalPlaces) prefix + addDecimal(value, data.market.positionDecimalPlaces)
@ -142,11 +156,20 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
<AgGridColumn field="type" /> <AgGridColumn field="type" />
<AgGridColumn <AgGridColumn
field="status" field="status"
valueFormatter={({ value, data }: ValueFormatterParams) => { valueFormatter={({
if (value === OrderStatus.Rejected) { value,
return `${value}: ${formatLabel(data.rejectionReason)}`; 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; return value;
}} }}
/> />
@ -154,10 +177,18 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
headerName={t('Filled')} headerName={t('Filled')}
field="remaining" field="remaining"
cellClass="font-mono" 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 dps = data.market.positionDecimalPlaces;
const size = new BigNumber(data.size); const size = new BigNumber(data.size);
const remaining = new BigNumber(data.remaining); const remaining = new BigNumber(value);
const fills = size.minus(remaining); const fills = size.minus(remaining);
return `${addDecimal(fills.toString(), dps)}/${addDecimal( return `${addDecimal(fills.toString(), dps)}/${addDecimal(
size.toString(), size.toString(),
@ -168,8 +199,18 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
<AgGridColumn <AgGridColumn
field="price" field="price"
cellClass="font-mono" cellClass="font-mono"
valueFormatter={({ value, data }: ValueFormatterParams) => { valueFormatter={({
if (data.type === 'Market') { value,
data,
}: OrderListTableValueFormatterParams & {
value?: Orders_party_ordersConnection_edges_node['price'];
}) => {
if (
value === undefined ||
!data ||
!data.market ||
data.type === 'Market'
) {
return '-'; return '-';
} }
return addDecimal(value, data.market.decimalPlaces); return addDecimal(value, data.market.decimalPlaces);
@ -177,7 +218,15 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
/> />
<AgGridColumn <AgGridColumn
field="timeInForce" field="timeInForce"
valueFormatter={({ value, data }: ValueFormatterParams) => { 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) { if (value === OrderTimeInForce.GTT && data.expiresAt) {
const expiry = getDateTimeFormat().format( const expiry = getDateTimeFormat().format(
new Date(data.expiresAt) new Date(data.expiresAt)
@ -190,13 +239,21 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
/> />
<AgGridColumn <AgGridColumn
field="createdAt" field="createdAt"
valueFormatter={({ value }: ValueFormatterParams) => { valueFormatter={({
return getDateTimeFormat().format(new Date(value)); value,
}: OrderListTableValueFormatterParams & {
value?: Orders_party_ordersConnection_edges_node['createdAt'];
}) => {
return value ? getDateTimeFormat().format(new Date(value)) : value;
}} }}
/> />
<AgGridColumn <AgGridColumn
field="updatedAt" field="updatedAt"
valueFormatter={({ value }: ValueFormatterParams) => { valueFormatter={({
value,
}: OrderListTableValueFormatterParams & {
value?: Orders_party_ordersConnection_edges_node['updatedAt'];
}) => {
return value ? getDateTimeFormat().format(new Date(value)) : '-'; return value ? getDateTimeFormat().format(new Date(value)) : '-';
}} }}
/> />

View File

@ -1,10 +1,11 @@
export * from './hooks'; export * from './hooks';
export * from './lib/context'; export * from './lib/context';
export * from './lib/determine-id';
export * from './lib/format'; export * from './lib/format';
export * from './lib/generic-data-provider'; export * from './lib/generic-data-provider';
export * from './lib/grid'; export * from './lib/grid';
export * from './lib/i18n'; export * from './lib/i18n';
export * from './lib/pagination';
export * from './lib/remove-0x'; export * from './lib/remove-0x';
export * from './lib/storage'; export * from './lib/storage';
export * from './lib/validate'; export * from './lib/validate';
export * from './lib/determine-id';

View File

@ -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 =
<T extends { node: any }>( // eslint-disable-line @typescript-eslint/no-explicit-any
newRows: MutableRefObject<number>,
data: MutableRefObject<(T | null)[] | null>,
totalCount: MutableRefObject<number | undefined>,
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();
}
};

View File

@ -1,12 +1,11 @@
import { useDataProvider } from '@vegaprotocol/react-helpers'; import {
useDataProvider,
makeInfiniteScrollGetRows,
} from '@vegaprotocol/react-helpers';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { useCallback, useMemo, useRef } from 'react'; import { useCallback, useMemo, useRef } from 'react';
import type { import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community';
IGetRowsParams,
BodyScrollEvent,
BodyScrollEndEvent,
} from 'ag-grid-community';
import { import {
MAX_TRADES, MAX_TRADES,
tradesDataProvider as dataProvider, tradesDataProvider as dataProvider,
@ -100,36 +99,13 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
totalCountRef.current = totalCount; totalCountRef.current = totalCount;
dataRef.current = data; dataRef.current = data;
const getRows = async ({ const getRows =
successCallback, makeInfiniteScrollGetRows<Trades_market_tradesConnection_edges>(
failCallback, newRows,
startRow, dataRef,
endRow, totalCountRef,
}: IGetRowsParams) => { load
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) => { const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
if (event.top === 0) { if (event.top === 0) {