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 <maciek@vegaprotocol.io>
This commit is contained in:
macqbat 2022-11-02 16:45:23 +01:00 committed by GitHub
parent 25eac19ac1
commit 0ee6773cb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 388 additions and 160 deletions

View File

@ -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');
});
});

View File

@ -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')

View File

@ -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',
};

View File

@ -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',
});
});

View File

@ -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')}
</span>
),
section: DEAL_TICKET_SECTION.TYPE,
};
}
if (
@ -145,6 +162,7 @@ export const useOrderValidation = ({
{t('Only limit orders are permitted when market is in auction')}
</span>
),
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 = ({
)}
</span>
),
section: DEAL_TICKET_SECTION.FORCE,
};
}
if (
@ -210,6 +230,7 @@ export const useOrderValidation = ({
)}
</span>
),
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: <ValidateMargin {...isInvalidOrderMargin} />,
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 };
};

View File

@ -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<OrderSubmissionBody['orderSubmission']>;
quoteName: string;
price?: string;
errorMessage?: DealTicketErrorMessage;
}
export const DealTicketAmount = ({

View File

@ -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 ? (
<div className="mb-6">
<Button
variant="primary"
fill
type="submit"
disabled={isDisabled}
data-testid="place-order"
>
{transactionStatus === 'pending' ? t('Pending...') : t('Place order')}
</Button>
<DealTicketError
errorMessage={errorMessage}
data-testid="dealticket-error-message"
section={DEAL_TICKET_SECTION.SUMMARY}
/>
</div>
) : (
<Button
variant="default"
fill
type="button"
data-testid="order-connect-wallet"
onClick={openVegaWalletDialog}
className="mb-6"
>
{t('Connect wallet')}
</Button>
);
};

View File

@ -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
) ? (
<div className="-mt-1">
<InputError
intent={errorMessage.isDisabled ? 'danger' : 'warning'}
data-testid={dataTestId}
>
{errorMessage.message}
</InputError>
</div>
) : null;

View File

@ -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 (
<div className="flex items-center gap-4">
<div className="flex-1">
<FormGroup label={t('Size')} labelFor="input-order-size-limit">
<Input
id="input-order-size-limit"
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
data-testid="order-size"
onWheel={(e) => e.currentTarget.blur()}
{...register('size', {
required: true,
min: sizeStep,
validate: validateSize(sizeStep),
})}
/>
</FormGroup>
</div>
<div>@</div>
<div className="flex-1">
<FormGroup
labelFor="input-price-quote"
label={t(`Price (${quoteName})`)}
labelAlign="right"
>
<Input
id="input-price-quote"
className="w-full"
type="number"
step={priceStep}
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
{...register('price', {
required: true,
min: 0,
})}
/>
</FormGroup>
<div className="mb-6">
<div className="flex items-center gap-4">
<div className="flex-1">
<FormGroup
label={t('Size')}
labelFor="input-order-size-limit"
className="!mb-1"
>
<Input
id="input-order-size-limit"
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
data-testid="order-size"
onWheel={(e) => e.currentTarget.blur()}
{...register('size', {
required: true,
min: sizeStep,
validate: validateSize(sizeStep),
})}
/>
</FormGroup>
</div>
<div className="flex-0 items-center">
<div className="flex">&nbsp;</div>
<div className="flex">@</div>
</div>
<div className="flex-1">
<FormGroup
labelFor="input-price-quote"
label={t(`Price (${quoteName})`)}
labelAlign="right"
className="!mb-1"
>
<Input
id="input-price-quote"
className="w-full"
type="number"
step={priceStep}
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
{...register('price', {
required: true,
min: 0,
})}
/>
</FormGroup>
</div>
</div>
<DealTicketError
errorMessage={errorMessage}
data-testid="dealticket-error-message-price-limit"
section={[DEAL_TICKET_SECTION.SIZE, DEAL_TICKET_SECTION.PRICE]}
/>
</div>
);
};

View File

