From a8c2f4e025b0a4c9232858d6e6ef207e9363a011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Thu, 31 Aug 2023 11:54:52 +0200 Subject: [PATCH] feat(deal-ticket): update deal ticket submit buttons (#4635) --- .../trading-deal-ticket-order.cy.ts | 4 +- .../deal-ticket/deal-ticket-button.tsx | 25 - .../deal-ticket/deal-ticket-fee-details.tsx | 132 ++--- .../deal-ticket/deal-ticket-size-iceberg.tsx | 4 +- .../deal-ticket-stop-order.spec.tsx | 291 ++++++++--- .../deal-ticket/deal-ticket-stop-order.tsx | 455 +++++++++++++++--- .../deal-ticket/deal-ticket.spec.tsx | 158 +++++- .../components/deal-ticket/deal-ticket.tsx | 93 +++- .../src/components/deal-ticket/key-value.tsx | 51 ++ .../src/components/trading-input/input.tsx | 13 +- 10 files changed, 914 insertions(+), 312 deletions(-) delete mode 100644 libs/deal-ticket/src/components/deal-ticket/deal-ticket-button.tsx create mode 100644 libs/deal-ticket/src/components/deal-ticket/key-value.tsx diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts index 9b83ed530..fe82aee3e 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts @@ -33,9 +33,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => { it('must see the price unit', function () { // 7002-SORD-018 - cy.getByTestId(orderPriceField) - .siblings('label') - .should('have.text', 'Price (DAI)'); + cy.getByTestId(orderPriceField).next().should('have.text', 'DAI'); }); it('must see warning when placing an order with expiry date in past', () => { diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-button.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-button.tsx deleted file mode 100644 index 39070688c..000000000 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-button.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { t } from '@vegaprotocol/i18n'; -import { Side } from '@vegaprotocol/types'; -import classNames from 'classnames'; - -interface Props { - side: Side; - label?: string; -} - -export const DealTicketButton = ({ side, label }: Props) => { - const buttonClasses = classNames( - 'px-10 py-2 uppercase rounded-md text-white w-full', - { - 'bg-market-red': side === Side.SIDE_SELL, - 'bg-market-green-550': side === Side.SIDE_BUY, - } - ); - return ( -
- -
- ); -}; diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx index 7df3041f3..182ab55fa 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx @@ -1,7 +1,4 @@ import { useCallback, useState } from 'react'; -import { Tooltip } from '@vegaprotocol/ui-toolkit'; -import classnames from 'classnames'; -import type { ReactNode } from 'react'; import { t } from '@vegaprotocol/i18n'; import { FeesBreakdown } from '@vegaprotocol/markets'; import type { OrderSubmissionBody } from '@vegaprotocol/wallet'; @@ -16,7 +13,6 @@ import { marketMarginDataProvider } from '@vegaprotocol/accounts'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { - NOTIONAL_SIZE_TOOLTIP_TEXT, MARGIN_DIFF_TOOLTIP_TEXT, DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT, TOTAL_MARGIN_AVAILABLE, @@ -25,114 +21,54 @@ import { MARGIN_ACCOUNT_TOOLTIP_TEXT, } from '../../constants'; import { useEstimateFees } from '../../hooks'; +import { KeyValue } from './key-value'; const emptyValue = '-'; -export interface DealTicketFeeDetailPros { - label: string; - value?: string | null | undefined; - symbol: string; - indent?: boolean | undefined; - labelDescription?: ReactNode; - formattedValue?: string; - onClick?: () => void; -} - -export const DealTicketFeeDetail = ({ - label, - value, - labelDescription, - symbol, - indent, - onClick, - formattedValue, -}: DealTicketFeeDetailPros) => { - const displayValue = `${formattedValue ?? '-'} ${symbol || ''}`; - const valueElement = onClick ? ( - - ) : ( -
{displayValue}
- ); - return ( -
- -
{label}
-
- - {valueElement} - -
- ); -}; - export interface DealTicketFeeDetailsProps { assetSymbol: string; order: OrderSubmissionBody['orderSubmission']; market: Market; - notionalSize: string | null; } export const DealTicketFeeDetails = ({ assetSymbol, order, market, - notionalSize, }: DealTicketFeeDetailsProps) => { const feeEstimate = useEstimateFees(order); const { settlementAsset: asset } = market.tradableInstrument.instrument.product; const { decimals: assetDecimals, quantum } = asset; - const marketDecimals = market.decimalPlaces; - const quoteName = market.tradableInstrument.instrument.product.quoteName; return ( - <> - - - - {t( - `An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.` - )} - - - - } - symbol={assetSymbol} - /> - + + + {t( + `An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.` + )} + + + + } + symbol={assetSymbol} + /> ); }; @@ -209,7 +145,7 @@ export const DealTicketMarginDetails = ({ BigInt(marginAccountBalance); deductionFromCollateral = ( - ); projectedMargin = ( - - - {deductionFromCollateral} - {projectedMargin} - { if (peakSizeError) { return ( - + {peakSizeError} ); @@ -44,7 +44,7 @@ export const DealTicketSizeIceberg = ({ const renderMinimumSizeError = () => { if (minimumVisibleSizeError) { return ( - + {minimumVisibleSizeError} ); diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.spec.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.spec.tsx index c389b0541..ce1762733 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.spec.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.spec.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { VegaWalletContext } from '@vegaprotocol/wallet'; -import { render, screen, waitFor } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { generateMarket } from '../../test-helpers'; import { StopOrder } from './deal-ticket-stop-order'; @@ -12,6 +12,7 @@ import { useDealTicketFormValues, } from '../../hooks/use-form-values'; import type { FeatureFlags } from '@vegaprotocol/environment'; +import { formatForInput } from '@vegaprotocol/utils'; jest.mock('zustand'); jest.mock('./deal-ticket-fee-details', () => ({ @@ -57,7 +58,7 @@ const orderSideBuy = 'order-side-SIDE_BUY'; const orderSideSell = 'order-side-SIDE_SELL'; const triggerDirectionRisesAbove = 'triggerDirection-risesAbove'; -// const triggerDirectionFallsBelow = 'triggerDirection-fallsBelow'; +const triggerDirectionFallsBelow = 'triggerDirection-fallsBelow'; const expiryStrategySubmit = 'expiryStrategy-submit'; const expiryStrategyCancel = 'expiryStrategy-cancel'; @@ -65,6 +66,7 @@ const expiryStrategyCancel = 'expiryStrategy-cancel'; const triggerTypePrice = 'triggerType-price'; const triggerTypeTrailingPercentOffset = 'triggerType-trailingPercentOffset'; +const oco = 'oco'; const expire = 'expire'; const datePicker = 'date-picker-field'; const timeInForce = 'order-tif'; @@ -76,6 +78,8 @@ const triggerPriceWarningMessage = 'stop-order-warning-message-trigger-price'; const triggerTrailingPercentOffsetErrorMessage = 'stop-order-error-message-trigger-trailing-percent-offset'; +const ocoPostfix = (id: string, postfix = true) => (postfix ? `${id}-oco` : id); + describe('StopOrder', () => { beforeEach(() => { localStorage.clear(); @@ -107,6 +111,7 @@ describe('StopOrder', () => { 'checked' ); expect(screen.getByTestId(expire).dataset.state).toEqual('unchecked'); + expect(screen.getByTestId(oco).dataset.state).toEqual('unchecked'); await userEvent.click(screen.getByTestId(expire)); await waitFor(() => { expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual( @@ -115,6 +120,32 @@ describe('StopOrder', () => { }); }); + it('calculate notional for market limit', async () => { + render(generateJsx()); + await userEvent.type(screen.getByTestId(sizeInput), '10'); + await userEvent.type(screen.getByTestId(priceInput), '10'); + expect(screen.getByTestId('deal-ticket-fee-notional')).toHaveTextContent( + 'Notional100.00 BTC' + ); + }); + + it('calculates notional for limit order', async () => { + render(generateJsx()); + await userEvent.click(screen.getByTestId(orderTypeTrigger)); + await userEvent.click(screen.getByTestId(orderTypeMarket)); + await userEvent.type(screen.getByTestId(sizeInput), '10'); + // price trigger is selected but it's empty, calculate base on size and marketPrice prop + expect(screen.getByTestId('deal-ticket-fee-notional')).toHaveTextContent( + 'Notional20.00 BTC' + ); + + await userEvent.type(screen.getByTestId(triggerPriceInput), '3'); + // calculate base on size and price trigger + expect(screen.getByTestId('deal-ticket-fee-notional')).toHaveTextContent( + 'Notional30.00 BTC' + ); + }); + it('should use local storage state for initial values', async () => { const values: Partial = { type: Schema.OrderType.TYPE_LIMIT, @@ -125,6 +156,11 @@ describe('StopOrder', () => { expire: true, expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS, expiresAt: '2023-07-27T16:43:27.000', + oco: true, + ocoType: Schema.OrderType.TYPE_LIMIT, + ocoSize: '0.2', + ocoPrice: '300.23', + ocoTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK, }; useDealTicketFormValues.setState({ @@ -143,10 +179,22 @@ describe('StopOrder', () => { expect(screen.getByTestId(sizeInput)).toHaveDisplayValue( values.size as string ); - expect(screen.getByTestId('order-tif')).toHaveValue(values.timeInForce); + expect(screen.getByTestId(timeInForce)).toHaveValue(values.timeInForce); expect(screen.getByTestId(priceInput)).toHaveDisplayValue( values.price as string ); + + expect(screen.getByTestId(ocoPostfix(sizeInput))).toHaveDisplayValue( + values.ocoSize as string + ); + expect(screen.getByTestId(ocoPostfix(timeInForce))).toHaveValue( + values.ocoTimeInForce + ); + expect(screen.getByTestId(ocoPostfix(priceInput))).toHaveDisplayValue( + values.ocoPrice as string + ); + expect(screen.getByTestId('ocoTypeLimit').dataset.state).toEqual('checked'); + expect(screen.getByTestId(expire).dataset.state).toEqual('checked'); expect(screen.getByTestId(expiryStrategyCancel).dataset.state).toEqual( 'checked' @@ -154,6 +202,9 @@ describe('StopOrder', () => { expect(screen.getByTestId(datePicker)).toHaveDisplayValue( values.expiresAt as string ); + + await userEvent.click(screen.getByTestId(orderTypeMarket)); + expect(screen.getByTestId(oco).dataset.state).toEqual('unchecked'); }); it('does not submit if no wallet connected', async () => { @@ -174,145 +225,239 @@ describe('StopOrder', () => { expect(submit).toBeCalled(); }); - it('validates size field', async () => { + it.each([ + { fieldName: 'size', ocoValue: false }, + { fieldName: 'ocoSize', ocoValue: true }, + ])('validates $fieldName field', async ({ ocoValue }) => { render(generateJsx()); - + if (ocoValue) { + await userEvent.click(screen.getByTestId(oco)); + } await userEvent.click(screen.getByTestId(submitButton)); - + const getByTestId = (id: string) => + screen.getByTestId(ocoPostfix(id, ocoValue)); + const queryByTestId = (id: string) => + screen.queryByTestId(ocoPostfix(id, ocoValue)); // default value should be invalid - expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument(); + expect(getByTestId(sizeErrorMessage)).toBeInTheDocument(); // to small value should be invalid - await userEvent.type(screen.getByTestId(sizeInput), '0.01'); - expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument(); + await userEvent.type(getByTestId(sizeInput), '0.01'); + expect(getByTestId(sizeErrorMessage)).toBeInTheDocument(); // clear and fill using valid value - await userEvent.clear(screen.getByTestId(sizeInput)); - await userEvent.type(screen.getByTestId(sizeInput), '0.1'); - expect(screen.queryByTestId(sizeErrorMessage)).toBeNull(); + await userEvent.clear(getByTestId(sizeInput)); + await userEvent.type(getByTestId(sizeInput), '0.1'); + expect(queryByTestId(sizeErrorMessage)).toBeNull(); }); - it('validates price field', async () => { + it.each([ + { fieldName: 'price', ocoValue: false }, + { fieldName: 'ocoPrice', ocoValue: true }, + ])('validates $fieldName field', async ({ ocoValue }) => { render(generateJsx()); - + if (ocoValue) { + await userEvent.click(screen.getByTestId(oco)); + } await userEvent.click(screen.getByTestId(submitButton)); - // price error message should not show if size has error - // expect(screen.queryByTestId(priceErrorMessage)).toBeNull(); - // await userEvent.type(screen.getByTestId(sizeInput), '0.1'); - expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument(); - await userEvent.type(screen.getByTestId(priceInput), '0.001'); - expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument(); + + const getByTestId = (id: string) => + screen.getByTestId(ocoPostfix(id, ocoValue)); + const queryByTestId = (id: string) => + screen.queryByTestId(ocoPostfix(id, ocoValue)); + + expect(getByTestId(priceErrorMessage)).toBeInTheDocument(); + await userEvent.type(getByTestId(priceInput), '0.001'); + expect(getByTestId(priceErrorMessage)).toBeInTheDocument(); // switch to market order type error should disappear await userEvent.click(screen.getByTestId(orderTypeTrigger)); await userEvent.click(screen.getByTestId(orderTypeMarket)); await userEvent.click(screen.getByTestId(submitButton)); - expect(screen.queryByTestId(priceErrorMessage)).toBeNull(); + expect(queryByTestId(priceErrorMessage)).toBeNull(); // switch back to limit type await userEvent.click(screen.getByTestId(orderTypeTrigger)); await userEvent.click(screen.getByTestId(orderTypeLimit)); await userEvent.click(screen.getByTestId(submitButton)); - expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument(); + expect(getByTestId(priceErrorMessage)).toBeInTheDocument(); // to small value should be invalid - await userEvent.type(screen.getByTestId(priceInput), '0.001'); - expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument(); + await userEvent.type(getByTestId(priceInput), '0.001'); + expect(getByTestId(priceErrorMessage)).toBeInTheDocument(); // clear and fill using valid value - await userEvent.clear(screen.getByTestId(priceInput)); - await userEvent.type(screen.getByTestId(priceInput), '0.01'); - expect(screen.queryByTestId(priceErrorMessage)).toBeNull(); + await userEvent.clear(getByTestId(priceInput)); + await userEvent.type(getByTestId(priceInput), '0.01'); + expect(queryByTestId(priceErrorMessage)).toBeNull(); }); - it('validates trigger price field', async () => { + it.each([ + { fieldName: 'triggerPrice', ocoValue: false }, + { fieldName: 'ocoTriggerPrice', ocoValue: true }, + ])('validates $fieldName field', async ({ ocoValue }) => { render(generateJsx()); + if (ocoValue) { + await userEvent.click(screen.getByTestId(oco)); + await userEvent.click(screen.getByTestId(triggerDirectionFallsBelow)); + } await userEvent.click(screen.getByTestId(submitButton)); - expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument(); + const getByTestId = (id: string) => + screen.getByTestId(ocoPostfix(id, ocoValue)); + const queryByTestId = (id: string) => + screen.queryByTestId(ocoPostfix(id, ocoValue)); + expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument(); // switch to trailing percentage offset trigger type - await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset)); - expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull(); + await userEvent.click(getByTestId(triggerTypeTrailingPercentOffset)); + expect(queryByTestId(triggerPriceErrorMessage)).toBeNull(); // switch back to price trigger type - await userEvent.click(screen.getByTestId(triggerTypePrice)); - expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument(); + await userEvent.click(getByTestId(triggerTypePrice)); + expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument(); // to small value should be invalid - await userEvent.type(screen.getByTestId(triggerPriceInput), '0.001'); - expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument(); + await userEvent.type(getByTestId(triggerPriceInput), '0.001'); + expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument(); // clear and fill using value causing immediate trigger - await userEvent.clear(screen.getByTestId(triggerPriceInput)); - await userEvent.type(screen.getByTestId(triggerPriceInput), '0.01'); - expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull(); - expect( - screen.queryByTestId(triggerPriceWarningMessage) - ).toBeInTheDocument(); + await userEvent.clear(getByTestId(triggerPriceInput)); + await userEvent.type(getByTestId(triggerPriceInput), '0.01'); + expect(queryByTestId(triggerPriceErrorMessage)).toBeNull(); + expect(queryByTestId(triggerPriceWarningMessage)).toBeInTheDocument(); // change to correct value - await userEvent.type(screen.getByTestId(triggerPriceInput), '2'); - expect(screen.queryByTestId(triggerPriceWarningMessage)).toBeNull(); + await userEvent.type(getByTestId(triggerPriceInput), '2'); + expect(queryByTestId(triggerPriceWarningMessage)).toBeNull(); }); - it('validates trigger trailing percentage offset field', async () => { + it.each([ + { fieldName: 'trailingPercentageOffset', ocoValue: false }, + { fieldName: 'ocoTrailingPercentageOffset', ocoValue: true }, + ])('validates $fieldName field', async ({ ocoValue }) => { render(generateJsx()); + if (ocoValue) { + await userEvent.click(screen.getByTestId(oco)); + } + await userEvent.click(screen.getByTestId(submitButton)); + const getByTestId = (id: string) => + screen.getByTestId(ocoPostfix(id, ocoValue)); + const queryByTestId = (id: string) => + screen.queryByTestId(ocoPostfix(id, ocoValue)); // should not show error with default form values - await userEvent.click(screen.getByTestId(submitButton)); - expect( - screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage) - ).toBeNull(); + expect(queryByTestId(triggerTrailingPercentOffsetErrorMessage)).toBeNull(); // switch to trailing percentage offset trigger type - await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset)); + await userEvent.click(getByTestId(triggerTypeTrailingPercentOffset)); expect( - screen.getByTestId(triggerTrailingPercentOffsetErrorMessage) + getByTestId(triggerTrailingPercentOffsetErrorMessage) ).toBeInTheDocument(); // to small value should be invalid await userEvent.type( - screen.getByTestId(triggerTrailingPercentOffsetInput), + getByTestId(triggerTrailingPercentOffsetInput), '0.09' ); expect( - screen.getByTestId(triggerTrailingPercentOffsetErrorMessage) + getByTestId(triggerTrailingPercentOffsetErrorMessage) ).toBeInTheDocument(); // clear and fill using valid value - await userEvent.clear( - screen.getByTestId(triggerTrailingPercentOffsetInput) - ); - await userEvent.type( - screen.getByTestId(triggerTrailingPercentOffsetInput), - '0.1' - ); - expect( - screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage) - ).toBeNull(); + await userEvent.clear(getByTestId(triggerTrailingPercentOffsetInput)); + await userEvent.type(getByTestId(triggerTrailingPercentOffsetInput), '0.1'); + expect(queryByTestId(triggerTrailingPercentOffsetErrorMessage)).toBeNull(); // to big value should be invalid - await userEvent.clear( - screen.getByTestId(triggerTrailingPercentOffsetInput) - ); + await userEvent.clear(getByTestId(triggerTrailingPercentOffsetInput)); await userEvent.type( - screen.getByTestId(triggerTrailingPercentOffsetInput), + getByTestId(triggerTrailingPercentOffsetInput), '99.91' ); expect( - screen.getByTestId(triggerTrailingPercentOffsetErrorMessage) + getByTestId(triggerTrailingPercentOffsetErrorMessage) ).toBeInTheDocument(); // clear and fill using valid value - await userEvent.clear( - screen.getByTestId(triggerTrailingPercentOffsetInput) - ); + await userEvent.clear(getByTestId(triggerTrailingPercentOffsetInput)); await userEvent.type( - screen.getByTestId(triggerTrailingPercentOffsetInput), + getByTestId(triggerTrailingPercentOffsetInput), '99.9' ); + expect(queryByTestId(triggerTrailingPercentOffsetErrorMessage)).toBeNull(); + }); + + it('sync oco trigger', async () => { + render(generateJsx()); + await userEvent.click(screen.getByTestId(oco)); expect( - screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage) - ).toBeNull(); + screen.getByTestId(triggerDirectionRisesAbove).dataset.state + ).toEqual('checked'); + expect( + screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow)).dataset.state + ).toEqual('checked'); + await userEvent.click(screen.getByTestId(triggerDirectionFallsBelow)); + expect( + screen.getByTestId(triggerDirectionRisesAbove).dataset.state + ).toEqual('unchecked'); + expect( + screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow)).dataset.state + ).toEqual('unchecked'); + await userEvent.click( + screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow)) + ); + expect( + screen.getByTestId(triggerDirectionRisesAbove).dataset.state + ).toEqual('checked'); + expect( + screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow)).dataset.state + ).toEqual('checked'); + }); + + it('disables submit expiry strategy when OCO selected', async () => { + render(generateJsx()); + await userEvent.click(screen.getByTestId(expire)); + await userEvent.click(screen.getByTestId(expiryStrategySubmit)); + await userEvent.click(screen.getByTestId(oco)); + expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual( + 'unchecked' + ); + expect(screen.getByTestId(expiryStrategySubmit)).toBeDisabled(); + await userEvent.click(screen.getByTestId(oco)); + await userEvent.click(screen.getByTestId(expiryStrategySubmit)); + expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual( + 'checked' + ); + expect(screen.getByTestId(expiryStrategySubmit)).not.toBeDisabled(); + }); + + it('sets expiry time/date to now if expiry is changed to checked', async () => { + const now = Math.round(Date.now() / 1000) * 1000; + render(generateJsx()); + jest.spyOn(global.Date, 'now').mockImplementationOnce(() => now); + await userEvent.click(screen.getByTestId(expire)); + + // expiry time/date was empty it should be set to now + expect( + new Date(screen.getByTestId(datePicker).value).getTime() + ).toEqual(now); + + // set to the value in the past (now - 1s) + fireEvent.change(screen.getByTestId(datePicker), { + target: { value: formatForInput(new Date(now - 1000)) }, + }); + expect( + new Date( + screen.getByTestId(datePicker).value + ).getTime() + 1000 + ).toEqual(now); + + // switch expiry off and on + await userEvent.click(screen.getByTestId(expire)); + await userEvent.click(screen.getByTestId(expire)); + // expiry time/date was in the past it should be set to now + expect( + new Date(screen.getByTestId(datePicker).value).getTime() + ).toEqual(now); }); }); diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx index b18e72e4d..cf9368dad 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx @@ -1,8 +1,12 @@ import { useRef, useCallback, useEffect } from 'react'; import { useVegaWallet } from '@vegaprotocol/wallet'; -import type { StopOrdersSubmission } from '@vegaprotocol/wallet'; +import type { + OrderSubmissionBody, + StopOrdersSubmission, +} from '@vegaprotocol/wallet'; import { formatForInput, + formatValue, removeDecimal, toDecimal, validateAmount, @@ -19,6 +23,9 @@ import { TradingInputError as InputError, TradingSelect as Select, Tooltip, + TradingButton as Button, + Pill, + Intent, } from '@vegaprotocol/ui-toolkit'; import { getDerivedPrice } from '@vegaprotocol/markets'; import type { Market } from '@vegaprotocol/markets'; @@ -31,6 +38,7 @@ import { REDUCE_ONLY_TOOLTIP, stopSubmit, getNotionalSize, + getAssetUnit, } from './deal-ticket'; import { TypeToggle } from './type-selector'; import { @@ -41,9 +49,10 @@ import { } from '../../hooks/use-form-values'; import type { StopOrderFormValues } from '../../hooks/use-form-values'; import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-submission'; -import { DealTicketButton } from './deal-ticket-button'; import { DealTicketFeeDetails } from './deal-ticket-fee-details'; import { validateExpiration } from '../../utils'; +import { NOTIONAL_SIZE_TOOLTIP_TEXT } from '../../constants'; +import { KeyValue } from './key-value'; export interface StopOrderProps { market: Market; @@ -78,7 +87,7 @@ const Trigger = ({ control, watch, priceStep, - assetSymbol, + quoteName, oco, marketPrice, decimalPlaces, @@ -86,7 +95,7 @@ const Trigger = ({ control: Control; watch: UseFormWatch; priceStep: string; - assetSymbol: string; + quoteName: string; oco?: boolean; marketPrice?: string | null; decimalPlaces: number; @@ -181,7 +190,7 @@ const Trigger = ({ data-testid={`triggerPrice${oco ? '-oco' : ''}`} type="number" step={priceStep} - appendElement={assetSymbol} + appendElement={{quoteName}} value={value || ''} hasError={!!fieldState.error} {...props} @@ -249,7 +258,7 @@ const Trigger = ({ %} data-testid={`triggerTrailingPercentOffset${ oco ? '-oco' : '' }`} @@ -311,10 +320,14 @@ const Size = ({ control, sizeStep, oco, + isLimitType, + assetUnit, }: { control: Control; sizeStep: string; oco?: boolean; + isLimitType: boolean; + assetUnit?: string; }) => { return ( +
e.currentTarget.blur()} + appendElement={assetUnit && {assetUnit}} data-testid={id} value={value || ''} hasError={!!fieldState.error} @@ -394,12 +408,8 @@ const Price = ({ const { value, ...props } = field; const id = `order-price${oco ? '-oco' : ''}`; return ( -
- +
+ e.currentTarget.blur()} value={value || ''} hasError={!!fieldState.error} + appendElement={{quoteName}} {...props} /> @@ -434,17 +445,17 @@ const TimeInForce = ({ oco?: boolean; }) => ( { - const id = `select-time-in-force${oco ? '-oco' : ''}`; + const id = `order-tif${oco ? '-oco' : ''}`; return (
{assetUnit}} step={sizeStep} min={sizeStep} data-testid="order-size" @@ -411,7 +425,7 @@ export const DealTicket = ({
)} /> - {type === Schema.OrderType.TYPE_LIMIT && ( + {isLimitType && ( ( -
+
{quoteName}} className="w-full" type="number" step={priceStep} @@ -449,6 +464,22 @@ export const DealTicket = ({ )} /> )} +
+ + +
)} /> - {type === Schema.OrderType.TYPE_LIMIT && + {isLimitType && timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT && (
- {type === Schema.OrderType.TYPE_LIMIT && ( + {isLimitType && ( <>
- - + void; +} + +export const KeyValue = ({ + label, + value, + labelDescription, + symbol, + indent, + onClick, + formattedValue, +}: KeyValuePros) => { + const displayValue = `${formattedValue ?? '-'} ${symbol || ''}`; + const valueElement = onClick ? ( + + ) : ( +
{displayValue}
+ ); + return ( +
+ +
{label}
+
+ + {valueElement} + +
+ ); +}; diff --git a/libs/ui-toolkit/src/components/trading-input/input.tsx b/libs/ui-toolkit/src/components/trading-input/input.tsx index 7bef3d2cc..20345f08a 100644 --- a/libs/ui-toolkit/src/components/trading-input/input.tsx +++ b/libs/ui-toolkit/src/components/trading-input/input.tsx @@ -80,13 +80,14 @@ const getAffixElement = ({ appendIconName, appendIconDescription, }: Pick) => { - const position = prependIconName || prependElement ? 'pre' : 'post'; - const className = classNames( - ['fill-black dark:fill-white', 'absolute', 'z-10'], + 'absolute z-10 top-0 bottom-0 flex items-center', { - 'left-3': position === 'pre', - 'right-3': position === 'post', + 'fill-black dark:fill-white': prependIconName || appendIconName, + 'left-3': prependIconName, + 'right-3': appendIconName, + 'left-1': prependElement, + 'right-1': appendElement, } ); @@ -161,7 +162,7 @@ export const TradingInput = forwardRef( if (element) { return ( -
+
{hasPrepended && element} {input} {hasAppended && element}