feat(trading): deposit flow deal ticket (#2874)

This commit is contained in:
m.ray 2023-02-14 10:43:52 -05:00 committed by GitHub
parent 7017e24adf
commit 55d6dd4dce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 292 additions and 223 deletions

View File

@ -17,6 +17,7 @@ const itemValue = 'item-value';
describe('Market proposal notification', { tags: '@smoke' }, () => { describe('Market proposal notification', { tags: '@smoke' }, () => {
before(() => { before(() => {
cy.setVegaWallet();
cy.mockTradingPage( cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE, Schema.MarketState.STATE_ACTIVE,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION, Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
@ -239,7 +240,6 @@ describe('market states not accepting orders', { tags: '@smoke' }, function () {
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
}); });
it('must display that market is not accepting orders', function () { it('must display that market is not accepting orders', function () {
cy.getByTestId('place-order').click();
cy.getByTestId('dealticket-error-message-summary').should( cy.getByTestId('dealticket-error-message-summary').should(
'have.text', 'have.text',
`This market is ${marketState `This market is ${marketState

View File

@ -13,7 +13,6 @@ const toggleShort = 'order-side-SIDE_SELL';
const toggleLong = 'order-side-SIDE_BUY'; const toggleLong = 'order-side-SIDE_BUY';
const toggleLimit = 'order-type-TYPE_LIMIT'; const toggleLimit = 'order-type-TYPE_LIMIT';
const toggleMarket = 'order-type-TYPE_MARKET'; const toggleMarket = 'order-type-TYPE_MARKET';
const errorMessage = 'dealticket-error-message';
const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => { const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => {
return { return {
@ -363,11 +362,11 @@ describe('deal ticket validation', { tags: '@smoke' }, () => {
cy.wait('@Markets'); cy.wait('@Markets');
}); });
it('must not place an order if wallet is not connected', () => { it('must show place order button and connect wallet if wallet is not connected', () => {
cy.getByTestId('connect-vega-wallet'); // Not connected cy.getByTestId('connect-vega-wallet'); // Not connected
cy.getByTestId('order-connect-wallet').should('exist'); cy.getByTestId('order-connect-wallet').should('exist');
cy.getByTestId(placeOrderBtn).should('not.exist'); cy.getByTestId(placeOrderBtn).should('exist');
cy.getByTestId(errorMessage).should('not.exist'); cy.getByTestId('deal-ticket-connect-wallet').should('exist');
}); });
it('must be able to select order direction - long/short', function () { it('must be able to select order direction - long/short', function () {
@ -664,13 +663,13 @@ describe('account validation', { tags: '@regression' }, () => {
}); });
it('should show an error if your balance is zero', () => { it('should show an error if your balance is zero', () => {
cy.getByTestId('place-order').should('not.be.disabled');
cy.getByTestId('place-order').click();
cy.getByTestId('place-order').should('be.disabled'); cy.getByTestId('place-order').should('be.disabled');
//7002-SORD-003 //7002-SORD-003
cy.getByTestId('dealticket-error-message-zero-balance').should( cy.getByTestId('dealticket-error-message-zero-balance').should(
'have.text', 'have.text',
'Insufficient balance. Deposit ' + 'tDAI' 'You need ' +
'tDAI' +
' in your wallet to trade in this market. See all your collateral.Make a deposit'
); );
cy.getByTestId('deal-ticket-deposit-dialog-button').should('exist'); cy.getByTestId('deal-ticket-deposit-dialog-button').should('exist');
}); });
@ -708,7 +707,7 @@ describe('account validation', { tags: '@regression' }, () => {
); );
cy.getByTestId('dealticket-warning-margin').should( cy.getByTestId('dealticket-warning-margin').should(
'contain.text', 'contain.text',
'9,999.99 tDAI currently required, 1,000.00 tDAI available' '9,999.99 tDAI is currently required. You have only 1,000.00 tDAI available.Deposit tDAI'
); );
cy.getByTestId('deal-ticket-deposit-dialog-button').click(); cy.getByTestId('deal-ticket-deposit-dialog-button').click();
cy.getByTestId('dialog-content') cy.getByTestId('dialog-content')

View File

@ -98,8 +98,14 @@ export const MarketPage = () => {
if (w > 960) { if (w > 960) {
return <TradeGrid market={data} onSelect={onSelect} />; return <TradeGrid market={data} onSelect={onSelect} />;
} }
return <TradePanels market={data} onSelect={onSelect} />; return (
}, [w, data, onSelect]); <TradePanels
market={data}
onSelect={onSelect}
onClickCollateral={() => navigate('/portfolio')}
/>
);
}, [w, data, onSelect, navigate]);
if (!data && marketId) { if (!data && marketId) {
return ( return (
<Splash> <Splash>

View File

@ -111,7 +111,10 @@ const MainGrid = ({
<TradeGridChild> <TradeGridChild>
<Tabs> <Tabs>
<Tab id="ticket" name={t('Ticket')}> <Tab id="ticket" name={t('Ticket')}>
<TradingViews.Ticket marketId={marketId} /> <TradingViews.Ticket
marketId={marketId}
onClickCollateral={() => navigate('/portfolio')}
/>
</Tab> </Tab>
<Tab id="info" name={t('Info')}> <Tab id="info" name={t('Info')}>
<TradingViews.Info <TradingViews.Info
@ -210,15 +213,21 @@ interface TradePanelsProps {
market: Market | null; market: Market | null;
onSelect: (marketId: string) => void; onSelect: (marketId: string) => void;
onMarketClick?: (marketId: string) => void; onMarketClick?: (marketId: string) => void;
onClickCollateral: () => void;
} }
export const TradePanels = ({ market, onSelect }: TradePanelsProps) => { export const TradePanels = ({
market,
onSelect,
onClickCollateral,
}: TradePanelsProps) => {
const [view, setView] = useState<TradingView>('Candles'); const [view, setView] = useState<TradingView>('Candles');
const renderView = () => { const renderView = () => {
const Component = memo<{ const Component = memo<{
marketId: string; marketId: string;
onSelect: (marketId: string) => void; onSelect: (marketId: string) => void;
onMarketClick?: (marketId: string) => void; onMarketClick?: (marketId: string) => void;
onClickCollateral: () => void;
}>(TradingViews[view]); }>(TradingViews[view]);
if (!Component) { if (!Component) {
@ -227,7 +236,13 @@ export const TradePanels = ({ market, onSelect }: TradePanelsProps) => {
if (!market) return <Splash>{NO_MARKET}</Splash>; if (!market) return <Splash>{NO_MARKET}</Splash>;
return <Component marketId={market?.id} onSelect={onSelect} />; return (
<Component
marketId={market?.id}
onSelect={onSelect}
onClickCollateral={onClickCollateral}
/>
);
}; };
return ( return (

View File

@ -42,7 +42,7 @@ describe('Footer', () => {
describe('NodeHealth', () => { describe('NodeHealth', () => {
const cases = [ const cases = [
{ diff: 0, classname: 'bg-success', text: 'Operational' }, { diff: 0, classname: 'bg-vega-green-550', text: 'Operational' },
{ diff: 5, classname: 'bg-warning', text: '5 Blocks behind' }, { diff: 5, classname: 'bg-warning', text: '5 Blocks behind' },
{ diff: -1, classname: 'bg-danger', text: 'Non operational' }, { diff: -1, classname: 'bg-danger', text: 'Non operational' },
]; ];

View File

@ -1,5 +1,5 @@
import { formatNumber, t } from '@vegaprotocol/react-helpers'; import { formatNumber, t } from '@vegaprotocol/react-helpers';
import { ButtonLink } from '@vegaprotocol/ui-toolkit'; import { Notification, Intent } from '@vegaprotocol/ui-toolkit';
import { DepositDialog, useDepositDialog } from '@vegaprotocol/deposits'; import { DepositDialog, useDepositDialog } from '@vegaprotocol/deposits';
interface Props { interface Props {
@ -16,27 +16,23 @@ export const MarginWarning = ({ margin, balance, asset }: Props) => {
const openDepositDialog = useDepositDialog((state) => state.open); const openDepositDialog = useDepositDialog((state) => state.open);
return ( return (
<> <>
<div <Notification
className="text-xs text-warning mb-4" intent={Intent.Warning}
data-testid="dealticket-warning-margin" testId="dealticket-warning-margin"
> message={`You may not have enough margin available to open this position. ${formatNumber(
<p className="mb-2"> margin,
{t('You may not have enough margin available to open this position.')}{' '} asset.decimals
<ButtonLink )} ${asset.symbol} ${t(
data-testid="deal-ticket-deposit-dialog-button" 'is currently required. You have only'
onClick={() => openDepositDialog(asset.id)} )} ${formatNumber(balance, asset.decimals)} ${asset.symbol} ${t(
> 'available.'
{t(`Deposit ${asset.symbol}`)} )}`}
</ButtonLink> buttonProps={{
</p> text: t(`Deposit ${asset.symbol}`),
<p> action: () => openDepositDialog(asset.id),
{`${formatNumber(margin, asset.decimals)} ${asset.symbol} ${t( dataTestId: 'deal-ticket-deposit-dialog-button',
'currently required' }}
)}, ${formatNumber(balance, asset.decimals)} ${asset.symbol} ${t( />
'available'
)}`}
</p>
</div>
<DepositDialog /> <DepositDialog />
</> </>
); );

View File

@ -1,26 +1,36 @@
import { t } from '@vegaprotocol/react-helpers'; import { Intent, Notification, Link } from '@vegaprotocol/ui-toolkit';
import { ButtonLink, InputError } from '@vegaprotocol/ui-toolkit';
import { useDepositDialog } from '@vegaprotocol/deposits'; import { useDepositDialog } from '@vegaprotocol/deposits';
import { t } from '@vegaprotocol/react-helpers';
interface ZeroBalanceErrorProps { interface ZeroBalanceErrorProps {
asset: { asset: {
id: string; id: string;
symbol: string; symbol: string;
}; };
onClickCollateral: () => void;
} }
export const ZeroBalanceError = ({ asset }: ZeroBalanceErrorProps) => { export const ZeroBalanceError = ({
asset,
onClickCollateral,
}: ZeroBalanceErrorProps) => {
const openDepositDialog = useDepositDialog((state) => state.open); const openDepositDialog = useDepositDialog((state) => state.open);
return ( return (
<InputError data-testid="dealticket-error-message-zero-balance"> <Notification
<p className="mb-2"> intent={Intent.Warning}
{t('Insufficient balance. ')} testId="dealticket-error-message-zero-balance"
<ButtonLink message={
data-testid="deal-ticket-deposit-dialog-button" <>
onClick={() => openDepositDialog(asset.id)} You need {asset.symbol} in your wallet to trade in this market. See
> all your <Link onClick={onClickCollateral}>collateral</Link>.
{t(`Deposit ${asset.symbol}`)} </>
</ButtonLink> }
</p> buttonProps={{
</InputError> text: t(`Make a deposit`),
action: () => openDepositDialog(asset.id),
dataTestId: 'deal-ticket-deposit-dialog-button',
size: 'md',
}}
/>
); );
}; };

View File

@ -1,7 +1,7 @@
import { t } from '@vegaprotocol/react-helpers'; import { t } from '@vegaprotocol/react-helpers';
import type { ButtonVariant } from '@vegaprotocol/ui-toolkit'; import type { ButtonVariant } from '@vegaprotocol/ui-toolkit';
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
interface Props { interface Props {
disabled: boolean; disabled: boolean;
@ -9,32 +9,19 @@ interface Props {
} }
export const DealTicketButton = ({ disabled, variant }: Props) => { export const DealTicketButton = ({ disabled, variant }: Props) => {
const { pubKey } = useVegaWallet(); const { pubKey, isReadOnly } = useVegaWallet();
const openVegaWalletDialog = useVegaWalletDialogStore( const isDisabled = !pubKey || isReadOnly || disabled;
(store) => store.openVegaWalletDialog return (
);
return pubKey ? (
<div className="mb-4"> <div className="mb-4">
<Button <Button
variant={variant} variant={variant}
fill fill
type="submit" type="submit"
disabled={disabled} disabled={isDisabled}
data-testid="place-order" data-testid="place-order"
> >
{t('Place order')} {t('Place order')}
</Button> </Button>
</div> </div>
) : (
<Button
variant="default"
fill
type="button"
data-testid="order-connect-wallet"
onClick={openVegaWalletDialog}
className="mb-6"
>
{t('Connect wallet')}
</Button>
); );
}; };

View File

@ -8,9 +8,13 @@ import { DealTicket } from './deal-ticket';
export interface DealTicketContainerProps { export interface DealTicketContainerProps {
marketId: string; marketId: string;
onClickCollateral?: () => void;
} }
export const DealTicketContainer = ({ marketId }: DealTicketContainerProps) => { export const DealTicketContainer = ({
marketId,
onClickCollateral,
}: DealTicketContainerProps) => {
const { const {
data: market, data: market,
error: marketError, error: marketError,
@ -41,6 +45,7 @@ export const DealTicketContainer = ({ marketId }: DealTicketContainerProps) => {
market={market} market={market}
marketData={marketData} marketData={marketData}
submit={(orderSubmission) => create({ orderSubmission })} submit={(orderSubmission) => create({ orderSubmission })}
onClickCollateral={onClickCollateral || (() => null)}
/> />
) : ( ) : (
<Splash> <Splash>

View File

@ -1,4 +1,4 @@
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit'; import { FormGroup, Input, NotificationError } from '@vegaprotocol/ui-toolkit';
import { t, toDecimal, validateAmount } from '@vegaprotocol/react-helpers'; import { t, toDecimal, validateAmount } from '@vegaprotocol/react-helpers';
import type { DealTicketAmountProps } from './deal-ticket-amount'; import type { DealTicketAmountProps } from './deal-ticket-amount';
@ -20,17 +20,17 @@ export const DealTicketLimitAmount = ({
const renderError = () => { const renderError = () => {
if (sizeError) { if (sizeError) {
return ( return (
<InputError data-testid="dealticket-error-message-size-limit"> <NotificationError testId="dealticket-error-message-size-limit">
{sizeError} {sizeError}
</InputError> </NotificationError>
); );
} }
if (priceError) { if (priceError) {
return ( return (
<InputError data-testid="dealticket-error-message-price-limit"> <NotificationError testId="dealticket-error-message-price-limit">
{priceError} {priceError}
</InputError> </NotificationError>
); );
} }

View File

@ -4,7 +4,7 @@ import {
toDecimal, toDecimal,
validateAmount, validateAmount,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { Input, InputError, Tooltip } from '@vegaprotocol/ui-toolkit'; import { Input, NotificationError, Tooltip } from '@vegaprotocol/ui-toolkit';
import { isMarketInAuction } from '../../utils'; import { isMarketInAuction } from '../../utils';
import type { DealTicketAmountProps } from './deal-ticket-amount'; import type { DealTicketAmountProps } from './deal-ticket-amount';
import { getMarketPrice } from '../../utils/get-price'; import { getMarketPrice } from '../../utils/get-price';
@ -77,12 +77,12 @@ export const DealTicketMarketAmount = ({
</div> </div>
</div> </div>
{sizeError && ( {sizeError && (
<InputError <NotificationError
intent="danger" intent="danger"
data-testid="dealticket-error-message-size-market" testId="dealticket-error-message-size-market"
> >
{sizeError} {sizeError}
</InputError> </NotificationError>
)} )}
</div> </div>
); );

View File

@ -1,12 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { VegaWalletContext } from '@vegaprotocol/wallet'; import { VegaWalletContext } from '@vegaprotocol/wallet';
import { import { fireEvent, render, screen, act } from '@testing-library/react';
fireEvent,
render,
screen,
act,
waitFor,
} from '@testing-library/react';
import { generateMarket, generateMarketData } from '../../test-helpers'; import { generateMarket, generateMarketData } from '../../test-helpers';
import { DealTicket } from './deal-ticket'; import { DealTicket } from './deal-ticket';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
@ -15,14 +9,6 @@ import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import type { ChainIdQuery } from '@vegaprotocol/react-helpers'; import type { ChainIdQuery } from '@vegaprotocol/react-helpers';
import { ChainIdDocument, addDecimal } from '@vegaprotocol/react-helpers'; import { ChainIdDocument, addDecimal } from '@vegaprotocol/react-helpers';
import * as utils from '../../utils';
let mockHasNoBalance = false;
jest.mock('../../hooks/use-has-no-balance', () => {
return {
useHasNoBalance: () => mockHasNoBalance,
};
});
const market = generateMarket(); const market = generateMarket();
const marketData = generateMarketData(); const marketData = generateMarketData();
@ -148,50 +134,6 @@ describe('DealTicket', () => {
); );
}); });
it('validation should be reset', async () => {
mockHasNoBalance = true;
jest.spyOn(utils, 'validateMarketState').mockReturnValue('Wrong state');
jest
.spyOn(utils, 'validateMarketTradingMode')
.mockReturnValue('Wrong trading mode');
const { rerender } = render(generateJsx());
await act(async () => {
fireEvent.click(screen.getByTestId('place-order'));
});
await waitFor(async () => {
expect(
await screen.getByTestId('dealticket-error-message-summary')
).toHaveTextContent('Wrong state');
});
jest.spyOn(utils, 'validateMarketState').mockReturnValue(true);
await act(async () => {
rerender(generateJsx());
});
await act(async () => {
fireEvent.click(screen.getByTestId('place-order'));
});
await waitFor(async () => {
expect(
await screen.getByTestId('dealticket-error-message-zero-balance')
).toHaveTextContent('Insufficient balance.');
});
mockHasNoBalance = false;
await act(async () => {
rerender(generateJsx());
});
await act(async () => {
fireEvent.click(screen.getByTestId('place-order'));
});
await waitFor(async () => {
expect(
await screen.getByTestId('dealticket-error-message-summary')
).toHaveTextContent('Wrong trading mode');
});
});
it('can edit deal ticket', async () => { it('can edit deal ticket', async () => {
render(generateJsx()); render(generateJsx());

View File

@ -10,9 +10,15 @@ import { SideSelector } from './side-selector';
import { TimeInForceSelector } from './time-in-force-selector'; import { TimeInForceSelector } from './time-in-force-selector';
import { TypeSelector } from './type-selector'; import { TypeSelector } from './type-selector';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet'; import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { normalizeOrderSubmission } from '@vegaprotocol/wallet'; import { normalizeOrderSubmission } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { InputError } from '@vegaprotocol/ui-toolkit'; import {
ExternalLink,
NotificationError,
Intent,
Notification,
} from '@vegaprotocol/ui-toolkit';
import { useOrderMarginValidation } from '../../hooks/use-order-margin-validation'; import { useOrderMarginValidation } from '../../hooks/use-order-margin-validation';
import { MarginWarning } from '../deal-ticket-validation/margin-warning'; import { MarginWarning } from '../deal-ticket-validation/margin-warning';
import { import {
@ -37,6 +43,7 @@ export interface DealTicketProps {
market: Market; market: Market;
marketData: MarketData; marketData: MarketData;
submit: (order: OrderSubmissionBody['orderSubmission']) => void; submit: (order: OrderSubmissionBody['orderSubmission']) => void;
onClickCollateral?: () => void;
} }
export type DealTicketFormFields = OrderSubmissionBody['orderSubmission'] & { export type DealTicketFormFields = OrderSubmissionBody['orderSubmission'] & {
@ -45,7 +52,12 @@ export type DealTicketFormFields = OrderSubmissionBody['orderSubmission'] & {
summary: string; summary: string;
}; };
export const DealTicket = ({ market, marketData, submit }: DealTicketProps) => { export const DealTicket = ({
market,
marketData,
submit,
onClickCollateral,
}: DealTicketProps) => {
const { pubKey, isReadOnly } = useVegaWallet(); const { pubKey, isReadOnly } = useVegaWallet();
const { getPersistedOrder, setPersistedOrder } = usePersistedOrderStore( const { getPersistedOrder, setPersistedOrder } = usePersistedOrderStore(
(store) => ({ (store) => ({
@ -87,6 +99,44 @@ export const DealTicket = ({ market, marketData, submit }: DealTicketProps) => {
const marketTradingModeError = validateMarketTradingMode( const marketTradingModeError = validateMarketTradingMode(
marketData.marketTradingMode marketData.marketTradingMode
); );
const checkForErrors = useCallback(() => {
if (!pubKey) {
setError('summary', { message: t('No public key selected') });
return;
}
if (marketStateError !== true) {
setError('summary', {
message: marketStateError,
type: SummaryValidationType.MarketState,
});
return;
}
if (hasNoBalance) {
setError('summary', {
message: SummaryValidationType.NoCollateral,
type: SummaryValidationType.NoCollateral,
});
return;
}
if (marketTradingModeError !== true) {
setError('summary', {
message: marketTradingModeError,
type: SummaryValidationType.TradingMode,
});
return;
}
}, [
hasNoBalance,
marketStateError,
marketTradingModeError,
pubKey,
setError,
]);
useEffect(() => { useEffect(() => {
if ( if (
(!hasNoBalance && (!hasNoBalance &&
@ -98,6 +148,7 @@ export const DealTicket = ({ market, marketData, submit }: DealTicketProps) => {
) { ) {
clearErrors('summary'); clearErrors('summary');
} }
checkForErrors();
}, [ }, [
hasNoBalance, hasNoBalance,
marketStateError, marketStateError,
@ -105,39 +156,12 @@ export const DealTicket = ({ market, marketData, submit }: DealTicketProps) => {
clearErrors, clearErrors,
errors.summary?.message, errors.summary?.message,
errors.summary?.type, errors.summary?.type,
checkForErrors,
]); ]);
const onSubmit = useCallback( const onSubmit = useCallback(
(order: OrderSubmissionBody['orderSubmission']) => { (order: OrderSubmissionBody['orderSubmission']) => {
if (!pubKey) { checkForErrors();
setError('summary', { message: t('No public key selected') });
return;
}
if (marketStateError !== true) {
setError('summary', {
message: marketStateError,
type: SummaryValidationType.MarketState,
});
return;
}
if (hasNoBalance) {
setError('summary', {
message: SummaryValidationType.NoCollateral,
type: SummaryValidationType.NoCollateral,
});
return;
}
if (marketTradingModeError !== true) {
setError('summary', {
message: marketTradingModeError,
type: SummaryValidationType.TradingMode,
});
return;
}
submit( submit(
normalizeOrderSubmission( normalizeOrderSubmission(
order, order,
@ -146,16 +170,7 @@ export const DealTicket = ({ market, marketData, submit }: DealTicketProps) => {
) )
); );
}, },
[ [checkForErrors, submit, market.decimalPlaces, market.positionDecimalPlaces]
submit,
pubKey,
hasNoBalance,
market.positionDecimalPlaces,
market.decimalPlaces,
marketStateError,
marketTradingModeError,
setError,
]
); );
return ( return (
@ -233,16 +248,18 @@ export const DealTicket = ({ market, marketData, submit }: DealTicketProps) => {
)} )}
/> />
)} )}
<DealTicketButton
disabled={Object.keys(errors).length >= 1 || isReadOnly}
variant={order.side === Schema.Side.SIDE_BUY ? 'ternary' : 'secondary'}
/>
<SummaryMessage <SummaryMessage
errorMessage={errors.summary?.message} errorMessage={errors.summary?.message}
market={market} market={market}
marketData={marketData} marketData={marketData}
order={order} order={order}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
pubKey={pubKey}
onClickCollateral={onClickCollateral || (() => null)}
/>
<DealTicketButton
disabled={Object.keys(errors).length >= 1 || isReadOnly}
variant={order.side === Schema.Side.SIDE_BUY ? 'ternary' : 'secondary'}
/> />
<DealTicketFeeDetails <DealTicketFeeDetails
order={order} order={order}
@ -263,6 +280,8 @@ interface SummaryMessageProps {
marketData: MarketData; marketData: MarketData;
order: OrderSubmissionBody['orderSubmission']; order: OrderSubmissionBody['orderSubmission'];
isReadOnly: boolean; isReadOnly: boolean;
pubKey: string | null;
onClickCollateral: () => void;
} }
const SummaryMessage = memo( const SummaryMessage = memo(
({ ({
@ -271,30 +290,60 @@ const SummaryMessage = memo(
marketData, marketData,
order, order,
isReadOnly, isReadOnly,
pubKey,
onClickCollateral,
}: SummaryMessageProps) => { }: SummaryMessageProps) => {
// Specific error UI for if balance is so we can // Specific error UI for if balance is so we can
// render a deposit dialog // render a deposit dialog
const asset = market.tradableInstrument.instrument.product.settlementAsset; const asset = market.tradableInstrument.instrument.product.settlementAsset;
const assetSymbol = asset.symbol;
const { balanceError, balance, margin } = useOrderMarginValidation({ const { balanceError, balance, margin } = useOrderMarginValidation({
market, market,
marketData, marketData,
order, order,
}); });
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
if (isReadOnly) { if (isReadOnly) {
return ( return (
<div className="mb-4"> <div className="mb-4">
<InputError data-testid="dealticket-error-message-summary"> <NotificationError testId="dealticket-error-message-summary">
{ {
'You need to connect your own wallet to start trading on this market' 'You need to connect your own wallet to start trading on this market'
} }
</InputError> </NotificationError>
</div> </div>
); );
} }
if (!pubKey) {
return (
<Notification
testId={'deal-ticket-connect-wallet'}
intent={Intent.Warning}
message={
<p className="text-sm pb-2">
You need a{' '}
<ExternalLink href="https://vega.xyz/wallet">
Vega wallet
</ExternalLink>{' '}
with {assetSymbol} to start trading in this market.
</p>
}
buttonProps={{
text: t('Connect wallet'),
action: openVegaWalletDialog,
dataTestId: 'order-connect-wallet',
size: 'md',
}}
/>
);
}
if (errorMessage === SummaryValidationType.NoCollateral) { if (errorMessage === SummaryValidationType.NoCollateral) {
return ( return (
<ZeroBalanceError <ZeroBalanceError
asset={market.tradableInstrument.instrument.product.settlementAsset} asset={market.tradableInstrument.instrument.product.settlementAsset}
onClickCollateral={onClickCollateral}
/> />
); );
} }
@ -304,9 +353,9 @@ const SummaryMessage = memo(
if (errorMessage) { if (errorMessage) {
return ( return (
<div className="mb-4"> <div className="mb-4">
<InputError data-testid="dealticket-error-message-summary"> <NotificationError testId="dealticket-error-message-summary">
{errorMessage} {errorMessage}
</InputError> </NotificationError>
</div> </div>
); );
} }
@ -326,14 +375,13 @@ const SummaryMessage = memo(
].includes(marketData.marketTradingMode) ].includes(marketData.marketTradingMode)
) { ) {
return ( return (
<div <Notification
className="text-sm text-warning mb-4" intent={Intent.Warning}
data-testid="dealticket-warning-auction" testId={'dealticket-warning-auction'}
> message={t(
<p> 'Any orders placed now will not trade until the auction ends'
{t('Any orders placed now will not trade until the auction ends')} )}
</p> />
</div>
); );
} }

View File

@ -1,4 +1,4 @@
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit'; import { FormGroup, Input, NotificationError } from '@vegaprotocol/ui-toolkit';
import { formatForInput } from '@vegaprotocol/react-helpers'; import { formatForInput } from '@vegaprotocol/react-helpers';
import { t } from '@vegaprotocol/react-helpers'; import { t } from '@vegaprotocol/react-helpers';
import type { UseFormRegister } from 'react-hook-form'; import type { UseFormRegister } from 'react-hook-form';
@ -35,9 +35,9 @@ export const ExpirySelector = ({
})} })}
/> />
{errorMessage && ( {errorMessage && (
<InputError data-testid="dealticket-error-message-expiry"> <NotificationError testId="dealticket-error-message-expiry">
{errorMessage} {errorMessage}
</InputError> </NotificationError>
)} )}
</FormGroup> </FormGroup>
); );

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
FormGroup, FormGroup,
InputError, NotificationError,
Select, Select,
Tooltip, Tooltip,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
@ -139,9 +139,9 @@ export const TimeInForceSelector = ({
))} ))}
</Select> </Select>
{errorMessage && ( {errorMessage && (
<InputError data-testid="dealticket-error-message-tif"> <NotificationError testId="dealticket-error-message-tif">
{renderError(errorMessage)} {renderError(errorMessage)}
</InputError> </NotificationError>
)} )}
</FormGroup> </FormGroup>
); );

View File

@ -1,4 +1,8 @@
import { FormGroup, InputError, Tooltip } from '@vegaprotocol/ui-toolkit'; import {
FormGroup,
NotificationError,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { DataGrid, t } from '@vegaprotocol/react-helpers'; import { DataGrid, t } from '@vegaprotocol/react-helpers';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { Toggle } from '@vegaprotocol/ui-toolkit'; import { Toggle } from '@vegaprotocol/ui-toolkit';
@ -78,9 +82,9 @@ export const TypeSelector = ({
onChange={(e) => onSelect(e.target.value as Schema.OrderType)} onChange={(e) => onSelect(e.target.value as Schema.OrderType)}
/> />
{errorMessage && ( {errorMessage && (
<InputError data-testid="dealticket-error-message-type"> <NotificationError testId="dealticket-error-message-type">
{renderError(errorMessage as MarketModeValidationType)} {renderError(errorMessage as MarketModeValidationType)}
</InputError> </NotificationError>
)} )}
</FormGroup> </FormGroup>
); );

View File

@ -103,7 +103,7 @@ describe('FillsTable', () => {
}); });
const amountCell = cells.find((c) => c.getAttribute('col-id') === 'size'); const amountCell = cells.find((c) => c.getAttribute('col-id') === 'size');
expect(amountCell).toHaveClass('text-vega-green'); expect(amountCell).toHaveClass('text-vega-green-550');
}); });
it('should format cells correctly for seller fill', async () => { it('should format cells correctly for seller fill', async () => {

View File

@ -1,6 +1,6 @@
export * from './__generated__/MarketDepth'; export * from './__generated__/MarketDepth';
export * from './depth-chart'; export * from './depth-chart';
export { marketDepthProvider } from './market-depth-provider'; export * from './market-depth-provider';
export * from './orderbook-container'; export * from './orderbook-container';
export * from './orderbook-data'; export * from './orderbook-data';
export * from './orderbook-manager'; export * from './orderbook-manager';

View File

@ -334,7 +334,7 @@ export const Orderbook = ({
BigInt(maxPriceLevel) - BigInt(maxPriceLevel) -
BigInt(offsetTop) * BigInt(resolution) BigInt(offsetTop) * BigInt(resolution)
).toString() ).toString()
: rows?.[Math.min(offsetTop, rows.length - 1)].price.toString(); : rows?.[Math.min(offsetTop, rows.length - 1)]?.price?.toString();
if (lockOnMidPrice) { if (lockOnMidPrice) {
setLockOnMidPrice(false); setLockOnMidPrice(false);
} }

View File

@ -92,7 +92,7 @@ it('add color and sign to amount, displays positive notional value', async () =>
}); });
let cells = screen.getAllByRole('gridcell'); let cells = screen.getAllByRole('gridcell');
expect(cells[2].classList.contains('text-vega-green')).toBeTruthy(); expect(cells[2].classList.contains('text-vega-green-550')).toBeTruthy();
expect(cells[2].classList.contains('text-vega-pink')).toBeFalsy(); expect(cells[2].classList.contains('text-vega-pink')).toBeFalsy();
expect(cells[2].textContent).toEqual('+100'); expect(cells[2].textContent).toEqual('+100');
expect(cells[1].textContent).toEqual('1,230.0'); expect(cells[1].textContent).toEqual('1,230.0');
@ -105,7 +105,7 @@ it('add color and sign to amount, displays positive notional value', async () =>
); );
}); });
cells = screen.getAllByRole('gridcell'); cells = screen.getAllByRole('gridcell');
expect(cells[2].classList.contains('text-vega-green')).toBeFalsy(); expect(cells[2].classList.contains('text-vega-green-550')).toBeFalsy();
expect(cells[2].classList.contains('text-vega-pink')).toBeTruthy(); expect(cells[2].classList.contains('text-vega-pink')).toBeTruthy();
expect(cells[2].textContent?.startsWith('-100')).toBeTruthy(); expect(cells[2].textContent?.startsWith('-100')).toBeTruthy();
expect(cells[1].textContent).toEqual('1,230.0'); expect(cells[1].textContent).toEqual('1,230.0');

View File

@ -1,4 +1,4 @@
export const positiveClassNames = 'text-vega-green dark:text-vega-green'; export const positiveClassNames = 'text-vega-green-550 dark:text-vega-green';
export const negativeClassNames = 'text-vega-pink dark:text-vega-pink'; export const negativeClassNames = 'text-vega-pink dark:text-vega-pink';
const isPositive = ({ value }: { value: string | bigint | number }) => const isPositive = ({ value }: { value: string | bigint | number }) =>

View File

@ -116,7 +116,10 @@ export const TradesTable = forwardRef<AgGridReact, Props>((props, ref) => {
if (!data?.market) { if (!data?.market) {
return null; return null;
} }
return addDecimal(value, data.market.positionDecimalPlaces); return addDecimalsFormatNumber(
value,
data.market.positionDecimalPlaces
);
}} }}
/> />
<AgGridColumn <AgGridColumn

View File

@ -9,7 +9,7 @@ import { Icon } from '../icon';
import classnames from 'classnames'; import classnames from 'classnames';
export type ButtonVariant = 'default' | 'primary' | 'secondary' | 'ternary'; export type ButtonVariant = 'default' | 'primary' | 'secondary' | 'ternary';
type ButtonSize = 'lg' | 'md' | 'sm' | 'xs'; export type ButtonSize = 'lg' | 'md' | 'sm' | 'xs';
const base = 'inline-block uppercase border rounded-md disabled:opacity-60'; const base = 'inline-block uppercase border rounded-md disabled:opacity-60';
const xs = 'px-2 py-0 text-sm'; const xs = 'px-2 py-0 text-sm';

View File

@ -1,5 +1,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
import { Intent } from '../../utils/intent';
import { Notification } from '../notification';
interface InputErrorProps extends HTMLAttributes<HTMLDivElement> { interface InputErrorProps extends HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode; children?: React.ReactNode;
@ -7,6 +9,40 @@ interface InputErrorProps extends HTMLAttributes<HTMLDivElement> {
forInput?: string; forInput?: string;
} }
interface NotificationErrorProps extends HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
intent?: Intent | 'danger' | 'warning';
forInput?: string;
testId?: string;
}
const getIntent = (intent: Intent | 'danger' | 'warning') => {
switch (intent) {
case 'danger':
return Intent.Danger;
case 'warning':
return Intent.Warning;
default:
return intent;
}
};
export const NotificationError = ({
intent = Intent.Danger,
children,
forInput,
testId,
}: NotificationErrorProps) => {
return (
<Notification
intent={getIntent(intent)}
testId={testId || 'input-error-text'}
message={<div className="role">{children}</div>}
aria-describedby={forInput}
/>
);
};
export const InputError = ({ export const InputError = ({
intent = 'danger', intent = 'danger',
children, children,

View File

@ -4,13 +4,20 @@ import classNames from 'classnames';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { Intent } from '../../utils/intent'; import { Intent } from '../../utils/intent';
import { Icon } from '../icon'; import { Icon } from '../icon';
import type { ButtonSize } from '../button';
import { Button } from '../button'; import { Button } from '../button';
type NotificationProps = { type NotificationProps = {
intent: Intent; intent: Intent;
message: ReactNode | string; message: ReactNode | string;
title?: string; title?: string;
buttonProps?: { text: string; action: () => void; className?: string }; buttonProps?: {
text: string;
action: () => void;
className?: string;
dataTestId?: string;
size?: ButtonSize;
};
testId?: string; testId?: string;
className?: string; className?: string;
}; };
@ -41,11 +48,20 @@ export const Notification = ({
{ {
'border-gray-700 dark:border-gray-300': intent === Intent.None, 'border-gray-700 dark:border-gray-300': intent === Intent.None,
'border-vega-blue': intent === Intent.Primary, 'border-vega-blue': intent === Intent.Primary,
'border-vega-green dark:border-vega-green': intent === Intent.Success, 'border-vega-green-550 dark:border-vega-green':
'border-yellow-500': intent === Intent.Warning, intent === Intent.Success,
'border-vega-orange': intent === Intent.Warning,
'border-vega-pink': intent === Intent.Danger, 'border-vega-pink': intent === Intent.Danger,
}, },
'border rounded text-xs p-4 flex items-start gap-2.5 bg-neutral-100 dark:bg-neutral-900', {
'bg-vega-light-100 dark:bg-vega-dark-100 ': intent === Intent.None,
'bg-vega-blue-300 dark:bg-vega-blue-700': intent === Intent.Primary,
'bg-vega-green-300 dark:bg-vega-green-700': intent === Intent.Success,
'bg-vega-orange-300 dark:bg-vega-orange-650':
intent === Intent.Warning,
'bg-vega-pink-300 dark:bg-vega-pink-650': intent === Intent.Danger,
},
'border rounded p-2 flex items-start gap-2.5 my-4',
className className
)} )}
> >
@ -55,7 +71,7 @@ export const Notification = ({
'text-gray-700 dark:text-gray-300': intent === Intent.None, 'text-gray-700 dark:text-gray-300': intent === Intent.None,
'text-vega-blue': intent === Intent.Primary, 'text-vega-blue': intent === Intent.Primary,
'text-vega-green dark:text-vega-green': intent === Intent.Success, 'text-vega-green dark:text-vega-green': intent === Intent.Success,
'text-yellow-600 dark:text-yellow-500': intent === Intent.Warning, 'text-yellow-600 dark:text-yellow': intent === Intent.Warning,
'text-vega-pink': intent === Intent.Danger, 'text-vega-pink': intent === Intent.Danger,
}, },
'flex items-start mt-1' 'flex items-start mt-1'
@ -63,18 +79,19 @@ export const Notification = ({
> >
<Icon size={4} name={getIcon(intent)} /> <Icon size={4} name={getIcon(intent)} />
</div> </div>
<div className="flex flex-col flex-grow items-start gap-1.5 text-base"> <div className="flex flex-col flex-grow items-start gap-1.5">
{title && ( {title && (
<div className="whitespace-nowrap overflow-hidden text-ellipsis uppercase text-sm leading-6"> <div className="whitespace-nowrap overflow-hidden text-ellipsis uppercase leading-6">
{title} {title}
</div> </div>
)} )}
<div>{message}</div> <div className="text-sm">{message}</div>
{buttonProps && ( {buttonProps && (
<Button <Button
size="sm" size={buttonProps.size || 'sm'}
onClick={buttonProps.action} onClick={buttonProps.action}
className={classNames('mt-2 px-6 py-3', buttonProps.className)} className={classNames(buttonProps.className)}
data-testid={buttonProps.dataTestId}
> >
{buttonProps.text} {buttonProps.text}
</Button> </Button>

View File

@ -23,7 +23,8 @@ export const getIntentBackground = (intent?: Intent) => {
'bg-vega-pink dark:bg-vega-yellow': intent === Intent.Primary, 'bg-vega-pink dark:bg-vega-yellow': intent === Intent.Primary,
'bg-danger': intent === Intent.Danger, 'bg-danger': intent === Intent.Danger,
'bg-warning': intent === Intent.Warning, 'bg-warning': intent === Intent.Warning,
'bg-success': intent === Intent.Success, // contrast issues with light mode
'bg-vega-green-550 dark:bg-vega-green': intent === Intent.Success,
}; };
}; };