diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 336052ddd..15ffe688e 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -1,15 +1,14 @@ --- name: Release -about: - A template to outline the steps needed to for a successful release of our frontend apps +about: A template to outline the steps needed to for a successful release of our frontend apps title: 'Release [add dapp version]-core-[add core version]' -labels: +labels: assignees: '' --- ### Tasks -- [ ] Review [link to core release](xxx) +- [ ] Review [link to core release](xxx) - [ ] Tag frontend-monorepo - [ ] Create release and generate release notes - [ ] Run `@smoke` tests diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts index d3575cfbf..210b979ab 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts @@ -84,6 +84,8 @@ describe('must submit order', { tags: '@smoke' }, () => { type: Schema.OrderType.TYPE_MARKET, side: Schema.Side.SIDE_BUY, timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK, + postOnly: false, + reduceOnly: false, size: '100', }; createOrder(order); @@ -98,6 +100,8 @@ describe('must submit order', { tags: '@smoke' }, () => { side: Schema.Side.SIDE_SELL, timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC, size: '100', + postOnly: false, + reduceOnly: false, }; createOrder(order); testOrderSubmission(order); @@ -112,6 +116,8 @@ describe('must submit order', { tags: '@smoke' }, () => { side: Schema.Side.SIDE_BUY, timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC, size: '100', + postOnly: false, + reduceOnly: false, price: '200', }; createOrder(order); @@ -126,6 +132,8 @@ describe('must submit order', { tags: '@smoke' }, () => { side: Schema.Side.SIDE_SELL, timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GFN, size: '100', + postOnly: false, + reduceOnly: false, price: '50000', }; createOrder(order); @@ -143,6 +151,8 @@ describe('must submit order', { tags: '@smoke' }, () => { size: '100', price: '1.00', expiresAt: expiresAt.toISOString().substring(0, 16), + postOnly: false, + reduceOnly: false, }; createOrder(order); @@ -150,6 +160,8 @@ describe('must submit order', { tags: '@smoke' }, () => { price: '100000', expiresAt: new Date(order.expiresAt as string).getTime().toString() + '000000', + postOnly: false, + reduceOnly: false, }); }); }); @@ -182,6 +194,8 @@ describe( side: Schema.Side.SIDE_BUY, timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC, size: '100', + postOnly: false, + reduceOnly: false, price: '200', }; createOrder(order); @@ -196,6 +210,8 @@ describe( side: Schema.Side.SIDE_SELL, timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC, size: '100', + postOnly: false, + reduceOnly: false, price: '50000', }; createOrder(order); @@ -210,12 +226,16 @@ describe( side: Schema.Side.SIDE_SELL, timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT, size: '100', + postOnly: false, + reduceOnly: false, price: '1.00', expiresAt: displayTomorrow(), }; createOrder(order); testOrderSubmission(order, { price: '100000', + postOnly: false, + reduceOnly: false, expiresAt: new Date(order.expiresAt as string).getTime().toString() + '000000', }); @@ -251,6 +271,8 @@ describe( side: Schema.Side.SIDE_BUY, timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC, size: '100', + postOnly: false, + reduceOnly: false, price: '200', }; createOrder(order); @@ -265,6 +287,8 @@ describe( side: Schema.Side.SIDE_SELL, timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC, size: '100', + postOnly: false, + reduceOnly: false, price: '50000', }; createOrder(order); @@ -281,6 +305,8 @@ describe( size: '100', price: '1.00', expiresAt: displayTomorrow(), + postOnly: false, + reduceOnly: false, }; createOrder(order); testOrderSubmission(order, { @@ -320,6 +346,8 @@ describe( side: Schema.Side.SIDE_BUY, timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC, size: '100', + postOnly: false, + reduceOnly: false, price: '200', }; createOrder(order); @@ -335,6 +363,8 @@ describe( timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC, size: '100', price: '50000', + postOnly: false, + reduceOnly: false, }; createOrder(order); testOrderSubmission(order, { price: '5000000000' }); @@ -350,6 +380,8 @@ describe( size: '100', price: '1.00', expiresAt: displayTomorrow(), + postOnly: false, + reduceOnly: false, }; createOrder(order); testOrderSubmission(order, { diff --git a/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx b/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx index 55b84c85a..50bf9e9e6 100644 --- a/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx +++ b/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx @@ -474,15 +474,17 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => { } if (tx.order && tx.order.rejectionReason) { + const rejectionReason = getRejectionReason(tx.order) || ' '; return ( <> {t('Order rejected')} -

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

