feat(trading): order details dialog (#4281)

This commit is contained in:
m.ray 2023-08-03 13:05:12 +03:00 committed by GitHub
parent 6686755763
commit a74cf02030
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 508 additions and 18 deletions

View File

@ -1 +0,0 @@
export * from './order-actions-dropdown';

View File

@ -1,13 +0,0 @@
import {
ActionsDropdown,
TradingDropdownCopyItem,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
export const OrderActionsDropdown = ({ id }: { id: string }) => {
return (
<ActionsDropdown data-testid="order-actions-content">
<TradingDropdownCopyItem value={id} text={t('Copy order ID')} />
</ActionsDropdown>
);
};

View File

@ -14,6 +14,7 @@ import {
import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/wallet';
import { OrderEditDialog } from '../order-list/order-edit-dialog';
import type { Order } from '../order-data-provider';
import { OrderViewDialog } from '../order-list/order-view-dialog';
export enum Filter {
'Open' = 'Open',
@ -42,6 +43,7 @@ export const OrderListManager = ({
}: OrderListManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [editOrder, setEditOrder] = useState<Order | null>(null);
const [viewOrder, setViewOrder] = useState<Order | null>(null);
const create = useVegaTransactionStore((state) => state.create);
const hasAmendableOrder = useHasAmendableOrder(marketId);
const variables =
@ -89,6 +91,7 @@ export const OrderListManager = ({
filter={filter}
onCancel={cancel}
onEdit={setEditOrder}
onView={setViewOrder}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
isReadOnly={isReadOnly}
@ -134,6 +137,14 @@ export const OrderListManager = ({
}}
/>
)}
{viewOrder && (
<OrderViewDialog
isOpen={Boolean(viewOrder)}
order={viewOrder}
onChange={() => setViewOrder(null)}
onMarketClick={onMarketClick}
/>
)}
</>
);
};

View File

@ -27,6 +27,7 @@ const defaultProps: OrderListTableProps = {
rowData: [],
onEdit: jest.fn(),
onCancel: jest.fn(),
onView: jest.fn(),
isReadOnly: false,
};

View File

@ -22,6 +22,9 @@ const Template: Story = (args) => {
onEdit={() => {
return;
}}
onView={() => {
return;
}}
isReadOnly={false}
/>
</div>
@ -49,6 +52,9 @@ const Template2: Story = (args) => {
rowData={args.data}
onCancel={cancel}
onEdit={setEditOrder}
onView={() => {
return;
}}
isReadOnly={false}
/>
</div>

View File

@ -6,7 +6,14 @@ import {
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import {
ActionsDropdown,
ButtonLink,
TradingDropdownCopyItem,
DropdownMenuItem,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { ForwardedRef } from 'react';
import { memo, forwardRef, useMemo } from 'react';
import {
@ -27,7 +34,6 @@ import type {
} from '@vegaprotocol/datagrid';
import type { AgGridReact } from 'ag-grid-react';
import type { Order } from '../order-data-provider';
import { OrderActionsDropdown } from '../order-actions-dropdown';
import { Filter } from '../order-list-manager';
import type { ColDef } from 'ag-grid-community';
@ -35,6 +41,7 @@ export type OrderListTableProps = TypedDataAgGrid<Order> & {
marketId?: string;
onCancel: (order: Order) => void;
onEdit: (order: Order) => void;
onView: (order: Order) => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void;
filter?: Filter;
@ -46,7 +53,15 @@ export const OrderListTable = memo<
>(
forwardRef<AgGridReact, OrderListTableProps>(
(
{ onCancel, onEdit, onMarketClick, onOrderTypeClick, filter, ...props },
{
onCancel,
onEdit,
onView,
onMarketClick,
onOrderTypeClick,
filter,
...props
},
ref
) => {
const showAllActions = props.isReadOnly
@ -295,7 +310,20 @@ export const OrderListTable = memo<
</ButtonLink>
</>
)}
<OrderActionsDropdown id={data?.id} />
<ActionsDropdown data-testid="market-actions-content">
<TradingDropdownCopyItem
value={data.id}
text={t('Copy order ID')}
/>
<DropdownMenuItem
key={'view-order'}
data-testid="view-order"
onClick={() => onView(data)}
>
<VegaIcon name={VegaIconNames.INFO} size={16} />
{t('View order details')}
</DropdownMenuItem>
</ActionsDropdown>
</div>
);
},
@ -305,6 +333,7 @@ export const OrderListTable = memo<
filter,
onCancel,
onEdit,
onView,
onMarketClick,
onOrderTypeClick,
props.isReadOnly,

View File

@ -0,0 +1,177 @@
import { render, screen } from '@testing-library/react';
import { OrderViewDialog } from './order-view-dialog';
import type { Order } from '../order-data-provider';
import { BrowserRouter } from 'react-router-dom';
import {
MarketState,
MarketTradingMode,
OrderStatus,
OrderTimeInForce,
OrderType,
Side,
} from '@vegaprotocol/types';
describe('OrderViewDialog', () => {
it('should render the order view dialog if the order is provided', async () => {
const order: Order = {
id: '7f2a78f370062e41683d26a0fcc4521b2bd4b747530b78bc6bc86195db0e5fb3',
market: {
__typename: 'Market',
id: 'b66cd4be223dfd900a4750bb5175e17d8f678996877d262be4c749a99e22a970',
decimalPlaces: 5,
positionDecimalPlaces: 3,
state: MarketState.STATE_ACTIVE,
tradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
fees: {
__typename: 'Fees',
factors: {
__typename: 'FeeFactors',
makerFee: '0.0002',
infrastructureFee: '0.0005',
liquidityFee: '0.001',
},
},
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '',
name: 'Tesla Quarterly (Sep 2023)',
code: 'TSLA.QM21',
metadata: {
__typename: 'InstrumentMetadata',
tags: [
'formerly:5A86B190C384997F',
'quote:EURO',
'ticker:TSLA',
'class:equities/single-stock-futures',
'sector:tech',
'listing_venue:NASDAQ',
'country:US',
'auto:tsla',
],
},
product: {
__typename: 'Future',
settlementAsset: {
__typename: 'Asset',
id: '177e8f6c25a955bd18475084b99b2b1d37f28f3dec393fab7755a7e69c3d8c3b',
symbol: 'tEURO',
name: 'tEURO Fairground',
decimals: 5,
quantum: '1',
},
quoteName: 'EURO',
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceSpec',
id: '3f5941ba047dabb3bec23f112db0a8cc8aecbbbc6b34d1366d9383ed1b0f39df',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionExternal',
sourceType: {
__typename: 'DataSourceSpecConfiguration',
signers: [
{
__typename: 'Signer',
signer: {
__typename: 'PubKey',
key: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
},
},
],
filters: [],
},
},
},
},
dataSourceSpecForSettlementData: {
__typename: 'DataSourceSpec',
id: '05ca5485678aa7c705aa6a683fd062714cbcfdac2d1a27641cd27b66168d9c1d',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionExternal',
sourceType: {
__typename: 'DataSourceSpecConfiguration',
signers: [
{
__typename: 'Signer',
signer: {
__typename: 'PubKey',
key: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
},
},
],
filters: [],
},
},
},
},
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
settlementDataProperty: 'prices.TSLA.value',
tradingTerminationProperty: 'termination.TSLA.value',
},
},
},
},
marketTimestamps: {
__typename: 'MarketTimestamps',
open: '2023-07-19T12:05:25.822854221Z',
close: null,
},
},
type: OrderType.TYPE_LIMIT,
side: Side.SIDE_BUY,
size: '10000',
status: OrderStatus.STATUS_ACTIVE,
rejectionReason: null,
price: '15000000',
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTT,
remaining: '5000',
expiresAt: null,
createdAt: '2023-08-01T14:47:34.977742Z',
updatedAt: null,
postOnly: false,
reduceOnly: false,
liquidityProvision: null,
peggedOrder: null,
icebergOrder: {
__typename: 'IcebergOrder',
peakSize: '5000',
minimumVisibleSize: '2000',
reservedRemaining: '5000',
},
__typename: 'Order',
};
render(
<BrowserRouter>
<OrderViewDialog order={order} onChange={jest.fn()} isOpen={true} />
</BrowserRouter>
);
expect(screen.getByTestId('order-market-label')).toHaveTextContent(
'Market'
);
expect(screen.getByTestId('order-market-value')).toHaveTextContent(
'Tesla Quarterly (Sep 2023)'
);
expect(screen.getByTestId('order-type-label')).toHaveTextContent('Type');
expect(screen.getByTestId('order-type-value')).toHaveTextContent('Limit');
expect(screen.getByTestId('order-price-label')).toHaveTextContent('Price');
expect(screen.getByTestId('order-price-value')).toHaveTextContent('150.00');
expect(screen.getByTestId('order-size-label')).toHaveTextContent('Size');
expect(screen.getByTestId('order-size-value')).toHaveTextContent('+10.00');
expect(screen.getByTestId('order-remaining-label')).toHaveTextContent(
'Remaining'
);
expect(screen.getByTestId('order-remaining-value')).toHaveTextContent(
'+5.00'
);
expect(
screen.getByTestId('order-iceberg-order-reserved-remaining-value')
).toHaveTextContent('5.00');
});
});

