feat(trading): deposit flow deal ticket (#2874)
This commit is contained in:
parent
7017e24adf
commit
55d6dd4dce
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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>
|
||||
|
@ -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 (
|
||||
|
@ -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' },
|
||||
];
|
||||
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
|
@ -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',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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'
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 () => {
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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 }) =>
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user