From 474543d91b8d9d1c3b88efc33393d4033b342399 Mon Sep 17 00:00:00 2001 From: "m.ray" <16125548+MadalinaRaicu@users.noreply.github.com> Date: Mon, 3 Apr 2023 15:21:56 -0400 Subject: [PATCH] fix(trading): order stopped intent toasts (#3347) --- .../lib/hooks/use-vega-transaction-toasts.tsx | 17 ++++-- .../deal-ticket/deal-ticket.spec.tsx | 56 ++++++++++++++++++- .../components/deal-ticket/deal-ticket.tsx | 52 +++++++++++++++-- .../order-data-provider/Orders.graphql | 2 + .../__generated__/Orders.ts | 8 ++- .../lib/components/order-list/order-list.tsx | 14 ++++- .../src/components/checkbox/checkbox.tsx | 1 + 7 files changed, 131 insertions(+), 19 deletions(-) diff --git a/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx b/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx index 50bf9e9e6..2b394c5f5 100644 --- a/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx +++ b/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx @@ -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 ( <> {t('Order rejected')} - {rejectionReason ? ( + {rejectionReason || tx.order.rejectionReason ? (

- {t('Your order has been rejected because: %s', [rejectionReason])} + {t('Your order has been rejected because: %s', [ + rejectionReason || tx.order.rejectionReason, + ])}

) : (

{t('Your order has been rejected.')}

@@ -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]; diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx index 8b1403c70..385fb33e5 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx @@ -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 () => { diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx index 2ad012c6d..84bb027e5 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx @@ -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 = ({ { update({ postOnly: false, reduceOnly: !order.reduceOnly }); }} @@ -343,9 +379,13 @@ export const DealTicket = ({ - {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.' + )} } > diff --git a/libs/orders/src/lib/components/order-data-provider/Orders.graphql b/libs/orders/src/lib/components/order-data-provider/Orders.graphql index 08a06fa6f..5a409e326 100644 --- a/libs/orders/src/lib/components/order-data-provider/Orders.graphql +++ b/libs/orders/src/lib/components/order-data-provider/Orders.graphql @@ -14,6 +14,8 @@ fragment OrderFields on Order { expiresAt createdAt updatedAt + postOnly + reduceOnly liquidityProvision { __typename } diff --git a/libs/orders/src/lib/components/order-data-provider/__generated__/Orders.ts b/libs/orders/src/lib/components/order-data-provider/__generated__/Orders.ts index 6b89f9412..6ea18be70 100644 --- a/libs/orders/src/lib/components/order-data-provider/__generated__/Orders.ts +++ b/libs/orders/src/lib/components/order-data-provider/__generated__/Orders.ts @@ -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 } diff --git a/libs/orders/src/lib/components/order-list/order-list.tsx b/libs/orders/src/lib/components/order-list/order-list.tsx index b190e9287..3d484e15d 100644 --- a/libs/orders/src/lib/components/order-list/order-list.tsx +++ b/libs/orders/src/lib/components/order-list/order-list.tsx @@ -129,8 +129,9 @@ export const OrderListTable = memo( }: VegaValueFormatterParams) => { 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} /> diff --git a/libs/ui-toolkit/src/components/checkbox/checkbox.tsx b/libs/ui-toolkit/src/components/checkbox/checkbox.tsx index 267b503cb..78994b6b2 100644 --- a/libs/ui-toolkit/src/components/checkbox/checkbox.tsx +++ b/libs/ui-toolkit/src/components/checkbox/checkbox.tsx @@ -38,6 +38,7 @@ export const Checkbox = ({ checked={checked} onCheckedChange={onCheckedChange} disabled={disabled} + data-testid={name} > {checked === 'indeterminate' ? (