From a9fc09cff426732507010f9608d7e04bfaaeeef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Mon, 28 Aug 2023 16:53:16 +0200 Subject: [PATCH] feat(deal-ticket): update deal ticket submit buttons --- .../deal-ticket/deal-ticket-button.tsx | 25 ---- .../deal-ticket/deal-ticket-fee-details.tsx | 132 +++++------------- .../deal-ticket-stop-order.spec.tsx | 26 ++++ .../deal-ticket/deal-ticket-stop-order.tsx | 88 +++++++++--- .../components/deal-ticket/deal-ticket.tsx | 47 +++++-- .../src/components/deal-ticket/key-value.tsx | 51 +++++++ 6 files changed, 217 insertions(+), 152 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/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} - { }); }); + 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, 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..0470ace5a 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 @@ -3,6 +3,7 @@ import { useVegaWallet } from '@vegaprotocol/wallet'; import type { StopOrdersSubmission } from '@vegaprotocol/wallet'; import { formatForInput, + formatValue, removeDecimal, toDecimal, validateAmount, @@ -19,6 +20,9 @@ import { TradingInputError as InputError, TradingSelect as Select, Tooltip, + TradingButton, + Intent, + Pill, } from '@vegaprotocol/ui-toolkit'; import { getDerivedPrice } from '@vegaprotocol/markets'; import type { Market } from '@vegaprotocol/markets'; @@ -41,9 +45,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 +83,7 @@ const Trigger = ({ control, watch, priceStep, - assetSymbol, + quoteName, oco, marketPrice, decimalPlaces, @@ -86,7 +91,7 @@ const Trigger = ({ control: Control; watch: UseFormWatch; priceStep: string; - assetSymbol: string; + quoteName: string; oco?: boolean; marketPrice?: string | null; decimalPlaces: number; @@ -181,7 +186,7 @@ const Trigger = ({ data-testid={`triggerPrice${oco ? '-oco' : ''}`} type="number" step={priceStep} - appendElement={assetSymbol} + appendElement={{quoteName}} value={value || ''} hasError={!!fieldState.error} {...props} @@ -249,7 +254,7 @@ const Trigger = ({ %} data-testid={`triggerTrailingPercentOffset${ oco ? '-oco' : '' }`} @@ -311,10 +316,12 @@ const Size = ({ control, sizeStep, oco, + isLimitType, }: { control: Control; sizeStep: string; oco?: boolean; + isLimitType: boolean; }) => { return ( +
- +
+ e.currentTarget.blur()} value={value || ''} hasError={!!fieldState.error} + appendElement={{quoteName}} {...props} /> @@ -530,6 +534,9 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => { const rawSize = watch('size'); const oco = watch('oco'); const expiresAt = watch('expiresAt'); + const ocoPrice = watch('ocoPrice'); + const ocoSize = watch('ocoSize'); + const ocoType = watch('ocoType'); useEffect(() => { const size = storedFormValues?.[dealTicketType]?.size; @@ -566,6 +573,13 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => { market.positionDecimalPlaces ); + const notionalSizeOco = getNotionalSize( + ocoPrice, + ocoSize, + market.decimalPlaces, + market.positionDecimalPlaces + ); + useEffect(() => { const subscription = watch((value, { name, type }) => { updateStoredFormValues(market.id, value); @@ -620,18 +634,31 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => { control={control} watch={watch} priceStep={priceStep} - assetSymbol={asset.symbol} + quoteName={quoteName} marketPrice={marketPrice} decimalPlaces={market.decimalPlaces} />
+ - +
+ +
@@ -699,12 +726,18 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => { control={control} watch={watch} priceStep={priceStep} - assetSymbol={asset.symbol} + quoteName={quoteName} marketPrice={marketPrice} decimalPlaces={market.decimalPlaces} oco />
+ { quoteName={quoteName} oco /> - +
+ +
@@ -803,7 +847,16 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => { )} - + + {t('Place order')} + { timeInForce, type, }} - notionalSize={notionalSize} assetSymbol={asset.symbol} market={market} /> 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 0319ee8de..eda8c8e42 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx @@ -3,7 +3,6 @@ import * as Schema from '@vegaprotocol/types'; import type { FormEventHandler } from 'react'; import { memo, useCallback, useEffect, useRef, useMemo } from 'react'; import { Controller, useController, useForm } from 'react-hook-form'; -import { DealTicketButton } from './deal-ticket-button'; import { DealTicketFeeDetails, DealTicketMarginDetails, @@ -23,6 +22,8 @@ import { Intent, Notification, Tooltip, + TradingButton, + Pill, } from '@vegaprotocol/ui-toolkit'; import { @@ -35,6 +36,7 @@ import { validateAmount, toDecimal, formatForInput, + formatValue, } from '@vegaprotocol/utils'; import { activeOrdersProvider } from '@vegaprotocol/orders'; import { getDerivedPrice } from '@vegaprotocol/markets'; @@ -46,7 +48,10 @@ import { validateType, } from '../../utils'; import { ZeroBalanceError } from '../deal-ticket-validation/zero-balance-error'; -import { SummaryValidationType } from '../../constants'; +import { + NOTIONAL_SIZE_TOOLTIP_TEXT, + SummaryValidationType, +} from '../../constants'; import type { Market, MarketData, @@ -68,6 +73,7 @@ import { useDealTicketFormValues } from '../../hooks/use-form-values'; import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg'; import noop from 'lodash/noop'; import { isNonPersistentOrder } from '../../utils/time-in-force-persistance'; +import { KeyValue } from './key-value'; export const REDUCE_ONLY_TOOLTIP = '"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.'; @@ -338,6 +344,7 @@ export const DealTicket = ({ const priceStep = toDecimal(market?.decimalPlaces); const sizeStep = toDecimal(market?.positionDecimalPlaces); const quoteName = market.tradableInstrument.instrument.product.quoteName; + const isLimitType = type === Schema.OrderType.TYPE_LIMIT; return (
( -
+
)} /> - {type === Schema.OrderType.TYPE_LIMIT && ( + {isLimitType && ( ( -
+
{quoteName}} className="w-full" type="number" step={priceStep} @@ -449,6 +457,15 @@ export const DealTicket = ({ )} /> )} +
+ +
)} /> - {type === Schema.OrderType.TYPE_LIMIT && + {isLimitType && timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT && (
- {type === Schema.OrderType.TYPE_LIMIT && ( + {isLimitType && ( <>
- + + {t('Place order')} + diff --git a/libs/deal-ticket/src/components/deal-ticket/key-value.tsx b/libs/deal-ticket/src/components/deal-ticket/key-value.tsx new file mode 100644 index 000000000..42f6a5e6f --- /dev/null +++ b/libs/deal-ticket/src/components/deal-ticket/key-value.tsx @@ -0,0 +1,51 @@ +import { Tooltip } from '@vegaprotocol/ui-toolkit'; +import classnames from 'classnames'; +import type { ReactNode } from 'react'; + +export interface KeyValuePros { + label: string; + value?: string | null | undefined; + symbol: string; + indent?: boolean | undefined; + labelDescription?: ReactNode; + formattedValue?: string; + onClick?: () => void; +} + +export const KeyValue = ({ + label, + value, + labelDescription, + symbol, + indent, + onClick, + formattedValue, +}: KeyValuePros) => { + const displayValue = `${formattedValue ?? '-'} ${symbol || ''}`; + const valueElement = onClick ? ( + + ) : ( +
{displayValue}
+ ); + return ( +
+ +
{label}
+
+ + {valueElement} + +
+ ); +};