+ {rejectionReason ? ( +

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

+ ) : ( +

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

+ )} {tx.txHash && (

{ walletNoConnectionCodes.includes(tx.error.code); if (orderRejection) { label = t('Order rejected'); - errorMessage = t( - 'Your order has been rejected because: %s', - orderRejection - ); + errorMessage = t('Your order has been rejected because: %s', [ + orderRejection, + ]); } if (walletError) { label = t('Wallet disconnected'); 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 71b99cf2b..8b1403c70 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,6 +107,50 @@ describe('DealTicket', () => { ); }); + it('should use local storage state for initial values reduceOnly and postOnly', () => { + 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_IOC, + persist: true, + reduceOnly: true, + postOnly: false, + }; + + 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 + ); + }); + it('handles TIF select box dependent on order type', async () => { render(generateJsx()); 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 8b6779123..e784f293c 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx @@ -1,6 +1,6 @@ import { t } from '@vegaprotocol/i18n'; import * as Schema from '@vegaprotocol/types'; -import { memo, useCallback, useEffect, useState, useRef } from 'react'; +import { memo, useCallback, useEffect, useState, useRef, useMemo } from 'react'; import { Controller } from 'react-hook-form'; import { DealTicketAmount } from './deal-ticket-amount'; import { DealTicketButton } from './deal-ticket-button'; @@ -16,10 +16,12 @@ import { useVegaWalletDialogStore, } from '@vegaprotocol/wallet'; import { + Checkbox, ExternalLink, InputError, Intent, Notification, + Tooltip, TinyScroll, } from '@vegaprotocol/ui-toolkit'; @@ -146,6 +148,16 @@ export const DealTicket = ({ clearErrors, ]); + const disablePostOnlyCheckbox = 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(); @@ -239,7 +251,7 @@ export const DealTicket = ({ value={order.timeInForce} orderType={order.type} onSelect={(timeInForce) => { - update({ timeInForce }); + update({ timeInForce, postOnly: false, reduceOnly: false }); // 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) => ({ @@ -276,6 +288,65 @@ export const DealTicket = ({ )} /> )} +

+ ( + { + update({ postOnly: !order.postOnly, reduceOnly: false }); + }} + label={ + + {disablePostOnlyCheckbox + ? t( + '"Post only" can not be used on "Fill or Kill" or "Immediate or Cancel" orders.' + ) + : t( + '"Post only" will ensure the order is not filled immediately but is placed on the order book as a passive order. When the order is processed it is either stopped (if it would not be filled immediately), or placed in the order book as a passive order until the price taker matches with it.' + )} + + } + > + {t('Post only')} + + } + /> + )} + /> + ( + { + update({ postOnly: false, reduceOnly: !order.reduceOnly }); + }} + label={ + + {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.' + )} + + } + > + {t('Reduce only')} + + } + /> + )} + /> +
{ - // setPreviousTimeInForce({ - // ...previousTimeInForce, - // [orderType]: e.target.value, - // }); - - // if (previousOrderType !== orderType) { - // setPreviousOrderType(orderType); - // const prev = previousTimeInForce[orderType as OrderType]; - // onSelect(prev); - // } onSelect(e.target.value as Schema.OrderTimeInForce); }} className="w-full" diff --git a/libs/deal-ticket/src/setup-tests.ts b/libs/deal-ticket/src/setup-tests.ts index 068c53d36..e62ea0326 100644 --- a/libs/deal-ticket/src/setup-tests.ts +++ b/libs/deal-ticket/src/setup-tests.ts @@ -1,2 +1,7 @@ import '@testing-library/jest-dom'; import 'jest-canvas-mock'; +import ResizeObserver from 'resize-observer-polyfill'; +import { defaultFallbackInView } from 'react-intersection-observer'; + +defaultFallbackInView(true); +global.ResizeObserver = ResizeObserver; diff --git a/libs/orders/src/lib/order-hooks/use-order-store.ts b/libs/orders/src/lib/order-hooks/use-order-store.ts index c79c29d31..00dae623f 100644 --- a/libs/orders/src/lib/order-hooks/use-order-store.ts +++ b/libs/orders/src/lib/order-hooks/use-order-store.ts @@ -13,6 +13,8 @@ export type OrderObj = { price?: string; expiresAt?: string | undefined; persist: boolean; // key used to determine if order should be kept in localStorage + postOnly?: boolean; + reduceOnly?: boolean; }; type OrderMap = { [marketId: string]: OrderObj | undefined }; @@ -114,4 +116,6 @@ export const getDefaultOrder = (marketId: string): OrderObj => ({ price: '0', expiresAt: undefined, persist: false, + postOnly: false, + reduceOnly: false, }); diff --git a/libs/ui-toolkit/src/components/checkbox/checkbox.tsx b/libs/ui-toolkit/src/components/checkbox/checkbox.tsx index fed4c2aa1..267b503cb 100644 --- a/libs/ui-toolkit/src/components/checkbox/checkbox.tsx +++ b/libs/ui-toolkit/src/components/checkbox/checkbox.tsx @@ -54,7 +54,12 @@ export const Checkbox = ({ )} -