From 0ee6773cb6fc33b45c66197d6ee7c1d69bd9115d Mon Sep 17 00:00:00 2001 From: macqbat Date: Wed, 2 Nov 2022 16:45:23 +0100 Subject: [PATCH] Feat/1657 show deal ticket errors contextually (#1900) * feat: deal ticket contextual validation * feat: deal ticket contextual validation * feat: show deal ticket errors contextually * feat: show deal ticket errors contextually * feat: show deal ticket errors contextually * feat: show deal ticket errors contextually - adjust int tests * feat: show deal ticket errors contextually - adjust size and price sections * feat: show deal ticket errors contextually - fix lin failings * feat: show deal ticket errors contextually - use set timeout for create a transition effect * feat: show deal ticket errors contextually - removing animations * feat: show deal ticket errors contextually - reove unnecessary cast of section prop * feat: show deal ticket errors contextually - reove unnecessary cast of section prop * feat: show deal ticket errors contextually - add clickable order button, refactor error passing * feat: show deal ticket errors contextually - fix market-info int tests * feat: show deal ticket errors contextually - fix market-trade int tests * feat: show deal ticket errors contextually - add back price after reset * feat: show deal ticket errors contextually - remove reset after sent Co-authored-by: maciek --- .../src/integration/market-info.cy.ts | 3 +- .../src/integration/trading-deal-ticket.cy.ts | 24 ++-- libs/deal-ticket/src/components/constants.ts | 9 ++ .../use-order-validation.spec.tsx | 30 +++-- .../use-order-validation.tsx | 46 ++++++- .../deal-ticket/deal-ticket-amount.tsx | 2 + .../deal-ticket/deal-ticket-button.tsx | 52 ++++++++ .../deal-ticket/deal-ticket-error.tsx | 34 +++++ .../deal-ticket/deal-ticket-limit-amount.tsx | 96 ++++++++------ .../deal-ticket/deal-ticket-market-amount.tsx | 93 ++++++++------ .../components/deal-ticket/deal-ticket.tsx | 119 +++++++++--------- .../deal-ticket/expiry-selector.tsx | 15 ++- .../deal-ticket/time-in-force-selector.tsx | 10 ++ .../components/deal-ticket/type-selector.tsx | 15 ++- 14 files changed, 388 insertions(+), 160 deletions(-) create mode 100644 libs/deal-ticket/src/components/deal-ticket/deal-ticket-button.tsx create mode 100644 libs/deal-ticket/src/components/deal-ticket/deal-ticket-error.tsx diff --git a/apps/trading-e2e/src/integration/market-info.cy.ts b/apps/trading-e2e/src/integration/market-info.cy.ts index 59a66fd28..19d061fe1 100644 --- a/apps/trading-e2e/src/integration/market-info.cy.ts +++ b/apps/trading-e2e/src/integration/market-info.cy.ts @@ -227,6 +227,7 @@ describe('market states', { tags: '@smoke' }, function () { it.skip('must display correct market state'); //7002-/SORD-/061 no state displayed it('must display that market is not accepting orders', function () { + cy.getByTestId('place-order').click(); cy.getByTestId('dealticket-error-message').should( 'have.text', `This market is ${marketState @@ -234,8 +235,6 @@ describe('market states', { tags: '@smoke' }, function () { .pop() ?.toLowerCase()} and not accepting orders` ); - }); - it('must have place order button disabled', function () { cy.getByTestId('place-order').should('be.disabled'); }); }); 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 48a09de48..8ccf3be4e 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts @@ -251,7 +251,7 @@ describe('deal ticket validation', { tags: '@smoke' }, () => { }); describe('deal ticket size validation', { tags: '@smoke' }, function () { - before(() => { + beforeEach(() => { cy.mockTradingPage(); cy.visit('/markets/market-0'); cy.wait('@Market'); @@ -260,8 +260,10 @@ describe('deal ticket size validation', { tags: '@smoke' }, function () { it('must warn if order size input has too many digits after the decimal place', function () { //7002-SORD-016 cy.getByTestId(orderSizeField).clear().type('1.234'); + cy.getByTestId(placeOrderBtn).should('not.be.disabled'); + cy.getByTestId(placeOrderBtn).click(); cy.getByTestId(placeOrderBtn).should('be.disabled'); - cy.getByTestId(errorMessage).should( + cy.getByTestId('dealticket-error-message-price-market').should( 'have.text', 'Order sizes must be in whole numbers for this market' ); @@ -269,8 +271,10 @@ describe('deal ticket size validation', { tags: '@smoke' }, function () { it('must warn if order size is set to 0', function () { cy.getByTestId(orderSizeField).clear().type('0'); + cy.getByTestId(placeOrderBtn).should('not.be.disabled'); + cy.getByTestId(placeOrderBtn).click(); cy.getByTestId(placeOrderBtn).should('be.disabled'); - cy.getByTestId(errorMessage).should( + cy.getByTestId('dealticket-error-message-price-market').should( 'have.text', 'Size cannot be lower than "1"' ); @@ -416,8 +420,10 @@ describe('suspended market validation', { tags: '@regression' }, () => { it('should show warning for market order', function () { cy.getByTestId(toggleMarket).click(); + cy.getByTestId(placeOrderBtn).should('not.be.disabled'); + cy.getByTestId(placeOrderBtn).click(); cy.getByTestId(placeOrderBtn).should('be.disabled'); - cy.getByTestId(errorMessage).should( + cy.getByTestId('dealticket-error-message-type').should( 'have.text', 'This market is in auction until it reaches sufficient liquidity. Only limit orders are permitted when market is in auction' ); @@ -437,7 +443,7 @@ describe('suspended market validation', { tags: '@regression' }, () => { TIFlist.filter((item) => item.code === 'FOK')[0].value ); cy.getByTestId(placeOrderBtn).should('be.disabled'); - cy.getByTestId(errorMessage).should( + cy.getByTestId('dealticket-error-message-force').should( 'have.text', 'This market is in auction until it reaches sufficient liquidity. Until the auction ends, you can only place GFA, GTT, or GTC limit orders' ); @@ -464,14 +470,16 @@ describe('margin required validation', { tags: '@regression' }, () => { }); it('should display info and button for deposit', () => { + cy.getByTestId('place-order').should('not.be.disabled'); + cy.getByTestId('place-order').click(); cy.getByTestId('place-order').should('be.disabled'); - cy.getByTestId('dealticket-error-message').should( + cy.getByTestId('deal-ticket-margin-invalidated').should( 'contain.text', "You don't have enough margin available to open this position" ); - cy.getByTestId('dealticket-error-message').should( + cy.getByTestId('deal-ticket-margin-invalidated').should( 'contain.text', - '0.01000 tBTC currently required, 0.00100 tBTC available' + '0.01 tBTC currently required, 0.001 tBTC available' ); cy.getByTestId('deal-ticket-deposit-dialog-button').click(); cy.getByTestId('dialog-content') diff --git a/libs/deal-ticket/src/components/constants.ts b/libs/deal-ticket/src/components/constants.ts index ee36ac580..b8c812d44 100644 --- a/libs/deal-ticket/src/components/constants.ts +++ b/libs/deal-ticket/src/components/constants.ts @@ -19,3 +19,12 @@ export const EST_FEES_TOOLTIP_TEXT = t( export const EST_SLIPPAGE = t( 'When you execute a trade on Vega, the price obtained in the market may differ from the best available price displayed at the time of placing the trade. The estimated slippage shows the difference between the best available price and the estimated execution price, determined by market liquidity and your chosen order size.' ); + +export const DEAL_TICKET_SECTION = { + TYPE: 'sec-type', + SIZE: 'sec-size', + PRICE: 'sec-price', + FORCE: 'sec-force', + EXPIRY: 'sec-expiry', + SUMMARY: 'sec-summary', +}; diff --git a/libs/deal-ticket/src/components/deal-ticket-validation/use-order-validation.spec.tsx b/libs/deal-ticket/src/components/deal-ticket-validation/use-order-validation.spec.tsx index ae0d1236f..755f9fb18 100644 --- a/libs/deal-ticket/src/components/deal-ticket-validation/use-order-validation.spec.tsx +++ b/libs/deal-ticket/src/components/deal-ticket-validation/use-order-validation.spec.tsx @@ -127,7 +127,11 @@ describe('useOrderValidation', () => { .mockReturnValue(false); const { result } = setup(); - expect(result.current).toStrictEqual({ isDisabled: false, message: `` }); + expect(result.current).toStrictEqual({ + isDisabled: false, + message: ``, + section: '', + }); }); it('Returns an error message when no keypair found', () => { @@ -135,7 +139,11 @@ describe('useOrderValidation', () => { .spyOn(OrderMarginValidation, 'useOrderMarginValidation') .mockReturnValue(false); const { result } = setup(defaultOrder, { pubKey: null }); - expect(result.current).toStrictEqual({ isDisabled: false, message: `` }); + expect(result.current).toStrictEqual({ + isDisabled: false, + message: ``, + section: '', + }); }); it.each` @@ -154,6 +162,7 @@ describe('useOrderValidation', () => { message: `This market is ${marketTranslations( state )} and not accepting orders`, + section: 'sec-summary', }); } ); @@ -177,6 +186,7 @@ describe('useOrderValidation', () => { message: `This market is ${MarketStateMapping[ state as MarketState ].toLowerCase()} and only accepting liquidity commitment orders`, + section: 'sec-summary', }); } ); @@ -220,19 +230,20 @@ describe('useOrderValidation', () => { expect(result.current).toStrictEqual({ isDisabled: true, message: errorMessage, + section: 'sec-force', }); } ); it.each` - fieldName | errorType | errorMessage - ${`size`} | ${`required`} | ${ERROR.FIELD_SIZE_REQ} - ${`size`} | ${`min`} | ${ERROR.FIELD_SIZE_MIN} - ${`price`} | ${`required`} | ${ERROR.FIELD_PRICE_REQ} - ${`price`} | ${`min`} | ${ERROR.FIELD_PRICE_MIN} + fieldName | errorType | section | errorMessage + ${`size`} | ${`required`} | ${'sec-size'} | ${ERROR.FIELD_SIZE_REQ} + ${`size`} | ${`min`} | ${'sec-size'} | ${ERROR.FIELD_SIZE_MIN} + ${`price`} | ${`required`} | ${'sec-price'} | ${ERROR.FIELD_PRICE_REQ} + ${`price`} | ${`min`} | ${'sec-price'} | ${ERROR.FIELD_PRICE_MIN} `( `Returns an error message when the order $fieldName "$errorType" validation fails`, - ({ fieldName, errorType, errorMessage }) => { + ({ fieldName, errorType, section, errorMessage }) => { const { result } = setup({ fieldErrors: { [fieldName]: { type: errorType } }, orderType: Schema.OrderType.TYPE_LIMIT, @@ -240,6 +251,7 @@ describe('useOrderValidation', () => { expect(result.current).toStrictEqual({ isDisabled: true, message: errorMessage, + section, }); } ); @@ -252,6 +264,7 @@ describe('useOrderValidation', () => { expect(result.current).toStrictEqual({ isDisabled: true, message: ERROR.FIELD_PRICE_STEP_NULL, + section: 'sec-size', }); }); @@ -262,6 +275,7 @@ describe('useOrderValidation', () => { expect(result.current).toStrictEqual({ isDisabled: true, message: ERROR.FIELD_PRICE_STEP_DECIMAL, + section: 'sec-size', }); }); diff --git a/libs/deal-ticket/src/components/deal-ticket-validation/use-order-validation.tsx b/libs/deal-ticket/src/components/deal-ticket-validation/use-order-validation.tsx index 5fa59fa59..41e42bba1 100644 --- a/libs/deal-ticket/src/components/deal-ticket-validation/use-order-validation.tsx +++ b/libs/deal-ticket/src/components/deal-ticket-validation/use-order-validation.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react'; import type { FieldErrors } from 'react-hook-form'; import { useMemo } from 'react'; import { t, toDecimal } from '@vegaprotocol/react-helpers'; @@ -18,6 +19,7 @@ import type { DealTicketMarketFragment } from '../deal-ticket/__generated___/Dea import { ValidateMargin } from './validate-margin'; import type { OrderMargin } from '../../hooks/use-order-margin'; import { useOrderMarginValidation } from './use-order-margin-validation'; +import { DEAL_TICKET_SECTION } from '../constants'; export const isMarketInAuction = (market: DealTicketMarketFragment) => { return [ @@ -45,6 +47,10 @@ export const marketTranslations = (marketState: MarketState) => { } }; +export type DealTicketSection = + | '' + | typeof DEAL_TICKET_SECTION[keyof typeof DEAL_TICKET_SECTION]; + export const useOrderValidation = ({ market, fieldErrors = {}, @@ -52,17 +58,25 @@ export const useOrderValidation = ({ orderTimeInForce, estMargin, }: ValidationProps): { - message: React.ReactNode | string; + message: ReactNode | string; isDisabled: boolean; + section: DealTicketSection; } => { const { pubKey } = useVegaWallet(); const minSize = toDecimal(market.positionDecimalPlaces); - const isInvalidOrderMargin = useOrderMarginValidation({ market, estMargin }); - const { message, isDisabled } = useMemo(() => { + const { message, isDisabled, section } = useMemo<{ + message: ReactNode | string; + isDisabled: boolean; + section: DealTicketSection; + }>(() => { if (!pubKey) { - return { message: t('No public key selected'), isDisabled: true }; + return { + message: t('No public key selected'), + isDisabled: true, + section: DEAL_TICKET_SECTION.SUMMARY, + }; } if ( @@ -81,6 +95,7 @@ export const useOrderValidation = ({ market.state )} and not accepting orders` ), + section: DEAL_TICKET_SECTION.SUMMARY, }; } @@ -96,6 +111,7 @@ export const useOrderValidation = ({ market.state )} and only accepting liquidity commitment orders` ), + section: DEAL_TICKET_SECTION.SUMMARY, }; } @@ -122,6 +138,7 @@ export const useOrderValidation = ({ {t('Only limit orders are permitted when market is in auction')} ), + section: DEAL_TICKET_SECTION.TYPE, }; } if ( @@ -145,6 +162,7 @@ export const useOrderValidation = ({ {t('Only limit orders are permitted when market is in auction')} ), + section: DEAL_TICKET_SECTION.TYPE, }; } return { @@ -152,6 +170,7 @@ export const useOrderValidation = ({ message: t( 'Only limit orders are permitted when market is in auction' ), + section: DEAL_TICKET_SECTION.SUMMARY, }; } if ( @@ -185,6 +204,7 @@ export const useOrderValidation = ({ )} ), + section: DEAL_TICKET_SECTION.FORCE, }; } if ( @@ -210,6 +230,7 @@ export const useOrderValidation = ({ )} ), + section: DEAL_TICKET_SECTION.FORCE, }; } return { @@ -217,6 +238,7 @@ export const useOrderValidation = ({ message: t( `Until the auction ends, you can only place GFA, GTT, or GTC limit orders` ), + section: DEAL_TICKET_SECTION.FORCE, }; } } @@ -225,6 +247,7 @@ export const useOrderValidation = ({ return { isDisabled: true, message: t('You need to provide a size'), + section: DEAL_TICKET_SECTION.SIZE, }; } @@ -232,6 +255,7 @@ export const useOrderValidation = ({ return { isDisabled: true, message: t(`Size cannot be lower than "${minSize}"`), + section: DEAL_TICKET_SECTION.SIZE, }; } @@ -242,6 +266,7 @@ export const useOrderValidation = ({ return { isDisabled: true, message: t('You need to provide a price'), + section: DEAL_TICKET_SECTION.PRICE, }; } @@ -252,6 +277,7 @@ export const useOrderValidation = ({ return { isDisabled: true, message: t(`The price cannot be negative`), + section: DEAL_TICKET_SECTION.PRICE, }; } @@ -263,6 +289,7 @@ export const useOrderValidation = ({ return { isDisabled: true, message: t('Order sizes must be in whole numbers for this market'), + section: DEAL_TICKET_SECTION.SIZE, }; } return { @@ -270,6 +297,7 @@ export const useOrderValidation = ({ message: t( `The size field accepts up to ${market.positionDecimalPlaces} decimal places` ), + section: DEAL_TICKET_SECTION.SIZE, }; } @@ -277,6 +305,7 @@ export const useOrderValidation = ({ return { isDisabled: true, message: , + section: DEAL_TICKET_SECTION.PRICE, }; } @@ -292,10 +321,15 @@ export const useOrderValidation = ({ message: t( 'Any orders placed now will not trade until the auction ends' ), + section: DEAL_TICKET_SECTION.SUMMARY, }; } - return { isDisabled: false, message: '' }; + return { + isDisabled: false, + message: '', + section: '', + }; }, [ minSize, pubKey, @@ -308,5 +342,5 @@ export const useOrderValidation = ({ isInvalidOrderMargin, ]); - return { message, isDisabled }; + return { message, isDisabled, section }; }; diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-amount.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-amount.tsx index e57e1715f..a2e40beb4 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-amount.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-amount.tsx @@ -4,6 +4,7 @@ import { DealTicketMarketAmount } from './deal-ticket-market-amount'; import { DealTicketLimitAmount } from './deal-ticket-limit-amount'; import type { DealTicketMarketFragment } from './__generated___/DealTicket'; import { Schema } from '@vegaprotocol/types'; +import type { DealTicketErrorMessage } from './deal-ticket-error'; export interface DealTicketAmountProps { orderType: Schema.OrderType; @@ -11,6 +12,7 @@ export interface DealTicketAmountProps { register: UseFormRegister; quoteName: string; price?: string; + errorMessage?: DealTicketErrorMessage; } export const DealTicketAmount = ({ 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 new file mode 100644 index 000000000..a602561fc --- /dev/null +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-button.tsx @@ -0,0 +1,52 @@ +import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; +import { Button } from '@vegaprotocol/ui-toolkit'; +import { t } from '@vegaprotocol/react-helpers'; +import type { DealTicketErrorMessage } from './deal-ticket-error'; +import { DealTicketError } from './deal-ticket-error'; +import { DEAL_TICKET_SECTION } from '../constants'; + +interface Props { + transactionStatus: 'default' | 'pending'; + isDisabled: boolean; + errorMessage?: DealTicketErrorMessage; +} + +export const DealTicketButton = ({ + transactionStatus, + errorMessage, + isDisabled, +}: Props) => { + const { pubKey } = useVegaWallet(); + const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({ + openVegaWalletDialog: store.openVegaWalletDialog, + })); + return pubKey ? ( +
+ + +
+ ) : ( + + ); +}; diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-error.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-error.tsx new file mode 100644 index 000000000..209d5da78 --- /dev/null +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-error.tsx @@ -0,0 +1,34 @@ +import type { ReactNode } from 'react'; +import { InputError } from '@vegaprotocol/ui-toolkit'; +import type { DealTicketSection } from '../deal-ticket-validation'; + +export interface DealTicketErrorMessage { + message: ReactNode | string; + isDisabled: boolean; + errorSection: DealTicketSection; +} + +interface Props { + errorMessage?: DealTicketErrorMessage; + 'data-testid'?: string; + section: DealTicketSection | DealTicketSection[]; +} + +export const DealTicketError = ({ + errorMessage, + 'data-testid': dataTestId = 'deal-ticket-error-message', + section, +}: Props) => + errorMessage && + (Array.isArray(section) ? section : [section]).includes( + errorMessage.errorSection + ) ? ( +
+ + {errorMessage.message} + +
+ ) : null; diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-limit-amount.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-limit-amount.tsx index 0e7305f82..40c9072fc 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-limit-amount.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-limit-amount.tsx @@ -2,6 +2,8 @@ import { FormGroup, Input } from '@vegaprotocol/ui-toolkit'; import { t, toDecimal } from '@vegaprotocol/react-helpers'; import type { DealTicketAmountProps } from './deal-ticket-amount'; import { validateSize } from '../deal-ticket-validation'; +import { DealTicketError } from './deal-ticket-error'; +import { DEAL_TICKET_SECTION } from '../constants'; export type DealTicketLimitAmountProps = Omit< DealTicketAmountProps, @@ -12,51 +14,67 @@ export const DealTicketLimitAmount = ({ register, market, quoteName, + errorMessage, }: DealTicketLimitAmountProps) => { const priceStep = toDecimal(market?.decimalPlaces); const sizeStep = toDecimal(market?.positionDecimalPlaces); return ( -
-
- - e.currentTarget.blur()} - {...register('size', { - required: true, - min: sizeStep, - validate: validateSize(sizeStep), - })} - /> - -
-
@
-
- - e.currentTarget.blur()} - {...register('price', { - required: true, - min: 0, - })} - /> - +
+
+
+ + e.currentTarget.blur()} + {...register('size', { + required: true, + min: sizeStep, + validate: validateSize(sizeStep), + })} + /> + +
+
+
 
