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

View File

@ -13,7 +13,6 @@ const toggleShort = 'order-side-SIDE_SELL';
const toggleLong = 'order-side-SIDE_BUY';
const toggleLimit = 'order-type-TYPE_LIMIT';
const toggleMarket = 'order-type-TYPE_MARKET';
const errorMessage = 'dealticket-error-message';
const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => {
return {
@ -363,11 +362,11 @@ describe('deal ticket validation', { tags: '@smoke' }, () => {
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('order-connect-wallet').should('exist');
cy.getByTestId(placeOrderBtn).should('not.exist');
cy.getByTestId(errorMessage).should('not.exist');
cy.getByTestId(placeOrderBtn).should('exist');
cy.getByTestId('deal-ticket-connect-wallet').should('exist');
});
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', () => {
cy.getByTestId('place-order').should('not.be.disabled');
cy.getByTestId('place-order').click();
cy.getByTestId('place-order').should('be.disabled');
//7002-SORD-003
cy.getByTestId('dealticket-error-message-zero-balance').should(
'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');
});
@ -708,7 +707,7 @@ describe('account validation', { tags: '@regression' }, () => {
);
cy.getByTestId('dealticket-warning-margin').should(
'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('dialog-content')

View File

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

View File

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

View File

@ -42,7 +42,7 @@ describe('Footer', () => {
describe('NodeHealth', () => {
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: -1, classname: 'bg-danger', text: 'Non operational' },
];

View File

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

View File

@ -1,26 +1,36 @@
import { t } from '@vegaprotocol/react-helpers';
import { ButtonLink, InputError } from '@vegaprotocol/ui-toolkit';
import { Intent, Notification, Link } from '@vegaprotocol/ui-toolkit';
import { useDepositDialog } from '@vegaprotocol/deposits';
import { t } from '@vegaprotocol/react-helpers';
interface ZeroBalanceErrorProps {
asset: {
id: string;
symbol: string;
};
onClickCollateral: () => void;
}
export const ZeroBalanceError = ({ asset }: ZeroBalanceErrorProps) => {
export const ZeroBalanceError = ({
asset,
onClickCollateral,
}: ZeroBalanceErrorProps) => {
const openDepositDialog = useDepositDialog((state) => state.open);
return (
<InputError data-testid="dealticket-error-message-zero-balance">
<p className="mb-2">
{t('Insufficient balance. ')}
<ButtonLink
data-testid="deal-ticket-deposit-dialog-button"
onClick={() => openDepositDialog(asset.id)}
>
{t(`Deposit ${asset.symbol}`)}
</ButtonLink>
</p>
</InputError>
<Notification
intent={Intent.Warning}
testId="dealticket-error-message-zero-balance"
message={
<>
You need {asset.symbol} in your wallet to trade in this market. See
all your <Link onClick={onClickCollateral}>collateral</Link>.
</>
}
buttonProps={{
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 type { ButtonVariant } from '@vegaprotocol/ui-toolkit';
import { Button } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
interface Props {
disabled: boolean;
@ -9,32 +9,19 @@ interface Props {
}
export const DealTicketButton = ({ disabled, variant }: Props) => {
const { pubKey } = useVegaWallet();
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
return pubKey ? (
const { pubKey, isReadOnly } = useVegaWallet();
const isDisabled = !pubKey || isReadOnly || disabled;
return (
<div className="mb-4">
<Button
variant={variant}
fill
type="submit"
disabled={disabled}
disabled={isDisabled}
data-testid="place-order"
>
{t('Place order')}
</Button>
</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 {
marketId: string;
onClickCollateral?: () => void;
}
export const DealTicketContainer = ({ marketId }: DealTicketContainerProps) => {
export const DealTicketContainer = ({
marketId,
onClickCollateral,
}: DealTicketContainerProps) => {
const {
data: market,
error: marketError,
@ -41,6 +45,7 @@ export const DealTicketContainer = ({ marketId }: DealTicketContainerProps) => {
market={market}
marketData={marketData}
submit={(orderSubmission) => create({ orderSubmission })}
onClickCollateral={onClickCollateral || (() => null)}
/>
) : (
<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 type { DealTicketAmountProps } from './deal-ticket-amount';
@ -20,17 +20,17 @@ export const DealTicketLimitAmount = ({
const renderError = () => {
if (sizeError) {
return (
<InputError data-testid="dealticket-error-message-size-limit">
<NotificationError testId="dealticket-error-message-size-limit">
{sizeError}
</InputError>
</NotificationError>
);
}
if (priceError) {
return (
<InputError data-testid="dealticket-error-message-price-limit">
<NotificationError testId="dealticket-error-message-price-limit">
{priceError}
</InputError>
</NotificationError>
);
}

View File

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

View File

@ -1,12 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { VegaWalletContext } from '@vegaprotocol/wallet';
import {
fireEvent,
render,
screen,
act,
waitFor,
} from '@testing-library/react';
import { fireEvent, render, screen, act } from '@testing-library/react';
import { generateMarket, generateMarketData } from '../../test-helpers';
import { DealTicket } from './deal-ticket';
import * as Schema from '@vegaprotocol/types';
@ -15,14 +9,6 @@ import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type { ChainIdQuery } 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 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 () => {
render(generateJsx());

View File

@ -10,9 +10,15 @@ import { SideSelector } from './side-selector';
import { TimeInForceSelector } from './time-in-force-selector';
import { TypeSelector } from './type-selector';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { normalizeOrderSubmission } 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 { MarginWarning } from '../deal-ticket-validation/margin-warning';
import {
@ -37,6 +43,7 @@ export interface DealTicketProps {
market: Market;
marketData: MarketData;
submit: (order: OrderSubmissionBody['orderSubmission']) => void;
onClickCollateral?: () => void;
}
export type DealTicketFormFields = OrderSubmissionBody['orderSubmission'] & {
@ -45,7 +52,12 @@ export type DealTicketFormFields = OrderSubmissionBody['orderSubmission'] & {
summary: string;
};
export const DealTicket = ({ market, marketData, submit }: DealTicketProps) => {
export const DealTicket = ({
market,
marketData,
submit,
onClickCollateral,
}: DealTicketProps) => {
const { pubKey, isReadOnly } = useVegaWallet();
const { getPersistedOrder, setPersistedOrder } = usePersistedOrderStore(
(store) => ({
@ -87,6 +99,44 @@ export const DealTicket = ({ market, marketData, submit }: DealTicketProps) => {
const marketTradingModeError = validateMarketTradingMode(
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(() => {
if (
(!hasNoBalance &&
@ -98,6 +148,7 @@ export const DealTicket = ({ market, marketData, submit }: DealTicketProps) => {
) {
clearErrors('summary');
}
checkForErrors();
}, [
hasNoBalance,
marketStateError,
@ -105,39 +156,12 @@ export const DealTicket = ({ market, marketData, submit }: DealTicketProps) => {
clearErrors,
errors.summary?.message,
errors.summary?.type,
checkForErrors,
]);
const onSubmit = useCallback(
(order: OrderSubmissionBody['orderSubmission']) => {
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;
}
checkForErrors();
submit(
normalizeOrderSubmission(
order,
@ -146,16 +170,7 @@ export const DealTicket = ({ market, marketData, submit }: DealTicketProps) => {
)
);
},
[
submit,
pubKey,
hasNoBalance,
market.positionDecimalPlaces,
market.decimalPlaces,
marketStateError,
marketTradingModeError,
setError,
]
[checkForErrors, submit, market.decimalPlaces, market.positionDecimalPlaces]
);
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
errorMessage={errors.summary?.message}
market={market}
marketData={marketData}
order={order}
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
order={order}
@ -263,6 +280,8 @@ interface SummaryMessageProps {
marketData: MarketData;
order: OrderSubmissionBody['orderSubmission'];
isReadOnly: boolean;
pubKey: string | null;
onClickCollateral: () => void;
}
const SummaryMessage = memo(
({
@ -271,30 +290,60 @@ const SummaryMessage = memo(
marketData,
order,
isReadOnly,
pubKey,
onClickCollateral,
}: SummaryMessageProps) => {
// Specific error UI for if balance is so we can
// render a deposit dialog
const asset = market.tradableInstrument.instrument.product.settlementAsset;
const assetSymbol = asset.symbol;
const { balanceError, balance, margin } = useOrderMarginValidation({
market,
marketData,
order,
});
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
if (isReadOnly) {
return (
<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'
}
</InputError>
</NotificationError>
</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) {
return (
<ZeroBalanceError
asset={market.tradableInstrument.instrument.product.settlementAsset}
onClickCollateral={onClickCollateral}
/>
);
}
@ -304,9 +353,9 @@ const SummaryMessage = memo(
if (errorMessage) {
return (
<div className="mb-4">
<InputError data-testid="dealticket-error-message-summary">
<NotificationError testId="dealticket-error-message-summary">
{errorMessage}
</InputError>
</NotificationError>
</div>
);
}
@ -326,14 +375,13 @@ const SummaryMessage = memo(
].includes(marketData.marketTradingMode)
) {
return (
<div
className="text-sm text-warning mb-4"
data-testid="dealticket-warning-auction"
>
<p>
{t('Any orders placed now will not trade until the auction ends')}
</p>
</div>
<Notification
intent={Intent.Warning}
testId={'dealticket-warning-auction'}
message={t(
'Any orders placed now will not trade until the auction ends'
)}
/>
);
}

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 { t } from '@vegaprotocol/react-helpers';
import type { UseFormRegister } from 'react-hook-form';
@ -35,9 +35,9 @@ export const ExpirySelector = ({
})}
/>
{errorMessage && (
<InputError data-testid="dealticket-error-message-expiry">
<NotificationError testId="dealticket-error-message-expiry">
{errorMessage}
</InputError>
</NotificationError>
)}
</FormGroup>
);

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import {
FormGroup,
InputError,
NotificationError,
Select,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
@ -139,9 +139,9 @@ export const TimeInForceSelector = ({
))}
</Select>
{errorMessage && (
<InputError data-testid="dealticket-error-message-tif">
<NotificationError testId="dealticket-error-message-tif">
{renderError(errorMessage)}
</InputError>
</NotificationError>
)}
</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 * as Schema from '@vegaprotocol/types';
import { Toggle } from '@vegaprotocol/ui-toolkit';
@ -78,9 +82,9 @@ export const TypeSelector = ({
onChange={(e) => onSelect(e.target.value as Schema.OrderType)}
/>
{errorMessage && (
<InputError data-testid="dealticket-error-message-type">
<NotificationError testId="dealticket-error-message-type">
{renderError(errorMessage as MarketModeValidationType)}
</InputError>
</NotificationError>
)}
</FormGroup>
);

View File

@ -103,7 +103,7 @@ describe('FillsTable', () => {
});
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 () => {

View File

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

View File

@ -334,7 +334,7 @@ export const Orderbook = ({
BigInt(maxPriceLevel) -
BigInt(offsetTop) * BigInt(resolution)
).toString()
: rows?.[Math.min(offsetTop, rows.length - 1)].price.toString();
: rows?.[Math.min(offsetTop, rows.length - 1)]?.price?.toString();
if (lockOnMidPrice) {
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');
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].textContent).toEqual('+100');
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');
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].textContent?.startsWith('-100')).toBeTruthy();
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';
const isPositive = ({ value }: { value: string | bigint | number }) =>

View File

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

View File

@ -9,7 +9,7 @@ import { Icon } from '../icon';
import classnames from 'classnames';
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 xs = 'px-2 py-0 text-sm';

View File

@ -1,5 +1,7 @@
import classNames from 'classnames';
import type { HTMLAttributes } from 'react';
import { Intent } from '../../utils/intent';
import { Notification } from '../notification';
interface InputErrorProps extends HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
@ -7,6 +9,40 @@ interface InputErrorProps extends HTMLAttributes<HTMLDivElement> {
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 = ({
intent = 'danger',
children,

View File

@ -4,13 +4,20 @@ import classNames from 'classnames';
import type { ReactNode } from 'react';
import { Intent } from '../../utils/intent';
import { Icon } from '../icon';
import type { ButtonSize } from '../button';
import { Button } from '../button';
type NotificationProps = {
intent: Intent;
message: ReactNode | string;
title?: string;
buttonProps?: { text: string; action: () => void; className?: string };
buttonProps?: {
text: string;
action: () => void;
className?: string;
dataTestId?: string;
size?: ButtonSize;
};
testId?: string;
className?: string;
};
@ -41,11 +48,20 @@ export const Notification = ({
{
'border-gray-700 dark:border-gray-300': intent === Intent.None,
'border-vega-blue': intent === Intent.Primary,
'border-vega-green dark:border-vega-green': intent === Intent.Success,
'border-yellow-500': intent === Intent.Warning,
'border-vega-green-550 dark:border-vega-green':
intent === Intent.Success,
'border-vega-orange': intent === Intent.Warning,
'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
)}
>
@ -55,7 +71,7 @@ export const Notification = ({
'text-gray-700 dark:text-gray-300': intent === Intent.None,
'text-vega-blue': intent === Intent.Primary,
'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,
},
'flex items-start mt-1'
@ -63,18 +79,19 @@ export const Notification = ({
>
<Icon size={4} name={getIcon(intent)} />
</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 && (
<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}
</div>
)}
<div>{message}</div>
<div className="text-sm">{message}</div>
{buttonProps && (
<Button
size="sm"
size={buttonProps.size || 'sm'}
onClick={buttonProps.action}
className={classNames('mt-2 px-6 py-3', buttonProps.className)}
className={classNames(buttonProps.className)}
data-testid={buttonProps.dataTestId}
>
{buttonProps.text}
</Button>

View File

@ -23,7 +23,8 @@ export const getIntentBackground = (intent?: Intent) => {
'bg-vega-pink dark:bg-vega-yellow': intent === Intent.Primary,
'bg-danger': intent === Intent.Danger,
'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,
};
};