@ -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 (
<div className="flex items-center gap-4 relative">
<div className="flex-1">
<FormGroup label={t('Size')} labelFor="input-order-size-market">
<Input
id="input-order-size-market"
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
data-testid="order-size"
{...register('size', {
required: true,
min: sizeStep,
validate: validateSize(sizeStep),
})}
/>
</FormGroup>
</div>
<div>@</div>
<div className="flex-1" data-testid="last-price">
{isMarketInAuction(market) && (
<Tooltip
description={t(
'This market is in auction. The uncrossing price is an indication of what the price is expected to be when the auction ends.'
)}
<div className="mb-6">
<div className="flex items-center gap-4 relative">
<div className="flex-1">
<FormGroup
label={t('Size')}
labelFor="input-order-size-market"
className="!mb-1"
>
<div className="absolute top-0 right-0 text-sm">
{t(`Estimated uncrossing price`)}
</div>
</Tooltip>
)}
<div className="text-sm text-right">
{price && quoteName ? (
<>
~{formatNumber(price, market.decimalPlaces)} {quoteName}
</>
<Input
id="input-order-size-market"
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
data-testid="order-size"
{...register('size', {
required: true,
min: sizeStep,
validate: validateSize(sizeStep),
})}
/>
</FormGroup>
</div>
<div className="flex-0 items-center">
<div className="flex">&nbsp;</div>
<div className="flex">@</div>
</div>
<div className="flex-1" data-testid="last-price">
{isMarketInAuction(market) ? (
<Tooltip
description={t(
'This market is in auction. The uncrossing price is an indication of what the price is expected to be when the auction ends.'
)}
>
<div className="absolute top-0 right-0 text-sm">
{t(`Estimated uncrossing price`)}
</div>
</Tooltip>
) : (
'-'
<div>&nbsp;</div>
)}
<div className="text-sm text-right">
{price && quoteName ? (
<>
~{formatNumber(price, market.decimalPlaces)} {quoteName}
</>
) : (
'-'
)}
</div>
</div>
</div>
<DealTicketError
errorMessage={errorMessage}
data-testid="dealticket-error-message-price-market"
section={[DEAL_TICKET_SECTION.SIZE, DEAL_TICKET_SECTION.PRICE]}
/>
</div>
);
};

View File

@ -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<OrderSubmissionBody['orderSubmission']>({
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 }) => (
<TypeSelector value={field.value} onSelect={field.onChange} />
<TypeSelector
value={field.value}
onSelect={field.onChange}
errorMessage={errorMessage}
/>
)}
/>
<Controller
@ -129,6 +154,7 @@ export const DealTicket = ({
register={register}
price={order.price}
quoteName={market.tradableInstrument.instrument.product.quoteName}
errorMessage={errorMessage}
/>
<Controller
name="timeInForce"
@ -138,6 +164,7 @@ export const DealTicket = ({
value={field.value}
orderType={order.type}
onSelect={field.onChange}
errorMessage={errorMessage}
/>
)}
/>
@ -147,43 +174,19 @@ export const DealTicket = ({
name="expiresAt"
control={control}
render={({ field }) => (
<ExpirySelector value={field.value} onSelect={field.onChange} />
<ExpirySelector
value={field.value}
onSelect={field.onChange}
errorMessage={errorMessage}
/>
)}
/>
)}
{pubKey ? (
<>
<Button
variant="primary"
fill={true}
type="submit"
disabled={isDisabled}
data-testid="place-order"
>
{transactionStatus === 'pending'
? t('Pending...')
: t('Place order')}
</Button>
{message && (
<InputError
intent={isDisabled ? 'danger' : 'warning'}
data-testid="dealticket-error-message"
>
{message}
</InputError>
)}
</>
) : (
<Button
variant="default"
fill
type="button"
data-testid="order-connect-wallet"
onClick={openVegaWalletDialog}
>
{t('Connect wallet')}
</Button>
)}
<DealTicketButton
transactionStatus={transactionStatus}
isDisabled={isSubmitted && isDisabled}
errorMessage={errorMessage}
/>
<DealTicketFeeDetails details={details} />
</form>
);

View File

@ -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}
/>
<DealTicketError
errorMessage={errorMessage}
data-testid="dealticket-error-message-force"
section={DEAL_TICKET_SECTION.EXPIRY}
/>
</FormGroup>
);
};

View File

@ -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<PrevSelectedValue>({
[Schema.OrderType.TYPE_LIMIT]: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
@ -59,6 +64,11 @@ export const TimeInForceSelector = ({
</option>
))}
</Select>
<DealTicketError
errorMessage={errorMessage}
data-testid="dealticket-error-message-force"
section={DEAL_TICKET_SECTION.FORCE}
/>
</FormGroup>
);
};

View File

@ -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 (
<FormGroup label={t('Order type')} labelFor="order-type">
<Toggle
@ -23,6 +31,11 @@ export const TypeSelector = ({ value, onSelect }: TypeSelectorProps) => {
checkedValue={value}
onChange={(e) => onSelect(e.target.value as Schema.OrderType)}
/>
<DealTicketError
errorMessage={errorMessage}
data-testid="dealticket-error-message-type"
section={DEAL_TICKET_SECTION.TYPE}
/>
</FormGroup>
);
};