View File

@ -0,0 +1,280 @@
import {
addDecimalsFormatNumber,
getDateTimeFormat,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { Size } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types';
import {
ButtonLink,
Dialog,
KeyValueTable,
KeyValueTableRow,
Tooltip,
VegaIcon,
VegaIconNames,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
import type { Order } from '../order-data-provider';
import CopyToClipboard from 'react-copy-to-clipboard';
import { useCopyTimeout } from '@vegaprotocol/react-helpers';
interface OrderViewDialogProps {
isOpen: boolean;
order: Order;
onChange: (open: boolean) => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
}
export const OrderViewDialog = ({
isOpen,
order,
onChange,
onMarketClick,
}: OrderViewDialogProps) => {
const [, setCopied] = useCopyTimeout();
return (
<Dialog open={isOpen} title={t('Order details')} onChange={onChange}>
<KeyValueTable>
<KeyValueTableRow key={'order-market'}>
<div data-testid={'order-market-label'}>{t('Market')}</div>
<div data-testid={`order-market-value`}>
{onMarketClick ? (
<ButtonLink
onClick={() => order.market && onMarketClick(order.market?.id)}
>
{order.market?.tradableInstrument.instrument.name}
</ButtonLink>
) : (
order.market?.tradableInstrument.instrument.name
)}
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-type'}>
<div data-testid={'order-type-label'}>{t('Type')}</div>
<div data-testid={`order-type-value`}>
{Schema.OrderTypeMapping[order.type as Schema.OrderType]}
</div>
</KeyValueTableRow>
{order.market && (
<KeyValueTableRow key={'order-price'}>
<div data-testid={'order-price-label'}>{t('Price')}</div>
<div data-testid={`order-price-value`}>
{addDecimalsFormatNumber(
order.price,
order.market.decimalPlaces as number
)}
</div>
</KeyValueTableRow>
)}
{order.market && (
<KeyValueTableRow key={'order-size'}>
<div data-testid={'order-size-label'}>{t('Size')}</div>
<div data-testid={`order-size-value`}>
<Size
value={order.size}
side={order.side}
positionDecimalPlaces={
order.market.positionDecimalPlaces as number
}
/>
</div>
</KeyValueTableRow>
)}
{order.market && (
<KeyValueTableRow key={'order-remaining'} className="mb-4">
<div data-testid={'order-remaining-label'}>{t('Remaining')}</div>
<div data-testid={`order-remaining-value`}>
<Size
value={order.remaining}
side={order.side}
positionDecimalPlaces={
order.market.positionDecimalPlaces as number
}
/>
</div>
</KeyValueTableRow>
)}
<KeyValueTableRow key={'order-status'}>
<div data-testid={'order-status-label'}>{t('Status')}</div>
<div data-testid={`order-status-value`}>
{Schema.OrderStatusMapping[order.status as Schema.OrderStatus]}
</div>
</KeyValueTableRow>
{order.rejectionReason && (
<KeyValueTableRow key={'order-rejection-reason'}>
<div data-testid={'order-rejection-reason-label'}>
{t('rejection reason')}
</div>
<div data-testid={`order-rejection-reason-value`}>
{
Schema.OrderRejectionReasonMapping[
order.rejectionReason as Schema.OrderRejectionReason
]
}
</div>
</KeyValueTableRow>
)}
<KeyValueTableRow key={'order-id'}>
<div data-testid={'order-id-label'}>{t('Order ID')}</div>
<div data-testid={`order-id-value`}>
{truncateMiddle(order.id, 10)}
<CopyToClipboard text={order.id} onCopy={() => setCopied(true)}>
<button
type="button"
data-testid="copy-order-id"
onClick={(e) => e.stopPropagation()}
>
<span className="sr-only">{t('Copy')}</span>
<VegaIcon name={VegaIconNames.COPY} />
</button>
</CopyToClipboard>
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-created'}>
<div data-testid={'order-created-label'}>{t('Created')}</div>
<div data-testid={`order-created-value`}>
{getDateTimeFormat().format(new Date(order.createdAt))}
</div>
</KeyValueTableRow>
{order.updatedAt && (
<KeyValueTableRow key={'order-updated'}>
<div data-testid={'order-updated-label'}>{t('Updated')}</div>
<div data-testid={`order-updated-value`}>
{getDateTimeFormat().format(new Date(order.updatedAt))}
</div>
</KeyValueTableRow>
)}
{order.expiresAt && (
<KeyValueTableRow key={'order-expires'}>
<div data-testid={'order-expires-label'}>{t('Expires')}</div>
<div data-testid={`order-expires-value`}>
{getDateTimeFormat().format(new Date(order.expiresAt))}
</div>
</KeyValueTableRow>
)}
<KeyValueTableRow key={'order-time-in-force'} className="mt-4">
<div data-testid={'order-time-in-force-label'}>
{t('Time in force')}
</div>
<div data-testid={`order-time-in-force-value`}>
{
Schema.OrderTimeInForceMapping[
order.timeInForce as Schema.OrderTimeInForce
]
}
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-post-only'}>
<div data-testid={'order-post-only-label'}>{t('Post only')}</div>
<div data-testid={`order-post-only-value`}>
{order.postOnly ? t('Yes') : t('-')}
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-reduce-only'}>
<div data-testid={'order-reduce-only-label'}>{t('Reduce only')}</div>
<div data-testid={`order-reduce-only-value`}>
{order.reduceOnly ? t('Yes') : t('-')}
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-pegged'}>
<div data-testid={'order-pegged-label'}>{t('Pegged')}</div>
<div data-testid={`order-pegged-value`}>
{order.peggedOrder ? t('Yes') : t('-')}
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-liquidity-provision'}>
<div data-testid={'order-liquidity-provision-label'}>
{t('Liquidity provision')}
</div>
<div data-testid={`order-liquidity-provision-value`}>
{order.liquidityProvision ? t('Yes') : t('-')}
</div>
</KeyValueTableRow>
</KeyValueTable>
<KeyValueTableRow key={'order-iceberg-order'}>
<div data-testid={'order-iceberg-order-label'}>
{t('Iceberg order')}
</div>
<div data-testid={`order-iceberg-order-value`}>
{order.icebergOrder ? t('Yes') : t('-')}
</div>
</KeyValueTableRow>
{order.icebergOrder && (
<KeyValueTableRow
key={'order-iceberg-order-peak-size'}
className="ml-4"
>
<div data-testid={'order-iceberg-order-peak-size-label'}>
<Tooltip
description={t(
'The maximum volume that can be traded at once. Must be less than the total size of the order.'
)}
>
<span>{t('Peak size')}</span>
</Tooltip>
</div>
<div data-testid={`order-iceberg-order-peak-size-value`}>
<Size
value={order.icebergOrder.peakSize}
side={order.side}
positionDecimalPlaces={
order.market?.positionDecimalPlaces as number
}
/>
</div>
</KeyValueTableRow>
)}
{order.icebergOrder && (
<KeyValueTableRow
key={'order-iceberg-order-minimum-visible-size'}
className="ml-4"
>
<div data-testid={'order-iceberg-order-minimum-visible-size-label'}>
<Tooltip
description={t(
'When the order trades and its size falls below this threshold, it will be reset to the peak size and moved to the back of the priority order. Must be less than or equal to peak size, and greater than 0.'
)}
>
<span>{t('Minimum size')}</span>
</Tooltip>
</div>
<div data-testid={`order-iceberg-order-minimum-visible-size-value`}>
<Size
value={order.icebergOrder.minimumVisibleSize}
side={order.side}
positionDecimalPlaces={
order.market?.positionDecimalPlaces as number
}
/>
</div>
</KeyValueTableRow>
)}
{order.icebergOrder && (
<KeyValueTableRow
key={'order-iceberg-order-reserved-remaining'}
className="ml-4"
>
<div data-testid={'order-iceberg-order-reserved-remaining-label'}>
{t('Reserved remaining')}
</div>
<div data-testid={`order-iceberg-order-reserved-remaining-value`}>
<Size
value={order.icebergOrder.reservedRemaining}
side={order.side}
positionDecimalPlaces={
order.market?.positionDecimalPlaces as number
}
/>
</div>
</KeyValueTableRow>
)}
</Dialog>
);
};