Feat/1660 validate available margin (#1860)
* feat: deal ticket margin validation * feat: deal ticket margin validation - working solution * feat: deal ticket margin validation - add int test * feat: deal ticket margin validation - improve int test * feat: deal ticket margin validation - improve int test * feat: deal ticket margin validation - fix unit test * feat: deal ticket margin validation - improve case when no account * feat: deal ticket margin validation - fix unit test * feat: deal ticket margin validation - fix int test * feat: deal ticket margin validation - fix int test * feat: deal ticket margin validation - fix int test Co-authored-by: maciek <maciek@vegaprotocol.io>
This commit is contained in:
parent
75db1d3ec6
commit
5ecf7d6a9f
@ -1,15 +1,29 @@
|
||||
import { Schema as Types } from '@vegaprotocol/types';
|
||||
|
||||
export const generatePartyMarketData = () => {
|
||||
return {
|
||||
party: {
|
||||
id: '2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
|
||||
accounts: [
|
||||
{
|
||||
type: 'General',
|
||||
type: Types.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '1200000',
|
||||
asset: { id: 'fBTC', decimals: 5, __typename: 'Asset' },
|
||||
market: null,
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
{
|
||||
__typename: 'AccountBalance',
|
||||
type: Types.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '0.000000001',
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tUSD',
|
||||
name: 'usd',
|
||||
decimals: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
marginsConnection: { edges: null, __typename: 'MarginConnection' },
|
||||
positionsConnection: { edges: null, __typename: 'PositionConnection' },
|
||||
|
@ -68,19 +68,20 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
|
||||
const emptyString = ' - ';
|
||||
const step = toDecimal(market.positionDecimalPlaces);
|
||||
const order = watch();
|
||||
const { message: invalidText, isDisabled } = useOrderValidation({
|
||||
market,
|
||||
orderType: order.type,
|
||||
orderTimeInForce: order.timeInForce,
|
||||
fieldErrors: errors,
|
||||
});
|
||||
const { submit, transaction, finalizedOrder, Dialog } = useOrderSubmit();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const estMargin = useOrderMargin({
|
||||
order,
|
||||
market,
|
||||
partyId: pubKey || '',
|
||||
});
|
||||
const { message: invalidText, isDisabled } = useOrderValidation({
|
||||
market,
|
||||
orderType: order.type,
|
||||
orderTimeInForce: order.timeInForce,
|
||||
fieldErrors: errors,
|
||||
estMargin,
|
||||
});
|
||||
const { submit, transaction, finalizedOrder, Dialog } = useOrderSubmit();
|
||||
|
||||
const { data: partyBalance } = usePartyBalanceQuery({
|
||||
variables: { partyId: pubKey || '' },
|
||||
|
@ -3,7 +3,9 @@ import {
|
||||
MarketTradingMode,
|
||||
AuctionTrigger,
|
||||
} from '@vegaprotocol/types';
|
||||
import { generateEstimateOrder } from '../support/mocks/generate-fees';
|
||||
import { connectVegaWallet } from '../support/vega-wallet';
|
||||
import { aliasQuery } from '@vegaprotocol/cypress';
|
||||
|
||||
const orderSizeField = 'order-size';
|
||||
const orderPriceField = 'order-price';
|
||||
@ -201,7 +203,7 @@ describe('must submit order', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
describe('deal ticket validation', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
beforeEach(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.visit('/markets/market-0');
|
||||
cy.wait('@Market');
|
||||
@ -441,3 +443,40 @@ describe('suspended market validation', { tags: '@regression' }, () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('margin required validation', { tags: '@regression' }, () => {
|
||||
before(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockGQL((req) => {
|
||||
aliasQuery(
|
||||
req,
|
||||
'EstimateOrder',
|
||||
generateEstimateOrder({
|
||||
estimateOrder: {
|
||||
marginLevels: { __typename: 'MarginLevels', initialLevel: '1000' },
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
cy.visit('/markets/market-0');
|
||||
connectVegaWallet();
|
||||
cy.wait('@Market');
|
||||
});
|
||||
|
||||
it('should display info and button for deposit', () => {
|
||||
cy.getByTestId('place-order').should('be.disabled');
|
||||
cy.getByTestId('dealticket-error-message').should(
|
||||
'contain.text',
|
||||
"You don't have enough margin available to open this position"
|
||||
);
|
||||
cy.getByTestId('dealticket-error-message').should(
|
||||
'contain.text',
|
||||
'0.01000 tBTC currently required, 0.00100 tBTC available'
|
||||
);
|
||||
cy.getByTestId('deal-ticket-deposit-dialog-button').click();
|
||||
cy.getByTestId('dialog-content')
|
||||
.find('h1')
|
||||
.eq(0)
|
||||
.should('have.text', 'Deposit');
|
||||
});
|
||||
});
|
||||
|
98
apps/trading-e2e/src/support/mocks/generate-fees.ts
Normal file
98
apps/trading-e2e/src/support/mocks/generate-fees.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import merge from 'lodash/merge';
|
||||
import { Schema as Types } from '@vegaprotocol/types';
|
||||
import type {
|
||||
EstimateOrderQuery,
|
||||
MarketMarkPriceQuery,
|
||||
PartyBalanceQuery,
|
||||
PartyMarketDataQuery,
|
||||
} from '@vegaprotocol/deal-ticket';
|
||||
|
||||
const estimateOrderMock: EstimateOrderQuery = {
|
||||
estimateOrder: {
|
||||
__typename: 'OrderEstimate',
|
||||
totalFeeAmount: '0.0006',
|
||||
fee: {
|
||||
__typename: 'TradeFee',
|
||||
makerFee: '0.0001',
|
||||
infrastructureFee: '0.0002',
|
||||
liquidityFee: '0.0003',
|
||||
},
|
||||
marginLevels: { __typename: 'MarginLevels', initialLevel: '1' },
|
||||
},
|
||||
};
|
||||
|
||||
export const generateEstimateOrder = (
|
||||
override?: PartialDeep<EstimateOrderQuery>
|
||||
) => {
|
||||
return merge(estimateOrderMock, override);
|
||||
};
|
||||
|
||||
const marketMarkPriceMock: MarketMarkPriceQuery = {
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
decimalPlaces: 5,
|
||||
data: {
|
||||
__typename: 'MarketData',
|
||||
markPrice: '100',
|
||||
market: { __typename: 'Market', id: 'market-0' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const generateMarkPrice = () => {
|
||||
return marketMarkPriceMock;
|
||||
};
|
||||
|
||||
const partyBalanceMock: PartyBalanceQuery = {
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
accounts: [
|
||||
{
|
||||
__typename: 'AccountBalance',
|
||||
type: Types.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '100',
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
name: 'BTC',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const generatePartyBalance = () => {
|
||||
return partyBalanceMock;
|
||||
};
|
||||
|
||||
export const generatePartyMarketData = (): PartyMarketDataQuery => {
|
||||
return {
|
||||
party: {
|
||||
id: Cypress.env('VEGA_PUBLIC_KEY'),
|
||||
accounts: [
|
||||
{
|
||||
type: Types.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '1200000',
|
||||
asset: { id: 'fBTC', decimals: 5, __typename: 'Asset' },
|
||||
market: null,
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
{
|
||||
__typename: 'AccountBalance',
|
||||
type: Types.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '100',
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
marginsConnection: { edges: null, __typename: 'MarginConnection' },
|
||||
__typename: 'Party',
|
||||
},
|
||||
};
|
||||
};
|
@ -22,6 +22,12 @@ import { generateMargins, generatePositions } from './mocks/generate-positions';
|
||||
import { generateStatistics } from './mocks/generate-statistics';
|
||||
import { generateTrades } from './mocks/generate-trades';
|
||||
import { generateWithdrawals } from './mocks/generate-withdrawals';
|
||||
import {
|
||||
generateEstimateOrder,
|
||||
generateMarkPrice,
|
||||
generatePartyBalance,
|
||||
generatePartyMarketData,
|
||||
} from './mocks/generate-fees';
|
||||
|
||||
const mockTradingPage = (
|
||||
req: CyHttpMessages.IncomingHttpRequest,
|
||||
@ -96,6 +102,11 @@ const mockTradingPage = (
|
||||
aliasQuery(req, 'Candles', generateCandles());
|
||||
aliasQuery(req, 'Withdrawals', generateWithdrawals());
|
||||
aliasQuery(req, 'NetworkParamsQuery', generateNetworkParameters());
|
||||
aliasQuery(req, 'EstimateOrder', generateEstimateOrder());
|
||||
aliasQuery(req, 'MarketMarkPrice', generateMarkPrice());
|
||||
aliasQuery(req, 'PartyBalance', generatePartyBalance());
|
||||
aliasQuery(req, 'MarketPositions', generatePositions());
|
||||
aliasQuery(req, 'PartyMarketData', generatePartyMarketData());
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
@ -60,6 +60,12 @@ export function createClient(base?: string) {
|
||||
AccountUpdate: {
|
||||
keyFields: false,
|
||||
},
|
||||
Party: {
|
||||
keyFields: false,
|
||||
},
|
||||
Fees: {
|
||||
keyFields: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -41,6 +41,9 @@ function AppBody({ Component, pageProps }: AppProps) {
|
||||
const [theme, toggleTheme] = useThemeSwitcher();
|
||||
return (
|
||||
<ThemeContext.Provider value={theme}>
|
||||
<Head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</Head>
|
||||
<Title />
|
||||
<div className="h-full relative dark:bg-black dark:text-white z-0 grid grid-rows-[min-content,1fr,min-content]">
|
||||
<AppLoader>
|
||||
|
@ -0,0 +1,47 @@
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import { toBigNum } from '@vegaprotocol/react-helpers';
|
||||
import type { DealTicketMarketFragment } from '../deal-ticket/__generated___/DealTicket';
|
||||
import type { OrderMargin } from '../../hooks/use-order-margin';
|
||||
import { usePartyBalanceQuery, useSettlementAccount } from '../../hooks';
|
||||
|
||||
interface Props {
|
||||
market: DealTicketMarketFragment;
|
||||
estMargin: OrderMargin | null;
|
||||
}
|
||||
|
||||
export const useOrderMarginValidation = ({ market, estMargin }: Props) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const { data: partyBalance } = usePartyBalanceQuery({
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey,
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
|
||||
const settlementAccount = useSettlementAccount(
|
||||
market.tradableInstrument.instrument.product.settlementAsset.id,
|
||||
partyBalance?.party?.accounts || [],
|
||||
AccountType.ACCOUNT_TYPE_GENERAL
|
||||
);
|
||||
const balance = settlementAccount
|
||||
? toBigNum(
|
||||
settlementAccount.balance || 0,
|
||||
settlementAccount.asset.decimals || 0
|
||||
)
|
||||
: toBigNum('0', 0);
|
||||
const margin = toBigNum(estMargin?.margin || 0, 0);
|
||||
const { id, symbol, decimals } =
|
||||
market.tradableInstrument.instrument.product.settlementAsset;
|
||||
if (balance.isZero() || balance.isLessThan(margin)) {
|
||||
return {
|
||||
balance: balance.toString(),
|
||||
margin: margin.toString(),
|
||||
id,
|
||||
symbol,
|
||||
decimals,
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
@ -1,5 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
MarketState,
|
||||
@ -8,10 +10,11 @@ import {
|
||||
Schema,
|
||||
} from '@vegaprotocol/types';
|
||||
import type { ValidationProps } from './use-order-validation';
|
||||
import { marketTranslations } from './use-order-validation';
|
||||
import { useOrderValidation } from './use-order-validation';
|
||||
import { marketTranslations, useOrderValidation } from './use-order-validation';
|
||||
import { ERROR_SIZE_DECIMAL } from './validate-size';
|
||||
import type { DealTicketMarketFragment } from '../deal-ticket/__generated___/DealTicket';
|
||||
import * as OrderMarginValidation from './use-order-margin-validation';
|
||||
import { ValidateMargin } from './validate-margin';
|
||||
|
||||
jest.mock('@vegaprotocol/wallet');
|
||||
|
||||
@ -73,6 +76,15 @@ const defaultOrder = {
|
||||
step: 0.1,
|
||||
orderType: Schema.OrderType.TYPE_MARKET,
|
||||
orderTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
estMargin: {
|
||||
margin: '0,000001',
|
||||
totalFees: '0,000006',
|
||||
fees: {
|
||||
makerFee: '0,000003',
|
||||
liquidityFee: '0,000002',
|
||||
infrastructureFee: '0,000001',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ERROR = {
|
||||
@ -99,16 +111,29 @@ function setup(
|
||||
) {
|
||||
const mockUseVegaWallet = useVegaWallet as jest.Mock;
|
||||
mockUseVegaWallet.mockReturnValue({ ...defaultWalletContext, context });
|
||||
return renderHook(() => useOrderValidation({ ...defaultOrder, ...props }));
|
||||
return renderHook(() => useOrderValidation({ ...defaultOrder, ...props }), {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
}
|
||||
|
||||
describe('useOrderValidation', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('Returns empty string when given valid data', () => {
|
||||
jest
|
||||
.spyOn(OrderMarginValidation, 'useOrderMarginValidation')
|
||||
.mockReturnValue(false);
|
||||
|
||||
const { result } = setup();
|
||||
expect(result.current).toStrictEqual({ isDisabled: false, message: `` });
|
||||
});
|
||||
|
||||
it('Returns an error message when no keypair found', () => {
|
||||
jest
|
||||
.spyOn(OrderMarginValidation, 'useOrderMarginValidation')
|
||||
.mockReturnValue(false);
|
||||
const { result } = setup(defaultOrder, { pubKey: null });
|
||||
expect(result.current).toStrictEqual({ isDisabled: false, message: `` });
|
||||
});
|
||||
@ -239,4 +264,28 @@ describe('useOrderValidation', () => {
|
||||
message: ERROR.FIELD_PRICE_STEP_DECIMAL,
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns an error message when the estimated margin is higher than collateral', async () => {
|
||||
const invalidatedMockValue = {
|
||||
balance: '0000000,1',
|
||||
margin: '000,1',
|
||||
id: 'instrument-id',
|
||||
symbol: 'asset-symbol',
|
||||
decimals: 5,
|
||||
};
|
||||
jest
|
||||
.spyOn(OrderMarginValidation, 'useOrderMarginValidation')
|
||||
.mockReturnValue(invalidatedMockValue);
|
||||
const { result } = setup({});
|
||||
|
||||
expect(result.current.isDisabled).toBe(true);
|
||||
|
||||
const testElement = <ValidateMargin {...invalidatedMockValue} />;
|
||||
expect((result.current.message as React.ReactElement)?.props).toEqual(
|
||||
testElement.props
|
||||
);
|
||||
expect((result.current.message as React.ReactElement)?.type).toEqual(
|
||||
testElement.type
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -15,6 +15,9 @@ import { ERROR_SIZE_DECIMAL } from './validate-size';
|
||||
import { MarketDataGrid } from '../trading-mode-tooltip';
|
||||
import { compileGridData } from '../trading-mode-tooltip/compile-grid-data';
|
||||
import type { DealTicketMarketFragment } from '../deal-ticket/__generated___/DealTicket';
|
||||
import { ValidateMargin } from './validate-margin';
|
||||
import type { OrderMargin } from '../../hooks/use-order-margin';
|
||||
import { useOrderMarginValidation } from './use-order-margin-validation';
|
||||
|
||||
export const isMarketInAuction = (market: DealTicketMarketFragment) => {
|
||||
return [
|
||||
@ -30,6 +33,7 @@ export type ValidationProps = {
|
||||
orderType: Schema.OrderType;
|
||||
orderTimeInForce: Schema.OrderTimeInForce;
|
||||
fieldErrors?: FieldErrors<OrderSubmissionBody['orderSubmission']>;
|
||||
estMargin: OrderMargin | null;
|
||||
};
|
||||
|
||||
export const marketTranslations = (marketState: MarketState) => {
|
||||
@ -46,6 +50,7 @@ export const useOrderValidation = ({
|
||||
fieldErrors = {},
|
||||
orderType,
|
||||
orderTimeInForce,
|
||||
estMargin,
|
||||
}: ValidationProps): {
|
||||
message: React.ReactNode | string;
|
||||
isDisabled: boolean;
|
||||
@ -53,6 +58,8 @@ export const useOrderValidation = ({
|
||||
const { pubKey } = useVegaWallet();
|
||||
const minSize = toDecimal(market.positionDecimalPlaces);
|
||||
|
||||
const isInvalidOrderMargin = useOrderMarginValidation({ market, estMargin });
|
||||
|
||||
const { message, isDisabled } = useMemo(() => {
|
||||
if (!pubKey) {
|
||||
return { message: t('No public key selected'), isDisabled: true };
|
||||
@ -266,6 +273,13 @@ export const useOrderValidation = ({
|
||||
};
|
||||
}
|
||||
|
||||
if (isInvalidOrderMargin) {
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: <ValidateMargin {...isInvalidOrderMargin} />,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
MarketTradingMode.TRADING_MODE_BATCH_AUCTION,
|
||||
@ -291,6 +305,7 @@ export const useOrderValidation = ({
|
||||
fieldErrors?.price?.type,
|
||||
orderType,
|
||||
orderTimeInForce,
|
||||
isInvalidOrderMargin,
|
||||
]);
|
||||
|
||||
return { message, isDisabled };
|
||||
|
@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { formatNumber, t } from '@vegaprotocol/react-helpers';
|
||||
import { Button, Dialog } from '@vegaprotocol/ui-toolkit';
|
||||
import { useState } from 'react';
|
||||
import { DepositContainer } from '@vegaprotocol/deposits';
|
||||
|
||||
interface Props {
|
||||
margin: string;
|
||||
symbol: string;
|
||||
id: string;
|
||||
balance: string;
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
export const ValidateMargin = ({
|
||||
margin,
|
||||
symbol,
|
||||
id,
|
||||
balance,
|
||||
decimals,
|
||||
}: Props) => {
|
||||
const [depositDialog, setDepositDialog] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex flex-col center pb-3"
|
||||
data-testid="deal-ticket-margin-invalidated"
|
||||
>
|
||||
<p>
|
||||
{t("You don't have enough margin available to open this position.")}
|
||||
</p>
|
||||
<p>
|
||||
{`${formatNumber(margin, decimals)} ${symbol} ${t(
|
||||
'currently required'
|
||||
)}, ${formatNumber(balance, decimals)} ${symbol} ${t('available')}`}
|
||||
</p>
|
||||
<Button
|
||||
className="center mt-2"
|
||||
variant="default"
|
||||
size="xs"
|
||||
data-testid="deal-ticket-deposit-dialog-button"
|
||||
onClick={() => setDepositDialog(true)}
|
||||
>
|
||||
{t('Deposit')} {symbol}
|
||||
</Button>
|
||||
</div>
|
||||
<Dialog open={depositDialog} onChange={setDepositDialog}>
|
||||
<h1 className="text-2xl mb-4">{t('Deposit')}</h1>
|
||||
<DepositContainer assetId={id} />
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
@ -52,11 +52,16 @@ export const DealTicket = ({
|
||||
defaultValues: getDefaultOrder(market),
|
||||
});
|
||||
const order = watch();
|
||||
|
||||
const feeDetails = useFeeDealTicketDetails(order, market);
|
||||
const details = getFeeDetailsValues(feeDetails);
|
||||
|
||||
const { message, isDisabled: disabled } = useOrderValidation({
|
||||
market,
|
||||
orderType: order.type,
|
||||
orderTimeInForce: order.timeInForce,
|
||||
fieldErrors: errors,
|
||||
estMargin: feeDetails.estMargin,
|
||||
});
|
||||
const isDisabled = transactionStatus === 'pending' || disabled;
|
||||
|
||||
@ -101,9 +106,6 @@ export const DealTicket = ({
|
||||
}
|
||||
}, [marketPriceFormatted, order.type, setValue]);
|
||||
|
||||
const feeDetails = useFeeDealTicketDetails(order, market);
|
||||
const details = getFeeDetailsValues(feeDetails);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="p-4" noValidate>
|
||||
<Controller
|
||||
|
@ -1 +1,2 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import 'jest-canvas-mock';
|
||||
|
@ -29,7 +29,7 @@ export function Dialog({
|
||||
size = 'small',
|
||||
}: DialogProps) {
|
||||
const contentClasses = classNames(
|
||||
'fixed top-0 left-0 z-20 flex justify-center items-start overflow-scroll',
|
||||
'fixed top-0 left-0 z-20 flex justify-center items-start overflow-auto',
|
||||
'w-full h-full'
|
||||
);
|
||||
const wrapperClasses = classNames(
|
||||
|
Loading…
Reference in New Issue
Block a user