From 6ad2a7676ef03f064680adc22e28486f9516c84d Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Wed, 23 Mar 2022 12:08:10 -0700 Subject: [PATCH] Feat/84 Order list (#89) * scaffold dealticket package, remove trading views from react-helpers * add deal ticket component, add intent utils, expand dialog and form group styles * add splash component, show market not found message if market doesnt exist * tidy up error handling * add handleError method for vega tx hook * add better testname for provider test, flesh out tests a bit more for deal ticket * Add unit tests for useVegaTransaction and useOrderSubmit hooks * add wrapper component for order dialog styles * add vega styled loader to ui toolkit and use in order dialog * add title prop to order dialog * add button radio component * revert dialog styles * move splash component to ui-toolkit, add story * convert intent to enum * add date-fns, add datetime to helpers * add order types to wallet package, make price undefined if order type is market * tidy up order state by moving submit and transaction hooks out of deal ticket * add types package, delete old generated types from trading project * rename types package to graphql * add order list container and order list component * add test setup for useOrders * add test for use-orders hook * tidy unnecessary diff * regen types and use them in order-list, also change to use applytransaction hook for orderlist grid * make order table columns resizable * make market table not have highlightable cells, use splash for orders errors and loading states, unit test for orderlist container * add tests for order list table * show rejection reason and expires at depending on status and tif * add decimal places to query * only update row if data has changed, add test coverage * add setup tests file to avoid importing jest-dom for every test, add async-renderer component to handle fetch ui logic * install all of lodash but import individually to get tree shaking * add setup tests file for orderlist package * add missing fields to use orders spec mock order * fix act warnings in index page test * fix casing of app import * remove react-singleton-hook, simplify side formatting * fix linting errors --- .../src/support/pages/base-page.js | 2 +- .../step_definitions/network-page.step.js | 2 +- apps/stats-mainnet/jest.config.js | 1 + apps/stats-mainnet/src/main.tsx | 2 +- ...eportWebVitals.ts => report-web-vitals.ts} | 0 .../src/{setupTests.ts => setup-tests.ts} | 0 .../async-renderer/async-renderer.tsx | 27 +++ .../components/async-renderer/index.ts | 1 + .../deal-ticket-container/order-dialog.tsx | 5 +- .../components/order-list-container/index.ts | 1 + .../order-list-container.spec.tsx | 41 +++++ .../order-list-container.tsx | 14 ++ .../components/page-query-container/index.tsx | 20 +-- apps/trading/hooks/use-markets.ts | 17 +- apps/trading/hooks/use-orders.spec.tsx | 165 ++++++++++++++++++ apps/trading/hooks/use-orders.ts | 140 +++++++++++++++ apps/trading/jest.config.js | 1 + apps/trading/pages/_app.page.tsx | 2 - .../trading/pages/markets/[marketId].page.tsx | 2 +- apps/trading/pages/markets/grid-tabs.tsx | 2 +- apps/trading/pages/markets/index.page.tsx | 26 ++- apps/trading/pages/markets/trade-grid.tsx | 4 +- apps/trading/setup-tests.ts | 1 + apps/trading/specs/index.spec.tsx | 27 +-- libs/deal-ticket/src/deal-ticket.tsx | 3 - libs/graphql/src/__generated__/OrderFields.ts | 115 ++++++++++++ libs/graphql/src/__generated__/OrderSub.ts | 126 +++++++++++++ libs/graphql/src/__generated__/Orders.ts | 138 +++++++++++++++ libs/graphql/src/__generated__/globalTypes.ts | 20 +++ libs/graphql/src/index.ts | 3 + .../mainnet-stats-manager/src/config/types.ts | 2 + .../market-list/src/lib/market-list-table.tsx | 3 +- libs/order-list/.babelrc | 12 ++ libs/order-list/.eslintrc.json | 18 ++ libs/order-list/README.md | 7 + libs/order-list/jest.config.js | 10 ++ libs/order-list/project.json | 23 +++ libs/order-list/src/index.ts | 1 + libs/order-list/src/order-list.spec.tsx | 153 ++++++++++++++++ libs/order-list/src/order-list.tsx | 97 ++++++++++ libs/order-list/src/setup-tests.ts | 1 + libs/order-list/tsconfig.json | 25 +++ libs/order-list/tsconfig.lib.json | 22 +++ libs/order-list/tsconfig.spec.json | 19 ++ .../hooks/use-apply-grid-transaction.spec.ts | 74 ++++++++ .../src/hooks/use-apply-grid-transaction.ts | 5 +- libs/react-helpers/src/lib/format/format.ts | 4 +- package.json | 9 +- tsconfig.base.json | 1 + workspace.json | 1 + yarn.lock | 26 +-- 51 files changed, 1322 insertions(+), 99 deletions(-) rename apps/stats-mainnet/src/{reportWebVitals.ts => report-web-vitals.ts} (100%) rename apps/stats-mainnet/src/{setupTests.ts => setup-tests.ts} (100%) create mode 100644 apps/trading/components/async-renderer/async-renderer.tsx create mode 100644 apps/trading/components/async-renderer/index.ts create mode 100644 apps/trading/components/order-list-container/index.ts create mode 100644 apps/trading/components/order-list-container/order-list-container.spec.tsx create mode 100644 apps/trading/components/order-list-container/order-list-container.tsx create mode 100644 apps/trading/hooks/use-orders.spec.tsx create mode 100644 apps/trading/hooks/use-orders.ts create mode 100644 apps/trading/setup-tests.ts create mode 100644 libs/graphql/src/__generated__/OrderFields.ts create mode 100644 libs/graphql/src/__generated__/OrderSub.ts create mode 100644 libs/graphql/src/__generated__/Orders.ts create mode 100644 libs/order-list/.babelrc create mode 100644 libs/order-list/.eslintrc.json create mode 100644 libs/order-list/README.md create mode 100644 libs/order-list/jest.config.js create mode 100644 libs/order-list/project.json create mode 100644 libs/order-list/src/index.ts create mode 100644 libs/order-list/src/order-list.spec.tsx create mode 100644 libs/order-list/src/order-list.tsx create mode 100644 libs/order-list/src/setup-tests.ts create mode 100644 libs/order-list/tsconfig.json create mode 100644 libs/order-list/tsconfig.lib.json create mode 100644 libs/order-list/tsconfig.spec.json create mode 100644 libs/react-helpers/src/hooks/use-apply-grid-transaction.spec.ts diff --git a/apps/explorer-e2e/src/support/pages/base-page.js b/apps/explorer-e2e/src/support/pages/base-page.js index 344b6e125..206c94661 100644 --- a/apps/explorer-e2e/src/support/pages/base-page.js +++ b/apps/explorer-e2e/src/support/pages/base-page.js @@ -68,7 +68,7 @@ export default class BasePage { }); cy.get('.language-json') - .each(($asset, index, $list) => { + .each(($asset) => { expect($asset).to.not.be.empty; }) .then(($list) => { diff --git a/apps/explorer-e2e/src/support/step_definitions/network-page.step.js b/apps/explorer-e2e/src/support/step_definitions/network-page.step.js index 42ae9a063..8cfda1d37 100644 --- a/apps/explorer-e2e/src/support/step_definitions/network-page.step.js +++ b/apps/explorer-e2e/src/support/step_definitions/network-page.step.js @@ -1,4 +1,4 @@ -import { Given, Then, When } from 'cypress-cucumber-preprocessor/steps'; +import { Then, When } from 'cypress-cucumber-preprocessor/steps'; import NetworkPage from '../pages/network-page'; const networkPage = new NetworkPage(); diff --git a/apps/stats-mainnet/jest.config.js b/apps/stats-mainnet/jest.config.js index d4328090a..9d114d295 100644 --- a/apps/stats-mainnet/jest.config.js +++ b/apps/stats-mainnet/jest.config.js @@ -7,4 +7,5 @@ module.exports = { }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], coverageDirectory: '../../coverage/apps/stats-mainnet', + setupFilesAfterEnv: ['./src/setup-tests.ts'], }; diff --git a/apps/stats-mainnet/src/main.tsx b/apps/stats-mainnet/src/main.tsx index 9ad023d26..08e66f713 100644 --- a/apps/stats-mainnet/src/main.tsx +++ b/apps/stats-mainnet/src/main.tsx @@ -2,7 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import './styles/styles.css'; import App from './app'; -import reportWebVitals from './reportWebVitals'; +import reportWebVitals from './report-web-vitals'; ReactDOM.render( diff --git a/apps/stats-mainnet/src/reportWebVitals.ts b/apps/stats-mainnet/src/report-web-vitals.ts similarity index 100% rename from apps/stats-mainnet/src/reportWebVitals.ts rename to apps/stats-mainnet/src/report-web-vitals.ts diff --git a/apps/stats-mainnet/src/setupTests.ts b/apps/stats-mainnet/src/setup-tests.ts similarity index 100% rename from apps/stats-mainnet/src/setupTests.ts rename to apps/stats-mainnet/src/setup-tests.ts diff --git a/apps/trading/components/async-renderer/async-renderer.tsx b/apps/trading/components/async-renderer/async-renderer.tsx new file mode 100644 index 000000000..803d59401 --- /dev/null +++ b/apps/trading/components/async-renderer/async-renderer.tsx @@ -0,0 +1,27 @@ +import { Splash } from '@vegaprotocol/ui-toolkit'; +import { ReactNode } from 'react'; + +interface AsyncRendererProps { + loading: boolean; + error: Error | undefined; + data: T; + children: (data: T) => ReactNode; +} + +// eslint-disable-next-line +export function AsyncRenderer({ + loading, + error, + data, + children, +}: AsyncRendererProps) { + if (error) { + return Something went wrong: {error.message}; + } + + if (loading) { + return Loading...; + } + + return <>{children(data)}; +} diff --git a/apps/trading/components/async-renderer/index.ts b/apps/trading/components/async-renderer/index.ts new file mode 100644 index 000000000..2a3091dd4 --- /dev/null +++ b/apps/trading/components/async-renderer/index.ts @@ -0,0 +1 @@ +export * from './async-renderer'; diff --git a/apps/trading/components/deal-ticket-container/order-dialog.tsx b/apps/trading/components/deal-ticket-container/order-dialog.tsx index 4d2bb25d8..d5f0c6f67 100644 --- a/apps/trading/components/deal-ticket-container/order-dialog.tsx +++ b/apps/trading/components/deal-ticket-container/order-dialog.tsx @@ -4,10 +4,7 @@ import { TransactionState, VegaTxStatus, } from '../../hooks/use-vega-transaction'; -import { - OrderEvent_busEvents_event_Order, - OrderStatus, -} from '@vegaprotocol/graphql'; +import { OrderEvent_busEvents_event_Order } from '@vegaprotocol/graphql'; import { formatNumber } from '@vegaprotocol/react-helpers'; interface OrderDialogProps { diff --git a/apps/trading/components/order-list-container/index.ts b/apps/trading/components/order-list-container/index.ts new file mode 100644 index 000000000..ac2975314 --- /dev/null +++ b/apps/trading/components/order-list-container/index.ts @@ -0,0 +1 @@ +export * from './order-list-container'; diff --git a/apps/trading/components/order-list-container/order-list-container.spec.tsx b/apps/trading/components/order-list-container/order-list-container.spec.tsx new file mode 100644 index 000000000..77a64470d --- /dev/null +++ b/apps/trading/components/order-list-container/order-list-container.spec.tsx @@ -0,0 +1,41 @@ +import { OrderListContainer } from './order-list-container'; +import * as useOrdersHook from '../../hooks/use-orders'; +import { render, screen } from '@testing-library/react'; +import { Orders_party_orders } from '@vegaprotocol/graphql'; + +jest.mock('@vegaprotocol/order-list', () => ({ + OrderList: () =>
OrderList
, +})); + +test('Renders a loading state while awaiting orders', () => { + jest.spyOn(useOrdersHook, 'useOrders').mockReturnValue({ + orders: [], + loading: true, + error: null, + }); + render(); + expect(screen.getByText('Loading...')).toBeInTheDocument(); +}); + +test('Renders an error state', () => { + const errorMsg = 'Oops! An Error'; + jest.spyOn(useOrdersHook, 'useOrders').mockReturnValue({ + orders: [], + loading: false, + error: new Error(errorMsg), + }); + render(); + expect( + screen.getByText(`Something went wrong: ${errorMsg}`) + ).toBeInTheDocument(); +}); + +test('Renders the order list if orders provided', () => { + jest.spyOn(useOrdersHook, 'useOrders').mockReturnValue({ + orders: [{ id: '1' } as Orders_party_orders], + loading: false, + error: null, + }); + render(); + expect(screen.getByText('OrderList')).toBeInTheDocument(); +}); diff --git a/apps/trading/components/order-list-container/order-list-container.tsx b/apps/trading/components/order-list-container/order-list-container.tsx new file mode 100644 index 000000000..246891559 --- /dev/null +++ b/apps/trading/components/order-list-container/order-list-container.tsx @@ -0,0 +1,14 @@ +import { useOrders } from '../../hooks/use-orders'; +import { OrderList } from '@vegaprotocol/order-list'; +import { AsyncRenderer } from '../async-renderer'; +import { OrderFields } from '@vegaprotocol/graphql'; + +export const OrderListContainer = () => { + const { orders, loading, error } = useOrders(); + + return ( + loading={loading} error={error} data={orders}> + {(data) => } + + ); +}; diff --git a/apps/trading/components/page-query-container/index.tsx b/apps/trading/components/page-query-container/index.tsx index 455302743..74eb15635 100644 --- a/apps/trading/components/page-query-container/index.tsx +++ b/apps/trading/components/page-query-container/index.tsx @@ -1,7 +1,7 @@ import { OperationVariables, QueryHookOptions, useQuery } from '@apollo/client'; import { DocumentNode } from 'graphql'; import { ReactNode } from 'react'; -import { Splash } from '@vegaprotocol/ui-toolkit'; +import { AsyncRenderer } from '../async-renderer'; interface PageQueryContainerProps { query: DocumentNode; @@ -16,13 +16,13 @@ export const PageQueryContainer = ({ }: PageQueryContainerProps) => { const { data, loading, error } = useQuery(query, options); - if (loading || !data) { - return Loading...; - } - - if (error) { - return Something went wrong: {error.message}; - } - - return <>{children(data)}; + return ( + + loading={loading || Boolean(!data)} + error={error} + data={data} + > + {(data) => children(data)} + + ); }; diff --git a/apps/trading/hooks/use-markets.ts b/apps/trading/hooks/use-markets.ts index c2fd4e570..88b60e49f 100644 --- a/apps/trading/hooks/use-markets.ts +++ b/apps/trading/hooks/use-markets.ts @@ -1,5 +1,4 @@ import { gql, useApolloClient } from '@apollo/client'; -import { singletonHook } from 'react-singleton-hook'; import { Markets, Markets_markets, @@ -56,7 +55,13 @@ const MARKET_DATA_SUB = gql` } `; -export const useMarketsImpl = () => { +interface UseMarkets { + markets: Markets_markets[]; + error: Error | null; + loading: boolean; +} + +export const useMarkets = (): UseMarkets => { const client = useApolloClient(); const [markets, setMarkets] = useState([]); const [error, setError] = useState(null); @@ -121,11 +126,3 @@ export const useMarketsImpl = () => { return { markets, error, loading }; }; - -const initial = { - markets: [], - error: null, - loading: false, -}; - -export const useMarkets = singletonHook(initial, useMarketsImpl); diff --git a/apps/trading/hooks/use-orders.spec.tsx b/apps/trading/hooks/use-orders.spec.tsx new file mode 100644 index 000000000..ae3fbdf14 --- /dev/null +++ b/apps/trading/hooks/use-orders.spec.tsx @@ -0,0 +1,165 @@ +import { MockedProvider, MockedResponse } from '@apollo/client/testing'; +import { renderHook } from '@testing-library/react-hooks'; +import { + OrderFields, + Orders, + OrderStatus, + OrderSub, + OrderTimeInForce, + OrderType, + Side, +} from '@vegaprotocol/graphql'; +import { + VegaKeyExtended, + VegaWalletContext, + VegaWalletContextShape, +} from '@vegaprotocol/wallet'; +import { ReactNode } from 'react'; +import { ORDERS_QUERY, ORDERS_SUB, useOrders } from './use-orders'; + +const keypair = { pub: '0x123' } as VegaKeyExtended; +const defaultWalletContext = { + keypair, + keypairs: [keypair], + sendTx: jest.fn().mockReturnValue(Promise.resolve(null)), + connect: jest.fn(), + disconnect: jest.fn(), + selectPublicKey: jest.fn(), + connector: null, +}; + +function generateOrder(order?: Partial): OrderFields { + return { + __typename: 'Order', + id: '1', + market: { + __typename: 'Market', + id: 'market-id', + name: 'market-name', + decimalPlaces: 0, + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + code: 'instrument-code', + }, + }, + }, + type: OrderType.Market, + side: Side.Buy, + size: '10', + status: OrderStatus.Active, + rejectionReason: null, + price: '', + timeInForce: OrderTimeInForce.GTC, + remaining: '10', + createdAt: '2022-01-01T00:00:00', + updatedAt: null, + expiresAt: null, + ...order, + }; +} + +function setup( + context?: Partial, + mocks: MockedResponse[] = [] +) { + const wrapper = ({ children }: { children: ReactNode }) => ( + + + {children} + + + ); + return renderHook(() => useOrders(), { wrapper }); +} + +test('Fetches and subscribes to orders and merges appropriately', async () => { + const order = generateOrder(); + const mockOrderQuery: MockedResponse = { + request: { + query: ORDERS_QUERY, + variables: { partyId: keypair.pub }, + }, + result: { + data: { + party: { + __typename: 'Party', + id: keypair.pub, + orders: [order], + }, + }, + }, + }; + + const updatedOrder = generateOrder({ + id: '1', + remaining: '5', + updatedAt: '2022-01-01T00:01:00', + }); + const newOrder = generateOrder({ + id: '2', + createdAt: '2022-01-01T01:00:00', + }); + const mockOrderSub: MockedResponse = { + request: { + query: ORDERS_SUB, + variables: { partyId: keypair.pub }, + }, + result: { + data: { + orders: [updatedOrder, newOrder], + }, + }, + delay: 100, + }; + const { result, waitForNextUpdate } = setup(defaultWalletContext, [ + mockOrderQuery, + mockOrderSub, + ]); + expect(result.current.loading).toBe(true); + expect(result.current.error).toBe(null); + await waitForNextUpdate(); + expect(result.current.orders).toEqual([order]); + expect(result.current.loading).toBe(false); + await waitForNextUpdate(); + expect(result.current.orders).toEqual([newOrder, updatedOrder]); +}); + +test('Returns an error if fetch fails', async () => { + const error = new Error('Something failed'); + const mockFailedOrderQuery: MockedResponse = { + request: { + query: ORDERS_QUERY, + variables: { partyId: keypair.pub }, + }, + error, + }; + const { result, waitForNextUpdate } = setup(defaultWalletContext, [ + mockFailedOrderQuery, + ]); + expect(result.current.loading).toBe(true); + expect(result.current.error).toBe(null); + await waitForNextUpdate(); + expect(result.current.error).toEqual(error); + expect(result.current.loading).toBe(false); +}); + +test('No queries are made if no pubkey provided', () => { + const mockQuery: MockedResponse = { + request: { + query: ORDERS_QUERY, + variables: { partyId: keypair.pub }, + }, + newData: jest.fn(), + }; + const { result } = setup( + { ...defaultWalletContext, keypair: null, keypairs: [] }, + [mockQuery] + ); + expect(mockQuery.newData).not.toBeCalled(); + expect(result.current.loading).toBe(false); + expect(result.current.error).toBe(null); +}); diff --git a/apps/trading/hooks/use-orders.ts b/apps/trading/hooks/use-orders.ts new file mode 100644 index 000000000..734a8234d --- /dev/null +++ b/apps/trading/hooks/use-orders.ts @@ -0,0 +1,140 @@ +import { gql, useApolloClient } from '@apollo/client'; +import { useCallback, useEffect, useState } from 'react'; +import { + OrderSub, + OrderSubVariables, + Orders, + OrdersVariables, + OrderFields, +} from '@vegaprotocol/graphql'; +import uniqBy from 'lodash/uniqBy'; +import orderBy from 'lodash/orderBy'; +import { useVegaWallet } from '@vegaprotocol/wallet'; + +const ORDER_FRAGMENT = gql` + fragment OrderFields on Order { + id + market { + id + name + decimalPlaces + tradableInstrument { + instrument { + code + } + } + } + type + side + size + status + rejectionReason + price + timeInForce + remaining + expiresAt + createdAt + updatedAt + } +`; + +export const ORDERS_QUERY = gql` + ${ORDER_FRAGMENT} + query Orders($partyId: ID!) { + party(id: $partyId) { + id + orders { + ...OrderFields + } + } + } +`; + +export const ORDERS_SUB = gql` + ${ORDER_FRAGMENT} + subscription OrderSub($partyId: ID!) { + orders(partyId: $partyId) { + ...OrderFields + } + } +`; + +interface UseOrders { + orders: OrderFields[]; + error: Error | null; + loading: boolean; +} + +export const useOrders = (): UseOrders => { + const client = useApolloClient(); + const { keypair } = useVegaWallet(); + const [orders, setOrders] = useState([]); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const mergeOrders = useCallback((update: OrderFields[]) => { + // A subscription payload can contain multiple updates for a single order so we need to first + // sort them by updatedAt (or createdAt if the order hasn't been updated) with the newest first, + // then use uniqBy, which selects the first occuring order for an id to ensure we only get the latest order + setOrders((curr) => { + const sorted = orderBy( + [...curr, ...update], + (o) => { + if (!o.updatedAt) return new Date(o.createdAt).getTime(); + return new Date(o.updatedAt).getTime(); + }, + 'desc' + ); + const uniq = uniqBy(sorted, 'id'); + return uniq; + }); + }, []); + + // Make initial fetch + useEffect(() => { + const fetchOrders = async () => { + if (!keypair?.pub) return; + + setLoading(true); + + try { + const res = await client.query({ + query: ORDERS_QUERY, + variables: { partyId: keypair.pub }, + }); + + if (!res.data.party?.orders.length) return; + + mergeOrders(res.data.party.orders); + } catch (err) { + setError(err); + } finally { + setLoading(false); + } + }; + + fetchOrders(); + }, [mergeOrders, keypair, client]); + + // Start subscription + useEffect(() => { + if (!keypair?.pub) return; + + const sub = client + .subscribe({ + query: ORDERS_SUB, + variables: { partyId: keypair.pub }, + }) + .subscribe(({ data }) => { + mergeOrders(data.orders); + }); + + return () => { + if (sub) { + sub.unsubscribe(); + } + }; + }, [client, keypair, mergeOrders]); + + return { orders, error, loading }; +}; diff --git a/apps/trading/jest.config.js b/apps/trading/jest.config.js index 09d01143d..64e53be63 100644 --- a/apps/trading/jest.config.js +++ b/apps/trading/jest.config.js @@ -7,4 +7,5 @@ module.exports = { }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], coverageDirectory: '../../coverage/apps/trading', + setupFilesAfterEnv: ['./setup-tests.ts'], }; diff --git a/apps/trading/pages/_app.page.tsx b/apps/trading/pages/_app.page.tsx index b93d7c834..fe20d1396 100644 --- a/apps/trading/pages/_app.page.tsx +++ b/apps/trading/pages/_app.page.tsx @@ -5,7 +5,6 @@ import { ThemeContext } from '@vegaprotocol/react-helpers'; import { VegaConnectDialog, VegaWalletProvider } from '@vegaprotocol/wallet'; import { Connectors } from '../lib/connectors'; import { useCallback, useMemo, useState } from 'react'; -import { SingletonHooksContainer } from 'react-singleton-hook'; import { createClient } from '../lib/apollo-client'; import { ThemeSwitcher } from '@vegaprotocol/ui-toolkit'; import { ApolloProvider } from '@apollo/client'; @@ -35,7 +34,6 @@ function VegaTradingApp({ Component, pageProps }: AppProps) { - Welcome to trading! diff --git a/apps/trading/pages/markets/[marketId].page.tsx b/apps/trading/pages/markets/[marketId].page.tsx index aee507c3d..54033cf7c 100644 --- a/apps/trading/pages/markets/[marketId].page.tsx +++ b/apps/trading/pages/markets/[marketId].page.tsx @@ -3,7 +3,7 @@ import { Market, MarketVariables } from '@vegaprotocol/graphql'; import { Splash } from '@vegaprotocol/ui-toolkit'; import { useRouter } from 'next/router'; import React, { useEffect, useState } from 'react'; -import debounce from 'lodash.debounce'; +import debounce from 'lodash/debounce'; import { PageQueryContainer } from '../../components/page-query-container'; import { TradeGrid, TradePanels } from './trade-grid'; diff --git a/apps/trading/pages/markets/grid-tabs.tsx b/apps/trading/pages/markets/grid-tabs.tsx index 6b9ba88cf..dc9f3636f 100644 --- a/apps/trading/pages/markets/grid-tabs.tsx +++ b/apps/trading/pages/markets/grid-tabs.tsx @@ -67,7 +67,7 @@ export const GridTabs = ({ children, group }: GridTabsProps) => { {Children.map(children, (child) => { if (!isValidElement(child)) return null; return ( - + {child.props.children} ); diff --git a/apps/trading/pages/markets/index.page.tsx b/apps/trading/pages/markets/index.page.tsx index a930be52c..fd909eeca 100644 --- a/apps/trading/pages/markets/index.page.tsx +++ b/apps/trading/pages/markets/index.page.tsx @@ -2,27 +2,23 @@ import { Markets } from '@vegaprotocol/graphql'; import { useRouter } from 'next/router'; import { MarketListTable } from '@vegaprotocol/market-list'; import { useMarkets } from '../../hooks/use-markets'; -import { Splash } from '@vegaprotocol/ui-toolkit'; +import { AsyncRenderer } from '../../components/async-renderer'; const Markets = () => { const { pathname, push } = useRouter(); const { markets, error, loading } = useMarkets(); - if (error) { - return Something went wrong: {error.message}; - } - - if (loading) { - return Loading...; - } - return ( - - push(`${pathname}/${id}?portfolio=orders&trade=orderbook`) - } - /> + + {(data) => ( + + push(`${pathname}/${id}?portfolio=orders&trade=orderbook`) + } + /> + )} + ); }; diff --git a/apps/trading/pages/markets/trade-grid.tsx b/apps/trading/pages/markets/trade-grid.tsx index b3b9b4c42..dcb212621 100644 --- a/apps/trading/pages/markets/trade-grid.tsx +++ b/apps/trading/pages/markets/trade-grid.tsx @@ -4,10 +4,10 @@ import AutoSizer from 'react-virtualized-auto-sizer'; import { useState, ReactNode } from 'react'; import { GridTab, GridTabs } from './grid-tabs'; import { DealTicketContainer } from '../../components/deal-ticket-container'; +import { OrderListContainer } from '../..//components/order-list-container'; const Chart = () =>
TODO: Chart
; const Orderbook = () =>
TODO: Orderbook
; -const Orders = () =>
TODO: Orders
; const Positions = () =>
TODO: Positions
; const Collateral = () =>
TODO: Collateral
; @@ -17,7 +17,7 @@ const TradingViews = { chart: Chart, ticket: DealTicketContainer, orderbook: Orderbook, - orders: Orders, + orders: OrderListContainer, positions: Positions, collateral: Collateral, }; diff --git a/apps/trading/setup-tests.ts b/apps/trading/setup-tests.ts new file mode 100644 index 000000000..7b0828bfa --- /dev/null +++ b/apps/trading/setup-tests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/apps/trading/specs/index.spec.tsx b/apps/trading/specs/index.spec.tsx index 65b343075..1f61cff81 100644 --- a/apps/trading/specs/index.spec.tsx +++ b/apps/trading/specs/index.spec.tsx @@ -1,26 +1,17 @@ import React from 'react'; import { render } from '@testing-library/react'; - import Index from '../pages/index.page'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; + +jest.mock('@vegaprotocol/ui-toolkit', () => { + const original = jest.requireActual('@vegaprotocol/ui-toolkit'); + return { + ...original, + AgGridDynamic: () =>
AgGrid
, + }; +}); describe('Index', () => { it('should render successfully', () => { - const { baseElement } = render( - - - - ); - expect(baseElement).toBeTruthy(); + render(); }); }); diff --git a/libs/deal-ticket/src/deal-ticket.tsx b/libs/deal-ticket/src/deal-ticket.tsx index a49dbf958..a7235d957 100644 --- a/libs/deal-ticket/src/deal-ticket.tsx +++ b/libs/deal-ticket/src/deal-ticket.tsx @@ -12,9 +12,6 @@ const DEFAULT_ORDER: Order = { timeInForce: OrderTimeInForce.IOC, }; -// TODO: Consider using a generated type when we have a better solution for -// sharing the types from GQL - export type TransactionStatus = 'default' | 'pending'; export interface DealTicketProps { diff --git a/libs/graphql/src/__generated__/OrderFields.ts b/libs/graphql/src/__generated__/OrderFields.ts new file mode 100644 index 000000000..1fa9bff65 --- /dev/null +++ b/libs/graphql/src/__generated__/OrderFields.ts @@ -0,0 +1,115 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { OrderType, Side, OrderStatus, OrderRejectionReason, OrderTimeInForce } from "./globalTypes"; + +// ==================================================== +// 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; + /** + * 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/graphql/src/__generated__/OrderSub.ts b/libs/graphql/src/__generated__/OrderSub.ts new file mode 100644 index 000000000..8c8e05efc --- /dev/null +++ b/libs/graphql/src/__generated__/OrderSub.ts @@ -0,0 +1,126 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { OrderType, Side, OrderStatus, OrderRejectionReason, OrderTimeInForce } from "./globalTypes"; + +// ==================================================== +// 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; + /** + * 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/graphql/src/__generated__/Orders.ts b/libs/graphql/src/__generated__/Orders.ts new file mode 100644 index 000000000..37f376e44 --- /dev/null +++ b/libs/graphql/src/__generated__/Orders.ts @@ -0,0 +1,138 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { OrderType, Side, OrderStatus, OrderRejectionReason, OrderTimeInForce } from "./globalTypes"; + +// ==================================================== +// 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; + /** + * 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/graphql/src/__generated__/globalTypes.ts b/libs/graphql/src/__generated__/globalTypes.ts index e4c7a3e64..0e2c72879 100644 --- a/libs/graphql/src/__generated__/globalTypes.ts +++ b/libs/graphql/src/__generated__/globalTypes.ts @@ -162,6 +162,18 @@ export enum OrderStatus { Stopped = "Stopped", } +/** + * Valid order types, these determine what happens when an order is added to the book + */ +export enum OrderTimeInForce { + FOK = "FOK", + GFA = "GFA", + GFN = "GFN", + GTC = "GTC", + GTT = "GTT", + IOC = "IOC", +} + export enum OrderType { Limit = "Limit", Market = "Market", @@ -224,6 +236,14 @@ export enum ProposalState { WaitingForNodeVote = "WaitingForNodeVote", } +/** + * Whether the placer of an order is aiming to buy or sell on the market + */ +export enum Side { + Buy = "Buy", + Sell = "Sell", +} + export enum VoteValue { No = "No", Yes = "Yes", diff --git a/libs/graphql/src/index.ts b/libs/graphql/src/index.ts index 3e7077947..59c3a0077 100644 --- a/libs/graphql/src/index.ts +++ b/libs/graphql/src/index.ts @@ -9,5 +9,8 @@ export * from './__generated__/MarketDataFields'; export * from './__generated__/NetworkParametersQuery'; export * from './__generated__/NodesQuery'; export * from './__generated__/OrderEvent'; +export * from './__generated__/OrderFields'; +export * from './__generated__/Orders'; +export * from './__generated__/OrderSub'; export * from './__generated__/PartyAssetsQuery'; export * from './__generated__/ProposalsQuery'; diff --git a/libs/mainnet-stats-manager/src/config/types.ts b/libs/mainnet-stats-manager/src/config/types.ts index 9a81b709e..2cd040554 100644 --- a/libs/mainnet-stats-manager/src/config/types.ts +++ b/libs/mainnet-stats-manager/src/config/types.ts @@ -18,12 +18,14 @@ export interface Stats { chainId: string; } +// eslint-disable-next-line export type value = any; export type goodThreshold = (...args: value[]) => boolean; export interface StatFields { title: string; goodThreshold?: goodThreshold; + // eslint-disable-next-line formatter?: (arg0: value) => any; promoted?: boolean; value?: value; diff --git a/libs/market-list/src/lib/market-list-table.tsx b/libs/market-list/src/lib/market-list-table.tsx index 70afd331e..89295fe20 100644 --- a/libs/market-list/src/lib/market-list-table.tsx +++ b/libs/market-list/src/lib/market-list-table.tsx @@ -20,7 +20,7 @@ export const MarketListTable = ({ }: MarketListTableProps) => { const [initialMarkets] = useState(markets); const gridApi = useRef(null); - useApplyGridTransaction(markets, gridApi.current); + useApplyGridTransaction(markets, gridApi.current); return ( data.id} + suppressCellFocus={true} defaultColDef={{ flex: 1, resizable: true, diff --git a/libs/order-list/.babelrc b/libs/order-list/.babelrc new file mode 100644 index 000000000..ccae900be --- /dev/null +++ b/libs/order-list/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nrwl/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/order-list/.eslintrc.json b/libs/order-list/.eslintrc.json new file mode 100644 index 000000000..734ddacee --- /dev/null +++ b/libs/order-list/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/order-list/README.md b/libs/order-list/README.md new file mode 100644 index 000000000..0cf83d41e --- /dev/null +++ b/libs/order-list/README.md @@ -0,0 +1,7 @@ +# order-list + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test order-list` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/order-list/jest.config.js b/libs/order-list/jest.config.js new file mode 100644 index 000000000..a3d891f40 --- /dev/null +++ b/libs/order-list/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + displayName: 'order-list', + preset: '../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': 'babel-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/order-list', + setupFilesAfterEnv: ['./src/setup-tests.ts'], +}; diff --git a/libs/order-list/project.json b/libs/order-list/project.json new file mode 100644 index 000000000..a85b132a6 --- /dev/null +++ b/libs/order-list/project.json @@ -0,0 +1,23 @@ +{ + "root": "libs/order-list", + "sourceRoot": "libs/order-list/src", + "projectType": "library", + "tags": [], + "targets": { + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/order-list/**/*.{ts,tsx,js,jsx}"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/order-list"], + "options": { + "jestConfig": "libs/order-list/jest.config.js", + "passWithNoTests": true + } + } + } +} diff --git a/libs/order-list/src/index.ts b/libs/order-list/src/index.ts new file mode 100644 index 000000000..d432bcbe8 --- /dev/null +++ b/libs/order-list/src/index.ts @@ -0,0 +1 @@ +export * from './order-list'; diff --git a/libs/order-list/src/order-list.spec.tsx b/libs/order-list/src/order-list.spec.tsx new file mode 100644 index 000000000..cec0664de --- /dev/null +++ b/libs/order-list/src/order-list.spec.tsx @@ -0,0 +1,153 @@ +import { act, render, screen } from '@testing-library/react'; +import { formatNumber, getDateTimeFormat } from '@vegaprotocol/react-helpers'; +import { Orders_party_orders } from '@vegaprotocol/graphql'; +import { + OrderStatus, + OrderTimeInForce, + OrderType, + Side, + OrderRejectionReason, +} from '@vegaprotocol/graphql'; +import { OrderList } from './order-list'; + +test('No orders message shown', async () => { + await act(async () => { + render(); + }); + expect(screen.getByText('No orders')).toBeInTheDocument(); +}); + +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', + }, + }, + }, + size: '10', + type: OrderType.Market, + status: OrderStatus.Active, + side: Side.Buy, + remaining: '5', + price: '', + timeInForce: OrderTimeInForce.IOC, + createdAt: new Date().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', + }, + }, + }, + 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, +}; + +test('Correct columns are rendered', async () => { + await act(async () => { + render(); + }); + + const headers = screen.getAllByRole('columnheader'); + expect(headers).toHaveLength(8); + expect(headers.map((h) => h.textContent?.trim())).toEqual([ + 'Market', + 'Amount', + 'Type', + 'Status', + 'Filled', + 'Price', + 'Time In Force', + 'Created At', + ]); +}); + +test('Correct formatting applied for market order', async () => { + await act(async () => { + render(); + }); + + const cells = screen.getAllByRole('gridcell'); + const expectedValues = [ + marketOrder.market?.tradableInstrument.instrument.code, + '+10', + marketOrder.type, + marketOrder.status, + '5', + '-', + marketOrder.timeInForce, + getDateTimeFormat().format(new Date(marketOrder.createdAt)), + ]; + cells.forEach((cell, i) => { + expect(cell).toHaveTextContent(expectedValues[i]); + }); +}); + +test('Correct formatting applied for GTT limit order', async () => { + await act(async () => { + render(); + }); + const cells = screen.getAllByRole('gridcell'); + const expectedValues = [ + limitOrder.market?.tradableInstrument.instrument.code, + '-10', + limitOrder.type, + limitOrder.status, + '5', + formatNumber(limitOrder.price, limitOrder.market?.decimalPlaces ?? 0), + `${limitOrder.timeInForce}: ${getDateTimeFormat().format( + new Date(limitOrder.expiresAt ?? '') + )}`, + getDateTimeFormat().format(new Date(limitOrder.createdAt)), + ]; + cells.forEach((cell, i) => { + expect(cell).toHaveTextContent(expectedValues[i]); + }); +}); + +test('Correct formatting applied for a rejected order', async () => { + const rejectedOrder = { + ...marketOrder, + status: OrderStatus.Rejected, + rejectionReason: OrderRejectionReason.InsufficientAssetBalance, + }; + await act(async () => { + render(); + }); + const cells = screen.getAllByRole('gridcell'); + expect(cells[3]).toHaveTextContent( + `${rejectedOrder.status}: ${rejectedOrder.rejectionReason}` + ); +}); diff --git a/libs/order-list/src/order-list.tsx b/libs/order-list/src/order-list.tsx new file mode 100644 index 000000000..faf20b296 --- /dev/null +++ b/libs/order-list/src/order-list.tsx @@ -0,0 +1,97 @@ +import { + Orders_party_orders, + OrderTimeInForce, + OrderStatus, + Side, +} from '@vegaprotocol/graphql'; +import { + formatNumber, + getDateTimeFormat, + useApplyGridTransaction, +} from '@vegaprotocol/react-helpers'; +import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; +import { GridApi, ValueFormatterParams } from 'ag-grid-community'; +import { AgGridColumn } from 'ag-grid-react'; +import { useRef, useState } from 'react'; + +interface OrderListProps { + orders: Orders_party_orders[]; +} + +export const OrderList = ({ orders }: OrderListProps) => { + // Store initial orders for initial table data set, further updates + // are handled by the effect below + const [initialOrders] = useState(orders); + const gridApi = useRef(null); + useApplyGridTransaction(orders, gridApi.current); + + return ( + { + gridApi.current = params.api; + }} + getRowNodeId={(data) => data.id} + > + + { + const prefix = data.side === Side.Buy ? '+' : '-'; + return prefix + value; + }} + /> + + { + if (value === OrderStatus.Rejected) { + return `${value}: ${data.rejectionReason}`; + } + + return value; + }} + /> + { + return `${Number(data.size) - Number(data.remaining)}/${data.size}`; + }} + /> + { + if (data.type === 'Market') { + return '-'; + } + return formatNumber(value, data.market.decimalPlaces); + }} + /> + { + if (value === OrderTimeInForce.GTT && data.expiresAt) { + const expiry = getDateTimeFormat().format(new Date(data.expiresAt)); + return `${value}: ${expiry}`; + } + + return value; + }} + /> + { + return getDateTimeFormat().format(new Date(value)); + }} + /> + + ); +}; diff --git a/libs/order-list/src/setup-tests.ts b/libs/order-list/src/setup-tests.ts new file mode 100644 index 000000000..7b0828bfa --- /dev/null +++ b/libs/order-list/src/setup-tests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/libs/order-list/tsconfig.json b/libs/order-list/tsconfig.json new file mode 100644 index 000000000..4c089585e --- /dev/null +++ b/libs/order-list/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/order-list/tsconfig.lib.json b/libs/order-list/tsconfig.lib.json new file mode 100644 index 000000000..252904bb7 --- /dev/null +++ b/libs/order-list/tsconfig.lib.json @@ -0,0 +1,22 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/libs/order-list/tsconfig.spec.json b/libs/order-list/tsconfig.spec.json new file mode 100644 index 000000000..67f149c4c --- /dev/null +++ b/libs/order-list/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/libs/react-helpers/src/hooks/use-apply-grid-transaction.spec.ts b/libs/react-helpers/src/hooks/use-apply-grid-transaction.spec.ts new file mode 100644 index 000000000..5fdd048a6 --- /dev/null +++ b/libs/react-helpers/src/hooks/use-apply-grid-transaction.spec.ts @@ -0,0 +1,74 @@ +import { useApplyGridTransaction } from './use-apply-grid-transaction'; +import { renderHook } from '@testing-library/react-hooks'; +import { GridApi } from 'ag-grid-community'; + +type Items = Array<{ id: string; value: number }>; + +const item = { + id: '1', + value: 1, +}; +const item2 = { + id: '2', + value: 2, +}; +const items = [item, item2]; + +function setup(items: Items, rowNodes: Items) { + const gridApiMock = { + applyTransaction: jest.fn(), + getRowNode: (id: string) => { + const node = rowNodes.find((i) => i.id === id); + if (node) { + return { data: node }; + } + return undefined; + }, + }; + renderHook(() => useApplyGridTransaction(items, gridApiMock as any)); + return gridApiMock; +} + +test('Adds items', () => { + const gridApiMock = setup(items, []); + expect(gridApiMock.applyTransaction).toHaveBeenCalledWith({ + update: [], + add: items, + addIndex: 0, + }); +}); + +test('Doesnt update rows without changes', () => { + const rowNodes: Array<{ id: string; value: number }> = [...items]; + const gridApiMock = setup(items, rowNodes); + expect(gridApiMock.applyTransaction).toHaveBeenCalledWith({ + update: [], + add: [], + addIndex: 0, + }); +}); + +test('Update rows with changes', () => { + const rowNodes = [...items]; + const updatedItems = [ + { id: '1', value: 10 }, + { id: '2', value: 20 }, + ]; + const gridApiMock = setup(updatedItems, rowNodes); + expect(gridApiMock.applyTransaction).toHaveBeenCalledWith({ + update: updatedItems, + add: [], + addIndex: 0, + }); +}); + +test('Updates and adds at the same time', () => { + const newItem = { id: '3', value: 3 }; + const updatedItem = { id: '2', value: 20 }; + const gridApiMock = setup([newItem, updatedItem], [...items]); + expect(gridApiMock.applyTransaction).toHaveBeenCalledWith({ + update: [updatedItem], + add: [newItem], + addIndex: 0, + }); +}); diff --git a/libs/react-helpers/src/hooks/use-apply-grid-transaction.ts b/libs/react-helpers/src/hooks/use-apply-grid-transaction.ts index 4eda7be25..007f891a7 100644 --- a/libs/react-helpers/src/hooks/use-apply-grid-transaction.ts +++ b/libs/react-helpers/src/hooks/use-apply-grid-transaction.ts @@ -1,5 +1,6 @@ import { GridApi } from 'ag-grid-community'; import { useEffect } from 'react'; +import isEqual from 'lodash/isEqual'; export const useApplyGridTransaction = ( data: T[], @@ -18,7 +19,9 @@ export const useApplyGridTransaction = ( const rowNode = gridApi.getRowNode(d.id); if (rowNode) { - update.push(d); + if (!isEqual(rowNode.data, d)) { + update.push(d); + } } else { add.push(d); } diff --git a/libs/react-helpers/src/lib/format/format.ts b/libs/react-helpers/src/lib/format/format.ts index cd0478c81..10c9716f2 100644 --- a/libs/react-helpers/src/lib/format/format.ts +++ b/libs/react-helpers/src/lib/format/format.ts @@ -1,5 +1,5 @@ -import once from 'lodash.once'; -import memoize from 'lodash.memoize'; +import once from 'lodash/once'; +import memoize from 'lodash/memoize'; import { addDecimal } from '../decimals'; const getUserLocale = () => 'default'; diff --git a/package.json b/package.json index a93420ffd..8f0744025 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@radix-ui/react-tooltip": "^0.1.7", "@sentry/react": "^6.18.1", "@sentry/tracing": "^6.18.1", + "@types/lodash": "^4.14.180", "@types/uuid": "^8.3.4", "@vegaprotocol/vegawallet-service-api-client": "^0.4.6", "ag-grid-community": "^27.0.1", @@ -31,20 +32,18 @@ "bignumber.js": "^9.0.2", "classnames": "^2.3.1", "core-js": "^3.6.5", + "date-fns": "^2.28.0", "env-cmd": "^10.1.0", "ethers": "^5.6.0", "graphql": "^15.7.2", "graphql-ws": "^5.6.3", - "lodash.debounce": "^4.0.8", - "lodash.memoize": "^4.1.2", - "lodash.once": "^4.1.1", + "lodash": "^4.17.21", "next": "12.0.7", "nx": "^13.8.3", "postcss": "^8.4.6", "react": "17.0.2", "react-dom": "17.0.2", "react-hook-form": "^7.27.0", - "react-singleton-hook": "^3.2.3", "react-syntax-highlighter": "^15.4.5", "react-use-websocket": "^3.0.0", "react-virtualized-auto-sizer": "^1.0.6", @@ -80,8 +79,6 @@ "@testing-library/react-hooks": "7.0.2", "@types/classnames": "^2.3.1", "@types/jest": "27.0.2", - "@types/lodash.memoize": "^4.1.6", - "@types/lodash.once": "^4.1.6", "@types/node": "16.11.7", "@types/prismjs": "^1.26.0", "@types/react": "17.0.30", diff --git a/tsconfig.base.json b/tsconfig.base.json index 3315e4bb9..f7792cf38 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -21,6 +21,7 @@ "libs/mainnet-stats-manager/src/index.ts" ], "@vegaprotocol/market-list": ["libs/market-list/src/index.ts"], + "@vegaprotocol/order-list": ["libs/order-list/src/index.ts"], "@vegaprotocol/react-helpers": ["libs/react-helpers/src/index.ts"], "@vegaprotocol/tailwindcss-config": [ "libs/tailwindcss-config/src/index.js" diff --git a/workspace.json b/workspace.json index 4d72aaf7a..880fcc030 100644 --- a/workspace.json +++ b/workspace.json @@ -7,6 +7,7 @@ "graphql": "libs/graphql", "mainnet-stats-manager": "libs/mainnet-stats-manager", "market-list": "libs/market-list", + "order-list": "libs/order-list", "react-helpers": "libs/react-helpers", "stats-mainnet": "apps/stats-mainnet", "stats-mainnet-e2e": "apps/stats-mainnet-e2e", diff --git a/yarn.lock b/yarn.lock index 9ed729bc2..7d5c3fae4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5201,21 +5201,7 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/lodash.memoize@^4.1.6": - version "4.1.6" - resolved "https://registry.yarnpkg.com/@types/lodash.memoize/-/lodash.memoize-4.1.6.tgz#3221f981790a415cab1a239f25c17efd8b604c23" - integrity sha512-mYxjKiKzRadRJVClLKxS4wb3Iy9kzwJ1CkbyKiadVxejnswnRByyofmPMscFKscmYpl36BEEhCMPuWhA1R/1ZQ== - dependencies: - "@types/lodash" "*" - -"@types/lodash.once@^4.1.6": - version "4.1.6" - resolved "https://registry.yarnpkg.com/@types/lodash.once/-/lodash.once-4.1.6.tgz#f6ea46e3426bc3494b72a45e65efed440103b967" - integrity sha512-kawTe2cBNZ5OI4CvTJT9cs8wacGZK4BoakKAGASl/jH3LxflMTuy82wN2U5klYsxYjrABkNWmzgO33volt7urQ== - dependencies: - "@types/lodash" "*" - -"@types/lodash@*": +"@types/lodash@^4.14.180": version "4.14.180" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670" integrity sha512-XOKXa1KIxtNXgASAnwj7cnttJxS4fksBRywK/9LzRV5YxrF80BXZIGeQSuoESQ/VkUj30Ae0+YcuHc15wJCB2g== @@ -9230,6 +9216,11 @@ date-fns@^1.27.2: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== +date-fns@^2.28.0: + version "2.28.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2" + integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw== + dayjs@^1.10.4: version "1.10.8" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.8.tgz#267df4bc6276fcb33c04a6735287e3f429abec41" @@ -16577,11 +16568,6 @@ react-shallow-renderer@^16.13.1: object-assign "^4.1.1" react-is "^16.12.0 || ^17.0.0" -react-singleton-hook@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/react-singleton-hook/-/react-singleton-hook-3.2.3.tgz#1765e67b1910fd163cdcd186a840af82b60ca17e" - integrity sha512-DYJ70V8IVUZznygmRYYF+hxwLkGPLrCCTAD1Je7sZYXquE9HpAJqBVeEgb2XIZwxWUw97PN2ho5J/+63YwdKaA== - react-sizeme@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-3.0.2.tgz#4a2f167905ba8f8b8d932a9e35164e459f9020e4"