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
This commit is contained in:
parent
05d53bbf4c
commit
6ad2a7676e
@ -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) => {
|
||||
|
@ -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();
|
||||
|
||||
|
@ -7,4 +7,5 @@ module.exports = {
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/apps/stats-mainnet',
|
||||
setupFilesAfterEnv: ['./src/setup-tests.ts'],
|
||||
};
|
||||
|
@ -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(
|
||||
<React.StrictMode>
|
||||
|
27
apps/trading/components/async-renderer/async-renderer.tsx
Normal file
27
apps/trading/components/async-renderer/async-renderer.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface AsyncRendererProps<T> {
|
||||
loading: boolean;
|
||||
error: Error | undefined;
|
||||
data: T;
|
||||
children: (data: T) => ReactNode;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
export function AsyncRenderer<T = any>({
|
||||
loading,
|
||||
error,
|
||||
data,
|
||||
children,
|
||||
}: AsyncRendererProps<T>) {
|
||||
if (error) {
|
||||
return <Splash>Something went wrong: {error.message}</Splash>;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Splash>Loading...</Splash>;
|
||||
}
|
||||
|
||||
return <>{children(data)}</>;
|
||||
}
|
1
apps/trading/components/async-renderer/index.ts
Normal file
1
apps/trading/components/async-renderer/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './async-renderer';
|
@ -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 {
|
||||
|
1
apps/trading/components/order-list-container/index.ts
Normal file
1
apps/trading/components/order-list-container/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './order-list-container';
|
@ -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: () => <div>OrderList</div>,
|
||||
}));
|
||||
|
||||
test('Renders a loading state while awaiting orders', () => {
|
||||
jest.spyOn(useOrdersHook, 'useOrders').mockReturnValue({
|
||||
orders: [],
|
||||
loading: true,
|
||||
error: null,
|
||||
});
|
||||
render(<OrderListContainer />);
|
||||
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(<OrderListContainer />);
|
||||
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(<OrderListContainer />);
|
||||
expect(screen.getByText('OrderList')).toBeInTheDocument();
|
||||
});
|
@ -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 (
|
||||
<AsyncRenderer<OrderFields[]> loading={loading} error={error} data={orders}>
|
||||
{(data) => <OrderList orders={data} />}
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
@ -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<TData, TVariables> {
|
||||
query: DocumentNode;
|
||||
@ -16,13 +16,13 @@ export const PageQueryContainer = <TData, TVariables = OperationVariables>({
|
||||
}: PageQueryContainerProps<TData, TVariables>) => {
|
||||
const { data, loading, error } = useQuery<TData, TVariables>(query, options);
|
||||
|
||||
if (loading || !data) {
|
||||
return <Splash>Loading...</Splash>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <Splash>Something went wrong: {error.message}</Splash>;
|
||||
}
|
||||
|
||||
return <>{children(data)}</>;
|
||||
return (
|
||||
<AsyncRenderer<TData>
|
||||
loading={loading || Boolean(!data)}
|
||||
error={error}
|
||||
data={data}
|
||||
>
|
||||
{(data) => children(data)}
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
@ -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<Markets_markets[]>([]);
|
||||
const [error, setError] = useState<Error | null>(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);
|
||||
|
165
apps/trading/hooks/use-orders.spec.tsx
Normal file
165
apps/trading/hooks/use-orders.spec.tsx
Normal file
@ -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>): 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<VegaWalletContextShape>,
|
||||
mocks: MockedResponse[] = []
|
||||
) {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={mocks}>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ ...defaultWalletContext, ...context }}
|
||||
>
|
||||
{children}
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
return renderHook(() => useOrders(), { wrapper });
|
||||
}
|
||||
|
||||
test('Fetches and subscribes to orders and merges appropriately', async () => {
|
||||
const order = generateOrder();
|
||||
const mockOrderQuery: MockedResponse<Orders> = {
|
||||
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<OrderSub> = {
|
||||
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<Orders> = {
|
||||
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<Orders> = {
|
||||
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);
|
||||
});
|
140
apps/trading/hooks/use-orders.ts
Normal file
140
apps/trading/hooks/use-orders.ts
Normal file
@ -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<OrderFields[]>([]);
|
||||
const [error, setError] = useState<Error | null>(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<Orders, OrdersVariables>({
|
||||
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<OrderSub, OrderSubVariables>({
|
||||
query: ORDERS_SUB,
|
||||
variables: { partyId: keypair.pub },
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
mergeOrders(data.orders);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (sub) {
|
||||
sub.unsubscribe();
|
||||
}
|
||||
};
|
||||
}, [client, keypair, mergeOrders]);
|
||||
|
||||
return { orders, error, loading };
|
||||
};
|
@ -7,4 +7,5 @@ module.exports = {
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/apps/trading',
|
||||
setupFilesAfterEnv: ['./setup-tests.ts'],
|
||||
};
|
||||
|
@ -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) {
|
||||
<ThemeContext.Provider value={theme}>
|
||||
<ApolloProvider client={client}>
|
||||
<VegaWalletProvider>
|
||||
<SingletonHooksContainer />
|
||||
<AppLoader>
|
||||
<Head>
|
||||
<title>Welcome to trading!</title>
|
||||
|
@ -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';
|
||||
|
||||
|
@ -67,7 +67,7 @@ export const GridTabs = ({ children, group }: GridTabsProps) => {
|
||||
{Children.map(children, (child) => {
|
||||
if (!isValidElement(child)) return null;
|
||||
return (
|
||||
<Tabs.Content value={child.props.name}>
|
||||
<Tabs.Content value={child.props.name} className="h-full">
|
||||
{child.props.children}
|
||||
</Tabs.Content>
|
||||
);
|
||||
|
@ -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 <Splash>Something went wrong: {error.message}</Splash>;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Splash>Loading...</Splash>;
|
||||
}
|
||||
|
||||
return (
|
||||
<MarketListTable
|
||||
markets={markets}
|
||||
onRowClicked={(id) =>
|
||||
push(`${pathname}/${id}?portfolio=orders&trade=orderbook`)
|
||||
}
|
||||
/>
|
||||
<AsyncRenderer loading={loading} error={error} data={markets}>
|
||||
{(data) => (
|
||||
<MarketListTable
|
||||
markets={data}
|
||||
onRowClicked={(id) =>
|
||||
push(`${pathname}/${id}?portfolio=orders&trade=orderbook`)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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 = () => <div>TODO: Chart</div>;
|
||||
const Orderbook = () => <div>TODO: Orderbook</div>;
|
||||
const Orders = () => <div>TODO: Orders</div>;
|
||||
const Positions = () => <div>TODO: Positions</div>;
|
||||
const Collateral = () => <div>TODO: Collateral</div>;
|
||||
|
||||
@ -17,7 +17,7 @@ const TradingViews = {
|
||||
chart: Chart,
|
||||
ticket: DealTicketContainer,
|
||||
orderbook: Orderbook,
|
||||
orders: Orders,
|
||||
orders: OrderListContainer,
|
||||
positions: Positions,
|
||||
collateral: Collateral,
|
||||
};
|
||||
|
1
apps/trading/setup-tests.ts
Normal file
1
apps/trading/setup-tests.ts
Normal file
@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
@ -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: () => <div>AgGrid</div>,
|
||||
};
|
||||
});
|
||||
|
||||
describe('Index', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(
|
||||
<VegaWalletContext.Provider
|
||||
value={{
|
||||
keypair: null,
|
||||
keypairs: null,
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
selectPublicKey: jest.fn(),
|
||||
connector: null,
|
||||
sendTx: jest.fn(),
|
||||
}}
|
||||
>
|
||||
<Index />
|
||||
</VegaWalletContext.Provider>
|
||||
);
|
||||
expect(baseElement).toBeTruthy();
|
||||
render(<Index />);
|
||||
});
|
||||
});
|
||||
|
@ -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 {
|
||||
|
115
libs/graphql/src/__generated__/OrderFields.ts
generated
Normal file
115
libs/graphql/src/__generated__/OrderFields.ts
generated
Normal file
@ -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;
|
||||
}
|
126
libs/graphql/src/__generated__/OrderSub.ts
generated
Normal file
126
libs/graphql/src/__generated__/OrderSub.ts
generated
Normal file
@ -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;
|
||||
}
|
138
libs/graphql/src/__generated__/Orders.ts
generated
Normal file
138
libs/graphql/src/__generated__/Orders.ts
generated
Normal file
@ -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;
|
||||
}
|
20
libs/graphql/src/__generated__/globalTypes.ts
generated
20
libs/graphql/src/__generated__/globalTypes.ts
generated
@ -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",
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -20,7 +20,7 @@ export const MarketListTable = ({
|
||||
}: MarketListTableProps) => {
|
||||
const [initialMarkets] = useState(markets);
|
||||
const gridApi = useRef<GridApi | null>(null);
|
||||
useApplyGridTransaction(markets, gridApi.current);
|
||||
useApplyGridTransaction<Markets_markets>(markets, gridApi.current);
|
||||
|
||||
return (
|
||||
<AgGrid
|
||||
@ -28,6 +28,7 @@ export const MarketListTable = ({
|
||||
overlayNoRowsTemplate="No markets"
|
||||
rowData={initialMarkets}
|
||||
getRowNodeId={(data) => data.id}
|
||||
suppressCellFocus={true}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
|
12
libs/order-list/.babelrc
Normal file
12
libs/order-list/.babelrc
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nrwl/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
18
libs/order-list/.eslintrc.json
Normal file
18
libs/order-list/.eslintrc.json
Normal file
@ -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": {}
|
||||
}
|
||||
]
|
||||
}
|
7
libs/order-list/README.md
Normal file
7
libs/order-list/README.md
Normal file
@ -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).
|
10
libs/order-list/jest.config.js
Normal file
10
libs/order-list/jest.config.js
Normal file
@ -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'],
|
||||
};
|
23
libs/order-list/project.json
Normal file
23
libs/order-list/project.json
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
libs/order-list/src/index.ts
Normal file
1
libs/order-list/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './order-list';
|
153
libs/order-list/src/order-list.spec.tsx
Normal file
153
libs/order-list/src/order-list.spec.tsx
Normal file
@ -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(<OrderList orders={[]} />);
|
||||
});
|
||||
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(<OrderList orders={[marketOrder]} />);
|
||||
});
|
||||
|
||||
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(<OrderList orders={[marketOrder]} />);
|
||||
});
|
||||
|
||||
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(<OrderList orders={[limitOrder]} />);
|
||||
});
|
||||
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(<OrderList orders={[rejectedOrder]} />);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[3]).toHaveTextContent(
|
||||
`${rejectedOrder.status}: ${rejectedOrder.rejectionReason}`
|
||||
);
|
||||
});
|
97
libs/order-list/src/order-list.tsx
Normal file
97
libs/order-list/src/order-list.tsx
Normal file
@ -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<GridApi | null>(null);
|
||||
useApplyGridTransaction<Orders_party_orders>(orders, gridApi.current);
|
||||
|
||||
return (
|
||||
<AgGrid
|
||||
rowData={initialOrders}
|
||||
overlayNoRowsTemplate="No orders"
|
||||
defaultColDef={{ flex: 1, resizable: true }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
onGridReady={(params) => {
|
||||
gridApi.current = params.api;
|
||||
}}
|
||||
getRowNodeId={(data) => data.id}
|
||||
>
|
||||
<AgGridColumn
|
||||
headerName="Market"
|
||||
field="market.tradableInstrument.instrument.code"
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName="Amount"
|
||||
field="size"
|
||||
valueFormatter={({ value, data }: ValueFormatterParams) => {
|
||||
const prefix = data.side === Side.Buy ? '+' : '-';
|
||||
return prefix + value;
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn field="type" />
|
||||
<AgGridColumn
|
||||
field="status"
|
||||
valueFormatter={({ value, data }: ValueFormatterParams) => {
|
||||
if (value === OrderStatus.Rejected) {
|
||||
return `${value}: ${data.rejectionReason}`;
|
||||
}
|
||||
|
||||
return value;
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName="Filled"
|
||||
field="remaining"
|
||||
valueFormatter={({ data }: ValueFormatterParams) => {
|
||||
return `${Number(data.size) - Number(data.remaining)}/${data.size}`;
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
field="price"
|
||||
valueFormatter={({ value, data }: ValueFormatterParams) => {
|
||||
if (data.type === 'Market') {
|
||||
return '-';
|
||||
}
|
||||
return formatNumber(value, data.market.decimalPlaces);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
field="timeInForce"
|
||||
valueFormatter={({ value, data }: ValueFormatterParams) => {
|
||||
if (value === OrderTimeInForce.GTT && data.expiresAt) {
|
||||
const expiry = getDateTimeFormat().format(new Date(data.expiresAt));
|
||||
return `${value}: ${expiry}`;
|
||||
}
|
||||
|
||||
return value;
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
field="createdAt"
|
||||
valueFormatter={({ value }: ValueFormatterParams) => {
|
||||
return getDateTimeFormat().format(new Date(value));
|
||||
}}
|
||||
/>
|
||||
</AgGrid>
|
||||
);
|
||||
};
|
1
libs/order-list/src/setup-tests.ts
Normal file
1
libs/order-list/src/setup-tests.ts
Normal file
@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
25
libs/order-list/tsconfig.json
Normal file
25
libs/order-list/tsconfig.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
22
libs/order-list/tsconfig.lib.json
Normal file
22
libs/order-list/tsconfig.lib.json
Normal file
@ -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"]
|
||||
}
|
19
libs/order-list/tsconfig.spec.json
Normal file
19
libs/order-list/tsconfig.spec.json
Normal file
@ -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"
|
||||
]
|
||||
}
|
@ -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,
|
||||
});
|
||||
});
|
@ -1,5 +1,6 @@
|
||||
import { GridApi } from 'ag-grid-community';
|
||||
import { useEffect } from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
export const useApplyGridTransaction = <T extends { id: string }>(
|
||||
data: T[],
|
||||
@ -18,7 +19,9 @@ export const useApplyGridTransaction = <T extends { id: string }>(
|
||||
const rowNode = gridApi.getRowNode(d.id);
|
||||
|
||||
if (rowNode) {
|
||||
update.push(d);
|
||||
if (!isEqual(rowNode.data, d)) {
|
||||
update.push(d);
|
||||
}
|
||||
} else {
|
||||
add.push(d);
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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",
|
||||
|
26
yarn.lock
26
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"
|
||||
|
Loading…
Reference in New Issue
Block a user