+
@
+
+
+ + e.currentTarget.blur()} + {...register('price', { + required: true, + min: 0, + })} + /> + +
+
); }; diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-market-amount.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-market-amount.tsx index 2d3a58b59..92df3eca7 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-market-amount.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-market-amount.tsx @@ -3,6 +3,8 @@ import { formatNumber, t, toDecimal } from '@vegaprotocol/react-helpers'; import type { DealTicketAmountProps } from './deal-ticket-amount'; import { validateSize } from '../deal-ticket-validation/validate-size'; import { isMarketInAuction } from '../deal-ticket-validation/use-order-validation'; +import { DealTicketError } from './deal-ticket-error'; +import { DEAL_TICKET_SECTION } from '../constants'; export type DealTicketMarketAmountProps = Omit< DealTicketAmountProps, @@ -14,51 +16,68 @@ export const DealTicketMarketAmount = ({ price, market, quoteName, + errorMessage, }: DealTicketMarketAmountProps) => { const sizeStep = toDecimal(market?.positionDecimalPlaces); return ( -
-
- - e.currentTarget.blur()} - data-testid="order-size" - {...register('size', { - required: true, - min: sizeStep, - validate: validateSize(sizeStep), - })} - /> - -
-
@
-
- {isMarketInAuction(market) && ( - +
+
+ -
- {t(`Estimated uncrossing price`)} -
- - )} -
- {price && quoteName ? ( - <> - ~{formatNumber(price, market.decimalPlaces)} {quoteName} - + e.currentTarget.blur()} + data-testid="order-size" + {...register('size', { + required: true, + min: sizeStep, + validate: validateSize(sizeStep), + })} + /> + +
+
+
 
+
@
+
+
+ {isMarketInAuction(market) ? ( + +
+ {t(`Estimated uncrossing price`)} +
+
) : ( - '-' +
 
)} +
+ {price && quoteName ? ( + <> + ~{formatNumber(price, market.decimalPlaces)} {quoteName} + + ) : ( + '-' + )} +
+
); }; 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 af3ebdb41..4c2d80243 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx @@ -1,28 +1,27 @@ -import { addDecimal, removeDecimal, t } from '@vegaprotocol/react-helpers'; +import { useCallback, useEffect, useState } from 'react'; +import { useForm, Controller } from 'react-hook-form'; +import { removeDecimal, addDecimal } from '@vegaprotocol/react-helpers'; +import { TypeSelector } from './type-selector'; +import { SideSelector } from './side-selector'; +import { DealTicketAmount } from './deal-ticket-amount'; +import { TimeInForceSelector } from './time-in-force-selector'; +import type { DealTicketMarketFragment } from './__generated___/DealTicket'; +import { ExpirySelector } from './expiry-selector'; +import type { OrderSubmissionBody } from '@vegaprotocol/wallet'; import { Schema } from '@vegaprotocol/types'; -import { Button, InputError } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; -import { useCallback, useEffect } from 'react'; -import { Controller, useForm } from 'react-hook-form'; - -import { - getFeeDetailsValues, - useFeeDealTicketDetails, -} from '../../hooks/use-fee-deal-ticket-details'; import { getDefaultOrder } from '../deal-ticket-validation'; import { isMarketInAuction, useOrderValidation, } from '../deal-ticket-validation/use-order-validation'; -import { DealTicketAmount } from './deal-ticket-amount'; import { DealTicketFeeDetails } from './deal-ticket-fee-details'; -import { ExpirySelector } from './expiry-selector'; -import { SideSelector } from './side-selector'; -import { TimeInForceSelector } from './time-in-force-selector'; -import { TypeSelector } from './type-selector'; +import { + useFeeDealTicketDetails, + getFeeDetailsValues, +} from '../../hooks/use-fee-deal-ticket-details'; +import { DealTicketButton } from './deal-ticket-button'; +import type { DealTicketErrorMessage } from './deal-ticket-error'; -import type { DealTicketMarketFragment } from './__generated___/DealTicket'; -import type { OrderSubmissionBody } from '@vegaprotocol/wallet'; export type TransactionStatus = 'default' | 'pending'; export interface DealTicketProps { @@ -37,17 +36,18 @@ export const DealTicket = ({ submit, transactionStatus, }: DealTicketProps) => { - const { pubKey } = useVegaWallet(); - const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({ - openVegaWalletDialog: store.openVegaWalletDialog, - })); + const [errorMessage, setErrorMessage] = useState< + DealTicketErrorMessage | undefined + >(undefined); const { register, control, handleSubmit, watch, setValue, - formState: { errors }, + clearErrors, + setError, + formState: { errors, isSubmitted }, } = useForm({ mode: 'onChange', defaultValues: getDefaultOrder(market), @@ -57,13 +57,34 @@ export const DealTicket = ({ const feeDetails = useFeeDealTicketDetails(order, market); const details = getFeeDetailsValues(feeDetails); - const { message, isDisabled: disabled } = useOrderValidation({ + const { + message, + isDisabled: disabled, + section: errorSection, + } = useOrderValidation({ market, orderType: order.type, orderTimeInForce: order.timeInForce, fieldErrors: errors, estMargin: feeDetails.estMargin, }); + + useEffect(() => { + if (disabled) { + setError('marketId', {}); + } else { + clearErrors('marketId'); + } + }, [disabled, setError, clearErrors]); + + useEffect(() => { + if (isSubmitted) { + setErrorMessage({ message, isDisabled: disabled, errorSection }); + } else { + setErrorMessage(undefined); + } + }, [disabled, message, errorSection, isSubmitted]); + const isDisabled = transactionStatus === 'pending' || disabled; const onSubmit = useCallback( @@ -113,7 +134,11 @@ export const DealTicket = ({ name="type" control={control} render={({ field }) => ( - + )} /> )} /> @@ -147,43 +174,19 @@ export const DealTicket = ({ name="expiresAt" control={control} render={({ field }) => ( - + )} /> )} - {pubKey ? ( - <> - - {message && ( - - {message} - - )} - - ) : ( - - )} + ); diff --git a/libs/deal-ticket/src/components/deal-ticket/expiry-selector.tsx b/libs/deal-ticket/src/components/deal-ticket/expiry-selector.tsx index 54dd2b751..8e84aa402 100644 --- a/libs/deal-ticket/src/components/deal-ticket/expiry-selector.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/expiry-selector.tsx @@ -1,13 +1,21 @@ import { FormGroup, Input } from '@vegaprotocol/ui-toolkit'; import { formatForInput } from '@vegaprotocol/react-helpers'; import { t } from '@vegaprotocol/react-helpers'; +import type { DealTicketErrorMessage } from './deal-ticket-error'; +import { DealTicketError } from './deal-ticket-error'; +import { DEAL_TICKET_SECTION } from '../constants'; interface ExpirySelectorProps { value?: string; onSelect: (expiration: string | null) => void; + errorMessage?: DealTicketErrorMessage; } -export const ExpirySelector = ({ value, onSelect }: ExpirySelectorProps) => { +export const ExpirySelector = ({ + value, + onSelect, + errorMessage, +}: ExpirySelectorProps) => { const date = value ? new Date(value) : new Date(); const dateFormatted = formatForInput(date); const minDate = formatForInput(date); @@ -22,6 +30,11 @@ export const ExpirySelector = ({ value, onSelect }: ExpirySelectorProps) => { onChange={(e) => onSelect(e.target.value)} min={minDate} /> + ); }; diff --git a/libs/deal-ticket/src/components/deal-ticket/time-in-force-selector.tsx b/libs/deal-ticket/src/components/deal-ticket/time-in-force-selector.tsx index 02da4fa0b..40cef0ebc 100644 --- a/libs/deal-ticket/src/components/deal-ticket/time-in-force-selector.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/time-in-force-selector.tsx @@ -3,11 +3,15 @@ import { FormGroup, Select } from '@vegaprotocol/ui-toolkit'; import { Schema } from '@vegaprotocol/types'; import { t } from '@vegaprotocol/react-helpers'; import { timeInForceLabel } from '@vegaprotocol/orders'; +import type { DealTicketErrorMessage } from './deal-ticket-error'; +import { DealTicketError } from './deal-ticket-error'; +import { DEAL_TICKET_SECTION } from '../constants'; interface TimeInForceSelectorProps { value: Schema.OrderTimeInForce; orderType: Schema.OrderType; onSelect: (tif: Schema.OrderTimeInForce) => void; + errorMessage?: DealTicketErrorMessage; } type PossibleOrderKeys = Exclude< @@ -22,6 +26,7 @@ export const TimeInForceSelector = ({ value, orderType, onSelect, + errorMessage, }: TimeInForceSelectorProps) => { const [prevValue, setPrevValue] = useState({ [Schema.OrderType.TYPE_LIMIT]: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC, @@ -59,6 +64,11 @@ export const TimeInForceSelector = ({ ))} + ); }; diff --git a/libs/deal-ticket/src/components/deal-ticket/type-selector.tsx b/libs/deal-ticket/src/components/deal-ticket/type-selector.tsx index 1e4fe8ce2..55cd67da7 100644 --- a/libs/deal-ticket/src/components/deal-ticket/type-selector.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/type-selector.tsx @@ -2,10 +2,14 @@ import { FormGroup } from '@vegaprotocol/ui-toolkit'; import { t } from '@vegaprotocol/react-helpers'; import { Schema } from '@vegaprotocol/types'; import { Toggle } from '@vegaprotocol/ui-toolkit'; +import type { DealTicketErrorMessage } from './deal-ticket-error'; +import { DealTicketError } from './deal-ticket-error'; +import { DEAL_TICKET_SECTION } from '../constants'; interface TypeSelectorProps { value: Schema.OrderType; onSelect: (type: Schema.OrderType) => void; + errorMessage?: DealTicketErrorMessage; } const toggles = [ @@ -13,7 +17,11 @@ const toggles = [ { label: t('Limit'), value: Schema.OrderType.TYPE_LIMIT }, ]; -export const TypeSelector = ({ value, onSelect }: TypeSelectorProps) => { +export const TypeSelector = ({ + value, + onSelect, + errorMessage, +}: TypeSelectorProps) => { return ( { checkedValue={value} onChange={(e) => onSelect(e.target.value as Schema.OrderType)} /> + ); };