fix(trading): order stopped intent toasts (#3347)

This commit is contained in:
m.ray 2023-04-03 15:21:56 -04:00 committed by GitHub
parent 662753c74b
commit 474543d91b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 131 additions and 19 deletions

View File

@ -474,13 +474,16 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
}
if (tx.order && tx.order.rejectionReason) {
const rejectionReason = getRejectionReason(tx.order) || ' ';
const rejectionReason =
getRejectionReason(tx.order) || tx.order.rejectionReason || '';
return (
<>
<ToastHeading>{t('Order rejected')}</ToastHeading>
{rejectionReason ? (
{rejectionReason || tx.order.rejectionReason ? (
<p>
{t('Your order has been rejected because: %s', [rejectionReason])}
{t('Your order has been rejected because: %s', [
rejectionReason || tx.order.rejectionReason,
])}
</p>
) : (
<p>{t('Your order has been rejected.')}</p>
@ -579,7 +582,7 @@ const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => {
if (orderRejection) {
label = t('Order rejected');
errorMessage = t('Your order has been rejected because: %s', [
orderRejection,
orderRejection || tx.order?.rejectionReason || ' ',
]);
}
if (walletError) {
@ -646,7 +649,11 @@ export const useVegaTransactionToasts = () => {
// Transaction can be successful but the order can be rejected by the network
const intent =
tx.order && [OrderStatus.STATUS_REJECTED].includes(tx.order.status)
(tx.order &&
[OrderStatus.STATUS_REJECTED, OrderStatus.STATUS_STOPPED].includes(
tx.order.status
)) ||
tx.order?.rejectionReason
? Intent.Danger
: intentMap[tx.status];

View File

@ -107,7 +107,7 @@ describe('DealTicket', () => {
);
});
it('should use local storage state for initial values reduceOnly and postOnly', () => {
it('should set values for a non-persistent reduce only order and disable post only checkbox', () => {
const expectedOrder = {
marketId: market.id,
type: Schema.OrderType.TYPE_LIMIT,
@ -115,7 +115,7 @@ describe('DealTicket', () => {
size: '0.1',
price: '300.22',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
persist: true,
persist: false,
reduceOnly: true,
postOnly: false,
};
@ -149,6 +149,58 @@ describe('DealTicket', () => {
expect(screen.getByTestId('order-price')).toHaveDisplayValue(
expectedOrder.price
);
expect(screen.getByTestId('post-only')).toBeDisabled();
expect(screen.getByTestId('reduce-only')).toBeEnabled();
expect(screen.getByTestId('reduce-only')).toBeChecked();
expect(screen.getByTestId('post-only')).not.toBeChecked();
});
it('should set values for a persistent post only order and disable reduce only checkbox', () => {
const expectedOrder = {
marketId: market.id,
type: Schema.OrderType.TYPE_LIMIT,
side: Schema.Side.SIDE_SELL,
size: '0.1',
price: '300.22',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
persist: true,
reduceOnly: false,
postOnly: true,
};
useOrderStore.setState({
orders: {
[expectedOrder.marketId]: expectedOrder,
},
});
render(generateJsx());
// Assert correct defaults are used from store
expect(
screen
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
.querySelector('input')
).toBeChecked();
expect(
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
).toBeChecked();
expect(
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
).not.toBeChecked();
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
expectedOrder.size
);
expect(screen.getByTestId('order-tif')).toHaveValue(
expectedOrder.timeInForce
);
expect(screen.getByTestId('order-price')).toHaveDisplayValue(
expectedOrder.price
);
expect(screen.getByTestId('post-only')).toBeEnabled();
expect(screen.getByTestId('reduce-only')).toBeDisabled();
expect(screen.getByTestId('post-only')).toBeChecked();
expect(screen.getByTestId('reduce-only')).not.toBeChecked();
});
it('handles TIF select box dependent on order type', async () => {

View File

@ -167,6 +167,16 @@ export const DealTicket = ({
return disabled;
}, [order]);
const disableReduceOnlyCheckbox = useMemo(() => {
const disabled = order
? ![
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
].includes(order.timeInForce)
: true;
return disabled;
}, [order]);
const onSubmit = useCallback(
(order: OrderSubmission) => {
const now = new Date().getTime();
@ -211,8 +221,18 @@ export const DealTicket = ({
if (type === OrderType.TYPE_NETWORK) return;
update({
type,
// when changing type also update the tif to what was last used of new type
// when changing type also update the TIF to what was last used of new type
timeInForce: lastTIF[type] || order.timeInForce,
postOnly:
type === OrderType.TYPE_MARKET ? false : order.postOnly,
reduceOnly:
type === OrderType.TYPE_LIMIT &&
![
OrderTimeInForce.TIME_IN_FORCE_FOK,
OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(lastTIF[type] || order.timeInForce)
? false
: order.postOnly,
expiresAt: undefined,
});
clearErrors('expiresAt');
@ -260,8 +280,23 @@ export const DealTicket = ({
value={order.timeInForce}
orderType={order.type}
onSelect={(timeInForce) => {
update({ timeInForce, postOnly: false, reduceOnly: false });
// Set tif value for the given order type, so that when switching
// Reset post only and reduce only when changing TIF
update({
timeInForce,
postOnly: [
OrderTimeInForce.TIME_IN_FORCE_FOK,
OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(timeInForce)
? false
: order.postOnly,
reduceOnly: ![
OrderTimeInForce.TIME_IN_FORCE_FOK,
OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(timeInForce)
? false
: order.reduceOnly,
});
// Set TIF value for the given order type, so that when switching
// types we know the last used TIF for the given order type
setLastTIF((curr) => ({
...curr,
@ -336,6 +371,7 @@ export const DealTicket = ({
<Checkbox
name="reduce-only"
checked={order.reduceOnly}
disabled={disableReduceOnlyCheckbox}
onCheckedChange={() => {
update({ postOnly: false, reduceOnly: !order.reduceOnly });
}}
@ -343,9 +379,13 @@ export const DealTicket = ({
<Tooltip
description={
<span>
{t(
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.'
)}
{disableReduceOnlyCheckbox
? t(
'"Reduce only" can be used only with non-persistent orders, such as "Fill or Kill" or "Immediate or Cancel".'
)
: t(
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.'
)}
</span>
}
>

View File

@ -14,6 +14,8 @@ fragment OrderFields on Order {
expiresAt
createdAt
updatedAt
postOnly
reduceOnly
liquidityProvision {
__typename
}

View File

@ -3,14 +3,14 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type OrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, market: { __typename?: 'Market', id: string }, liquidityProvision?: { __typename: 'LiquidityProvision' } | null, peggedOrder?: { __typename: 'PeggedOrder' } | null };
export type OrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, postOnly?: boolean | null, reduceOnly?: boolean | null, market: { __typename?: 'Market', id: string }, liquidityProvision?: { __typename: 'LiquidityProvision' } | null, peggedOrder?: { __typename: 'PeggedOrder' } | null };
export type OrderByIdQueryVariables = Types.Exact<{
orderId: Types.Scalars['ID'];
}>;
export type OrderByIdQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, market: { __typename?: 'Market', id: string }, liquidityProvision?: { __typename: 'LiquidityProvision' } | null, peggedOrder?: { __typename: 'PeggedOrder' } | null } };
export type OrderByIdQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, postOnly?: boolean | null, reduceOnly?: boolean | null, market: { __typename?: 'Market', id: string }, liquidityProvision?: { __typename: 'LiquidityProvision' } | null, peggedOrder?: { __typename: 'PeggedOrder' } | null } };
export type OrdersQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
@ -19,7 +19,7 @@ export type OrdersQueryVariables = Types.Exact<{
}>;
export type OrdersQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, ordersConnection?: { __typename?: 'OrderConnection', edges?: Array<{ __typename?: 'OrderEdge', cursor?: string | null, node: { __typename?: 'Order', id: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, market: { __typename?: 'Market', id: string }, liquidityProvision?: { __typename: 'LiquidityProvision' } | null, peggedOrder?: { __typename: 'PeggedOrder' } | null } }> | null, pageInfo?: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } | null } | null } | null };
export type OrdersQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, ordersConnection?: { __typename?: 'OrderConnection', edges?: Array<{ __typename?: 'OrderEdge', cursor?: string | null, node: { __typename?: 'Order', id: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, postOnly?: boolean | null, reduceOnly?: boolean | null, market: { __typename?: 'Market', id: string }, liquidityProvision?: { __typename: 'LiquidityProvision' } | null, peggedOrder?: { __typename: 'PeggedOrder' } | null } }> | null, pageInfo?: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } | null } | null } | null };
export type OrderUpdateFieldsFragment = { __typename?: 'OrderUpdate', id: string, marketId: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, liquidityProvisionId?: string | null, peggedOrder?: { __typename: 'PeggedOrder' } | null };
@ -47,6 +47,8 @@ export const OrderFieldsFragmentDoc = gql`
expiresAt
createdAt
updatedAt
postOnly
reduceOnly
liquidityProvision {
__typename
}

View File

@ -129,8 +129,9 @@ export const OrderListTable = memo(
}: VegaValueFormatterParams<Order, 'status'>) => {
if (data?.rejectionReason && value) {
return `${Schema.OrderStatusMapping[value]}: ${
data?.rejectionReason &&
Schema.OrderRejectionReasonMapping[data.rejectionReason]
(data?.rejectionReason &&
Schema.OrderRejectionReasonMapping[data.rejectionReason]) ||
data?.rejectionReason
}`;
}
return value ? Schema.OrderStatusMapping[value] : '';
@ -218,7 +219,14 @@ export const OrderListTable = memo(
return `${Schema.OrderTimeInForceMapping[value]}: ${expiry}`;
}
return value ? Schema.OrderTimeInForceMapping[value] : '';
const tifLabel = value
? Schema.OrderTimeInForceMapping[value]
: '';
const label = `${tifLabel}${
data?.postOnly ? t('. Post Only') : ''
}${data?.reduceOnly ? t('. Reduce only') : ''}`;
return label;
}}
minWidth={150}
/>

View File

@ -38,6 +38,7 @@ export const Checkbox = ({
checked={checked}
onCheckedChange={onCheckedChange}
disabled={disabled}
data-testid={name}
>
<CheckboxPrimitive.CheckboxIndicator className="flex justify-center items-center w-[15px] h-[15px] bg-black dark:bg-white">
{checked === 'indeterminate' ? (