Compare commits

...
Author SHA1 Message Date
Matthew Russell ea41e55791 chore: update checkbox test 2023-08-22 11:30:34 +01:00
Matthew Russell 1aa275ccbd chore: switch checkbox tick to vega version 2023-08-22 10:13:53 +01:00
Matthew Russell bb6eea1087 chore: switch components to use trading versions 2023-08-22 09:19:24 +01:00
Matthew Russell e2deab3b0c chore: re add original form elements 2023-08-22 09:17:10 +01:00
Matthew Russell 05eee59899 chore: rename dirs of form elements for trading 2023-08-22 09:17:10 +01:00
Bartłomiej Głownia e45aa65e21 feat: fix rich select height 2023-08-22 09:17:10 +01:00
Bartłomiej Głownia ad549038e7 feat: handle disable state, update inpout background and border 2023-08-22 09:17:10 +01:00
Bartłomiej Głownia ab05f19865 feat: add hasError to deal ticket inputs 2023-08-22 09:17:10 +01:00
Bartłomiej Głownia 41dbfcb9e3 feat: align deal ticket with new form styles 2023-08-22 09:17:10 +01:00
Bartłomiej Głownia 66eaa64da1 feat: update input and select styles 2023-08-22 09:17:10 +01:00
Bartłomiej Głownia 7aecb21604 feat: update radio button and checkbox styles 2023-08-22 09:17:10 +01:00
Ben ab4f4e9084 chore(trading): remove trading chart tests (#4582) 2023-08-22 08:29:47 +01:00
Ben fa1825ca64 chore(trading): remove settings test (#4580) 2023-08-22 08:29:13 +01:00
Edd 206c6c7207 fix(explorer): check linking type when summing stake 2023-08-21 18:38:32 +01:00
Sam Keen 5b0bd69710 fix(governance): better vote error handling (#4579) 2023-08-21 10:05:21 +01:00
daro-maj 546b710093 test(trading): split and skip deal ticket account tests (#4578) 2023-08-21 09:49:27 +02:00
Joe Tsang afbf62be84 chore(governance): add assertion for vote change (#4571) 2023-08-18 18:15:55 +03:00
MaciekandMatthew Russell ef26d03d36 chore(markets): distinguish between product types - add specs (#4564)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-08-17 17:27:15 +01:00
Ben 7471116ebf chore(trading): remove market selector test (#4572) 2023-08-17 17:08:27 +01:00
Matthew Russell 7142fc956c chore(positions): spec updates for positions table changes (#4568) 2023-08-17 13:07:14 +01:00
Matthew Russell 611f8d7491 feat(positions): table layout changes (#4558) 2023-08-17 11:07:48 +01:00
Sam Keen f3b72b894e feat(explorer): view 'Cancel Stop Order' Transactions (#4521) 2023-08-17 10:27:47 +01:00
daro-maj 140f16f637 test(trading): adjust tests edit and cancel orders (#4563) 2023-08-17 09:10:28 +01:00
92 changed files with 2338 additions and 1677 deletions
@@ -11,6 +11,7 @@ import { TxDetailsBatch } from './tx-batch';
import { TxDetailsChainEvent } from './tx-chain-event';
import { TxDetailsNodeVote } from './tx-node-vote';
import { TxDetailsOrderCancel } from './tx-order-cancel';
import { TxDetailsStopOrderCancel } from './tx-stop-order-cancel';
import { TxDetailsOrderAmend } from './tx-order-amend';
import { TxDetailsWithdrawSubmission } from './tx-withdraw-submission';
import { TxDetailsDelegate } from './tx-delegation';
@@ -85,6 +86,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsProtocolUpgrade;
case 'Cancel Order':
return TxDetailsOrderCancel;
case 'Stop Orders Cancellation':
return TxDetailsStopOrderCancel;
case 'Amend Order':
return TxDetailsOrderAmend;
case 'Validator Heartbeat':
@@ -0,0 +1,60 @@
import { t } from '@vegaprotocol/i18n';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import { MarketLink } from '../../links/';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import { CancelSummary } from '../../order-summary/order-cancellation';
import Hash from '../../links/hash';
import type { components } from '../../../../types/explorer';
export type StopOrderCancellationTransaction =
components['schemas']['v1StopOrdersCancellation'];
interface TxDetailsStopOrderCancelProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* Someone cancelled a stop order
*/
export const TxDetailsStopOrderCancel = ({
txData,
pubKey,
blockData,
}: TxDetailsStopOrderCancelProps) => {
if (!txData || !txData.command) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const command: StopOrderCancellationTransaction =
txData.command.stopOrdersCancellation;
const { marketId, stopOrderId } = command;
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TableRow modifier="bordered">
<TableCell>{t('Cancel stop order')}</TableCell>
<TableCell>
{stopOrderId ? (
<Hash text={stopOrderId} />
) : (
<CancelSummary orderId={stopOrderId} marketId={marketId} />
)}
</TableCell>
</TableRow>
{marketId ? (
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>
<TableCell>
<MarketLink id={marketId} />
</TableCell>
</TableRow>
) : null}
</TableWithTbody>
);
};
@@ -34,6 +34,7 @@ export type FilterOption =
| 'Protocol Upgrade'
| 'Register new Node'
| 'State Variable Proposal'
| 'Stop Orders Cancellation'
| 'Submit Oracle Data'
| 'Submit Order'
| 'Transfer Funds'
@@ -53,6 +54,7 @@ export const PrimaryFilterOptions: FilterOption[] = [
'Delegate',
'Liquidity Provision Order',
'Proposal',
'Stop Orders Cancellation',
'Submit Oracle Data',
'Submit Order',
'Transfer Funds',
@@ -29,6 +29,37 @@ fragment ExplorerPartyAssetsAccounts on AccountBalance {
}
}
fragment ExplorerPartyLinks on AccountBalance {
asset {
name
id
decimals
symbol
source {
__typename
... on ERC20 {
contractAddress
}
}
}
type
balance
market {
id
decimalPlaces
tradableInstrument {
instrument {
name
product {
... on Future {
quoteName
}
}
}
}
}
}
query ExplorerPartyAssets($partyId: ID!) {
partiesConnection(id: $partyId) {
edges {
@@ -48,9 +79,11 @@ query ExplorerPartyAssets($partyId: ID!) {
}
stakingSummary {
currentStakeAvailable
linkings(pagination: { first: 100 }) {
linkings(pagination: { last: 100 }) {
edges {
node {
type
status
amount
}
}
@@ -10,7 +10,7 @@ export type ExplorerPartyAssetsQueryVariables = Types.Exact<{
}>;
export type ExplorerPartyAssetsQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string, linkings: { __typename?: 'StakesConnection', edges?: Array<{ __typename?: 'StakeLinkingEdge', node: { __typename?: 'StakeLinking', amount: string } } | null> | null } }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } }, market?: { __typename?: 'Market', id: string, decimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } | null } } | null> | null } | null } }> } | null };
export type ExplorerPartyAssetsQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string, linkings: { __typename?: 'StakesConnection', edges?: Array<{ __typename?: 'StakeLinkingEdge', node: { __typename?: 'StakeLinking', type: Types.StakeLinkingType, status: Types.StakeLinkingStatus, amount: string } } | null> | null } }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } }, market?: { __typename?: 'Market', id: string, decimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } | null } } | null> | null } | null } }> } | null };
export const ExplorerPartyAssetsAccountsFragmentDoc = gql`
fragment ExplorerPartyAssetsAccounts on AccountBalance {
@@ -67,6 +67,8 @@ export const ExplorerPartyAssetsDocument = gql`
linkings(pagination: {first: 100}) {
edges {
node {
type
status
amount
}
}
@@ -111,4 +113,4 @@ export function useExplorerPartyAssetsLazyQuery(baseOptions?: Apollo.LazyQueryHo
}
export type ExplorerPartyAssetsQueryHookResult = ReturnType<typeof useExplorerPartyAssetsQuery>;
export type ExplorerPartyAssetsLazyQueryHookResult = ReturnType<typeof useExplorerPartyAssetsLazyQuery>;
export type ExplorerPartyAssetsQueryResult = Apollo.QueryResult<ExplorerPartyAssetsQuery, ExplorerPartyAssetsQueryVariables>;
export type ExplorerPartyAssetsQueryResult = Apollo.QueryResult<ExplorerPartyAssetsQuery, ExplorerPartyAssetsQueryVariables>;
@@ -42,9 +42,15 @@ export const PartyBlockStake = ({
linkedLength && linkedLength > 0
? p?.stakingSummary?.linkings?.edges
?.reduce((total, e) => {
return new BigNumber(total).plus(
new BigNumber(e?.node.amount || 0)
);
const accumulator = new BigNumber(total)
const diff = new BigNumber(e?.node.amount || 0)
if (e?.node.type === 'TYPE_LINK') {
return accumulator.plus(diff);
} else if (e?.node.type === 'TYPE_UNLINK') {
return accumulator.minus(diff);
} else {
return accumulator
}
}, new BigNumber(0))
.toString()
: '0';
@@ -220,6 +220,7 @@ describe(
cy.getByTestId(changeVoteButton).should('be.visible').click();
voteForProposal('for');
// 3001-VOTE-064
cy.getByTestId('user-voted-yes').should('exist');
getProposalInformationFromTable('Tokens for proposal')
.should('have.text', (1).toFixed(2))
.and('be.visible');
@@ -182,9 +182,10 @@ export function clickOnValidatorFromList(
} else {
cy.get(`[row-id="${validatorNumber}"]`)
.should('be.visible')
.find(stakeValidatorListName)
.as('validatorOnList');
cy.get('@validatorOnList').click();
.first()
.within(() => {
cy.get(stakeValidatorListName).click();
});
}
}
@@ -55,7 +55,7 @@ export const Proposal = ({
mostRecentlyEnactedAssociatedMarketProposal,
}: ProposalProps) => {
const { t } = useTranslation();
const { submit, Dialog, finalizedVote } = useVoteSubmit();
const { submit, Dialog, finalizedVote, transaction } = useVoteSubmit();
const { voteState, voteDatetime } = useUserVote(proposal?.id, finalizedVote);
if (!proposal) {
@@ -215,6 +215,7 @@ export const Proposal = ({
}
submit={submit}
dialog={Dialog}
transaction={transaction}
voteState={voteState}
voteDatetime={voteDatetime}
/>
@@ -0,0 +1,110 @@
import { render, screen } from '@testing-library/react';
import { VoteTransactionDialog } from './vote-transaction-dialog';
import { VoteState } from './use-user-vote';
import { VegaTxStatus } from '@vegaprotocol/wallet';
describe('VoteTransactionDialog', () => {
const mockTransactionDialog = jest.fn(({ title, content }) => (
<div>
<div>{title}</div>
<div>{content?.Complete}</div>
</div>
));
it('renders without crashing', () => {
render(
<VoteTransactionDialog
voteState={VoteState.Yes}
transaction={null}
TransactionDialog={mockTransactionDialog}
/>
);
expect(screen.getByTestId('vote-transaction-dialog')).toBeInTheDocument();
});
it('renders with txRequested title when voteState is Requested', () => {
render(
<VoteTransactionDialog
voteState={VoteState.Requested}
transaction={null}
TransactionDialog={mockTransactionDialog}
/>
);
expect(screen.getByText('txRequested')).toBeInTheDocument();
});
it('renders with votePending title when voteState is Pending', () => {
render(
<VoteTransactionDialog
voteState={VoteState.Pending}
transaction={null}
TransactionDialog={mockTransactionDialog}
/>
);
expect(screen.getByText('votePending')).toBeInTheDocument();
});
it('renders with no title when voteState is neither Requested nor Pending', () => {
render(
<VoteTransactionDialog
voteState={VoteState.Yes} // or any other state other than Requested or Pending
transaction={null}
TransactionDialog={mockTransactionDialog}
/>
);
expect(screen.queryByText('txRequested')).not.toBeInTheDocument();
expect(screen.queryByText('votePending')).not.toBeInTheDocument();
});
it('renders custom error message when voteState is Failed and error message exists', () => {
render(
<VoteTransactionDialog
voteState={VoteState.Failed}
transaction={{
error: { message: 'Custom error test message', name: 'blah' },
txHash: null,
signature: null,
status: VegaTxStatus.Error,
dialogOpen: false,
}}
TransactionDialog={mockTransactionDialog}
/>
);
expect(screen.getByText('Custom error test message')).toBeInTheDocument();
});
it('renders default error message when voteState is failed and no error message exists on the tx', () => {
render(
<VoteTransactionDialog
voteState={VoteState.Failed}
transaction={{
error: null,
txHash: null,
signature: null,
status: VegaTxStatus.Error,
dialogOpen: false,
}}
TransactionDialog={mockTransactionDialog}
/>
);
expect(screen.getByText('voteError')).toBeInTheDocument();
});
it('renders default ui (i.e. not error) when not in a failed state', () => {
render(
<VoteTransactionDialog
voteState={VoteState.Yes}
transaction={null}
TransactionDialog={mockTransactionDialog}
/>
);
expect(screen.queryByText('voteError')).not.toBeInTheDocument();
});
});
@@ -1,4 +1,4 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { fireEvent, render, screen } from '@testing-library/react';
import BigNumber from 'bignumber.js';
import { VoteButtons } from './vote-buttons';
import { VoteState } from './use-user-vote';
@@ -24,6 +24,7 @@ describe('Vote buttons', () => {
currentStakeAvailable={new BigNumber(1)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -47,6 +48,7 @@ describe('Vote buttons', () => {
currentStakeAvailable={new BigNumber(1)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -81,6 +83,7 @@ describe('Vote buttons', () => {
currentStakeAvailable={new BigNumber(1)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -105,6 +108,7 @@ describe('Vote buttons', () => {
currentStakeAvailable={new BigNumber(0)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -132,6 +136,7 @@ describe('Vote buttons', () => {
currentStakeAvailable={new BigNumber(1)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -159,6 +164,7 @@ describe('Vote buttons', () => {
currentStakeAvailable={new BigNumber(10)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -183,6 +189,7 @@ describe('Vote buttons', () => {
currentStakeAvailable={new BigNumber(10)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -17,7 +17,7 @@ import { VoteState } from './use-user-vote';
import { ProposalMinRequirements, ProposalUserAction } from '../shared';
import { VoteTransactionDialog } from './vote-transaction-dialog';
import { useVoteButtonsQuery } from './__generated__/Stake';
import type { DialogProps } from '@vegaprotocol/wallet';
import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet';
interface VoteButtonsContainerProps {
voteState: VoteState | null;
@@ -27,6 +27,7 @@ interface VoteButtonsContainerProps {
minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null | undefined;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
transaction: VegaTxState | null;
dialog: (props: DialogProps) => JSX.Element;
className?: string;
}
@@ -67,6 +68,7 @@ export const VoteButtons = ({
minVoterBalance,
spamProtectionMinTokens,
submit,
transaction,
dialog: Dialog,
}: VoteButtonsProps) => {
const { t } = useTranslation();
@@ -208,7 +210,11 @@ export const VoteButtons = ({
</p>
)
)}
<VoteTransactionDialog voteState={voteState} TransactionDialog={Dialog} />
<VoteTransactionDialog
voteState={voteState}
transaction={transaction}
TransactionDialog={Dialog}
/>
</>
);
};
@@ -12,7 +12,7 @@ import { VoteButtonsContainer } from './vote-buttons';
import { SubHeading } from '../../../../components/heading';
import { ProposalType } from '../proposal/proposal';
import type { VoteValue } from '@vegaprotocol/types';
import type { DialogProps } from '@vegaprotocol/wallet';
import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { VoteState } from './use-user-vote';
@@ -22,6 +22,7 @@ interface VoteDetailsProps {
minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null | undefined;
proposalType: ProposalType | null;
transaction: VegaTxState | null;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
dialog: (props: DialogProps) => JSX.Element;
voteState: VoteState | null;
@@ -34,6 +35,7 @@ export const VoteDetails = ({
spamProtectionMinTokens,
proposalType,
submit,
transaction,
dialog,
voteState,
voteDatetime,
@@ -228,6 +230,7 @@ export const VoteDetails = ({
spamProtectionMinTokens={spamProtectionMinTokens}
className="flex"
submit={submit}
transaction={transaction}
dialog={dialog}
/>
)
@@ -1,9 +1,10 @@
import { t } from '@vegaprotocol/i18n';
import { VoteState } from './use-user-vote';
import type { DialogProps } from '@vegaprotocol/wallet';
import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet';
interface VoteTransactionDialogProps {
voteState: VoteState;
transaction: VegaTxState | null;
TransactionDialog: (props: DialogProps) => JSX.Element;
}
@@ -20,12 +21,15 @@ const dialogTitle = (voteState: VoteState): string | undefined => {
export const VoteTransactionDialog = ({
voteState,
transaction,
TransactionDialog,
}: VoteTransactionDialogProps) => {
// Render a custom message if the voting fails otherwise
// pass undefined so that the default vega transaction dialog UI gets used
const customMessage =
voteState === VoteState.Failed ? <p>{t('voteError')}</p> : undefined;
voteState === VoteState.Failed ? (
<p>{transaction?.error?.message || t('voteError')}</p>
) : undefined;
return (
<div data-testid="vote-transaction-dialog">
@@ -1,136 +0,0 @@
import {
AuctionTrigger,
MarketState,
MarketTradingMode,
} from '@vegaprotocol/types';
describe('markets selector', { tags: '@smoke' }, () => {
const list = 'market-selector-list';
const searchInput = 'search-term';
beforeEach(() => {
cy.window().then((window) => {
window.localStorage.setItem('marketId', 'market-1');
});
cy.setOnBoardingViewed();
cy.mockTradingPage(
MarketState.STATE_ACTIVE,
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
cy.mockSubscription();
cy.visit('/');
cy.wait('@Markets');
cy.wait('@MarketsData');
});
// 6001-MARK-066
it('can open popover to view markets', () => {
cy.getByTestId('market-selector').should('not.exist');
cy.getByTestId('header-title').should('be.visible').click();
cy.getByTestId('market-selector').should('be.visible');
});
// need function keyword as we need 'this' to access market data
it('displays data as expected', () => {
// TODO: load data from mocks in. Using alias and wrap intermittently fails
const data = [
{
code: 'SOLUSD',
markPrice: '84.41',
vol: '0.00',
productType: 'Futr',
},
{
code: 'ETHBTC.QM21',
markPrice: '46,126.90058',
vol: '0.00',
productType: 'Futr',
},
{
code: 'BTCUSD.MF21',
markPrice: '46,126.90058',
vol: '0.00',
productType: 'Futr',
},
{
code: 'AAPL.MF21',
markPrice: '46,126.90058',
vol: '0.00',
productType: 'Futr',
},
];
cy.getByTestId('header-title').should('be.visible').click();
cy.getByTestId(list)
.find('a')
.each((item, i) => {
const market = data[i];
// 6001-MARK-021
// 6001-MARK-022
expect(item.find('h3').text()).equals(
`${market.code} ${market.productType}`
);
expect(
item.find('[data-testid="market-selector-volume"]').text()
).contains(market.vol);
// 6001-MARK-024
expect(
item.find('[data-testid="market-selector-price"]').text()
).contains(market.markPrice);
// 6001-MARK-025
expect(item.find('[data-testid="sparkline-svg"]')).to.not.exist;
});
});
it('can use the filter options', () => {
cy.getByTestId('header-title').should('be.visible').click();
// 6001-MARK-027
// product type
cy.getByTestId('product-Spot').click();
cy.getByTestId(list).contains('Spot markets coming soon.');
cy.getByTestId('product-Perpetual').click();
cy.getByTestId(list).contains('Perpetual markets coming soon.');
cy.getByTestId('product-Future').click();
cy.getByTestId(list).find('a').should('have.length', 4);
// 6001-MARK-029
cy.getByTestId(searchInput).clear().type('btc');
cy.getByTestId(list).find('a').should('have.length', 2);
cy.getByTestId(list).find('a').eq(1).contains('BTCUSD.MF21');
cy.getByTestId(list).find('a').eq(0).contains('ETHBTC.QM21');
cy.getByTestId(searchInput).clear();
cy.getByTestId(list).find('a').should('have.length', 4);
});
it('can sort by by top gaining and top losing market', () => {
cy.getByTestId('header-title').should('be.visible').click();
// 6001-MARK-030
// 6001-MARK-031
// 6001-MARK-032
// 6001-MARK-033
cy.getByTestId(' sort-trigger').click();
cy.getByTestId('sort-item-Gained')
.contains('Top gaining')
.should('be.visible');
cy.getByTestId('sort-item-Lost')
.contains('Top losing')
.should('be.visible');
cy.getByTestId('sort-item-New')
.contains('New markets')
.should('be.visible');
});
it('can filter by settlement asset', () => {
cy.getByTestId('header-title').should('be.visible').click();
// 6001-MARK-028
cy.getByTestId('asset-trigger').click();
cy.getByTestId('asset-id-asset-3').contains('tBTC').click();
cy.getByTestId(list).find('a').should('have.length', 1);
cy.getByTestId(list).find('a').eq(0).contains('ETHBTC.QM21');
});
});
@@ -1,30 +0,0 @@
describe('Settings page', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/');
// Only click if not already active otherwise sidebar will close
cy.get('[data-testid="sidebar-content"]').then(($sidebarContent) => {
if ($sidebarContent.find('h2').text() !== 'Settings') {
cy.get('[data-testid="sidebar"] [data-testid="Settings"]').click();
}
});
});
it('telemetry checkbox should work well', () => {
const telemetrySwitch = '#switch-settings-telemetry-switch';
cy.get(telemetrySwitch).should('have.attr', 'data-state', 'unchecked');
cy.get(telemetrySwitch).click();
cy.get(telemetrySwitch).should('have.attr', 'data-state', 'checked');
cy.reload();
cy.get(telemetrySwitch).should('have.attr', 'data-state', 'checked');
cy.get(telemetrySwitch).click();
cy.get(telemetrySwitch).should('have.attr', 'data-state', 'unchecked');
cy.reload();
cy.get(telemetrySwitch).should('have.attr', 'data-state', 'unchecked');
});
});
@@ -1,186 +0,0 @@
interface ItemInfoType {
name: string;
infoText: string;
}
type CheckMenuItemsFnType = (
triggerSelector: string,
validTexts: string[],
clickItem?: string
) => void;
type CheckMenuItemCheckboxFnType = (
buttonText: string,
items: ItemInfoType[]
) => void;
const menuItemRadio = 'div[role="menuitemradio"]';
const menuItemCheckbox = 'div[role="menuitemcheckbox"]';
const button = 'button';
const indicatorInfo = '.indicator-info-wrapper';
const checkMenuItems: CheckMenuItemsFnType = (
triggerSelector,
validTexts,
clickItem
) => {
cy.get(triggerSelector).click();
cy.get(menuItemRadio)
.should('have.length', validTexts.length)
.each(($el, index) => {
const text = $el.text().trim();
expect(text).to.equal(validTexts[index]);
});
if (clickItem) {
cy.contains(menuItemRadio, clickItem).click();
cy.get(triggerSelector).click();
cy.get(`${menuItemRadio}[data-state="checked"]`)
.invoke('text')
.then((text: string) => {
expect(text.trim()).to.equal(clickItem);
});
}
};
const checkMenuItemCheckbox: CheckMenuItemCheckboxFnType = (
buttonText,
items
) => {
items.forEach((item) => {
cy.contains(button, buttonText).click();
cy.contains(menuItemCheckbox, item.name).click();
});
cy.contains(button, buttonText).click();
cy.get(menuItemCheckbox)
.should('have.length', items.length)
.each(($el, index) => {
const text = $el.text();
expect(text).to.equal(items[index].name);
});
items.forEach((item, index) => {
cy.get(indicatorInfo)
.eq(index + 1)
.invoke('text')
.should('eq', item.infoText);
});
cy.contains(button, buttonText).click({ force: true });
};
function getButtonSelectorByText(text: string): string {
return `${button}[aria-haspopup="menu"]:contains(${text})`;
}
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
describe(
'chart display options',
{ tags: '@smoke', testIsolation: true },
() => {
it('change time interval', () => {
// 6004-CHAR-001
checkMenuItems(
getButtonSelectorByText('Interval:'),
['1m', '5m', '15m', '1H', '6H', '1D'],
'1m'
);
});
it('change display type', () => {
// 6004-CHAR-002
// 6004-CHAR-003
checkMenuItems(
'[aria-label$="chart icon"]',
['Mountain', 'Candlestick', 'Line', 'OHLC'],
'Mountain'
);
});
it('Overlays', () => {
// 6004-CHAR-004
// 6004-CHAR-008
// 6004-CHAR-009
// 6004-CHAR-034
// 6004-CHAR-037
// 6004-CHAR-039
// 6004-CHAR-041
const overlayInfo: ItemInfoType[] = [
{
name: 'Bollinger bands',
infoText: 'Bollinger: Upper 174.78590Lower 173.38014',
},
{
name: 'Envelope',
infoText: 'Envelope: Upper 191.29000Lower 156.51000',
},
{ name: 'EMA', infoText: 'EMA: 174.06793' },
{ name: 'Moving average', infoText: 'Moving average: 174.08302' },
{
name: 'Price monitoring bounds',
infoText:
'Price Monitoring Bounds 1: Min 162.56291Max 182.96869Reference 172.47489',
},
];
checkMenuItemCheckbox('Overlays', overlayInfo);
});
it('Studies', () => {
// 6004-CHAR-005
// 6004-CHAR-006
// 6004-CHAR-007
// 6004-CHAR-042
// 6004-CHAR-045
// 6004-CHAR-047
// 6004-CHAR-049
// 6004-CHAR-051
const studyInfo: ItemInfoType[] = [
{
name: 'Eldar-ray',
infoText: 'Eldar-ray: Bull -0.08376Bear -0.58376',
},
{ name: 'Force index', infoText: 'Force index: 987.48858' },
{ name: 'MACD', infoText: 'MACD: S -0.06420D 0.00359MACD -0.06062' },
{ name: 'RSI', infoText: 'RSI: 47.08648' },
{ name: 'Volume', infoText: 'Volume: 55,000' },
];
cy.get(indicatorInfo).eq(1).realHover();
cy.get('.chart__wrapper [data-testid="split-view-view"]')
.last()
.find('[role="button"][title="Close"]')
.click({ force: true });
cy.get(indicatorInfo).should('have.length', 1);
checkMenuItemCheckbox('Studies', studyInfo);
});
it('price details', () => {
// 6004-CHAR-010
const expectedDateRegex = new RegExp(
/^\d{2}:\d{2} \d{2} [A-Za-z]{3} \d{4}$/
);
const expectedOhlc = `O 173.60000H 174.00000L 173.50000C 173.90000Change 0.60000(0.34%)`;
cy.get(indicatorInfo)
.eq(0)
.invoke('text')
.then((text) => {
const actualDate = text.slice(0, -67);
// eslint-disable-next-line no-console
console.log(actualDate);
const actualOhlc = text.slice(-67);
assert.isTrue(expectedDateRegex.test(actualDate));
assert.strictEqual(actualOhlc, expectedOhlc);
});
});
}
);
@@ -3,7 +3,7 @@ import { testOrderSubmission } from '../support/order-validation';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import { createOrder } from '../support/create-order';
describe('must submit order', { tags: '@smoke' }, () => {
describe.skip('must submit order', { tags: '@smoke' }, () => {
// 7002-SORD-039
before(() => {
cy.setVegaWallet();
@@ -0,0 +1,87 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import {
accountsQuery,
amendGeneralAccountBalance,
amendMarginAccountBalance,
} from '@vegaprotocol/mock';
describe.skip(
'account validation',
{ tags: '@regression', testIsolation: true },
() => {
describe('zero balance error', () => {
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
let accounts = accountsQuery();
accounts = amendMarginAccountBalance(accounts, 'market-0', '1000');
accounts = amendGeneralAccountBalance(accounts, 'market-0', '0');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('should show an error if your balance is zero', () => {
const accounts = accountsQuery();
amendMarginAccountBalance(accounts, 'market-0', '0');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
// 7002-SORD-060
cy.getByTestId('place-order').should('be.enabled');
// 7002-SORD-003
cy.getByTestId('deal-ticket-error-message-zero-balance').should(
'have.text',
'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');
});
});
describe('not enough balance warning', () => {
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
let accounts = accountsQuery();
accounts = amendMarginAccountBalance(accounts, 'market-0', '1000');
accounts = amendGeneralAccountBalance(accounts, 'market-0', '1');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.get('[data-testid="deal-ticket-form"]').then(($form) => {
if (!$form.length) {
cy.getByTestId('Order').click();
}
});
});
it('should display info and button for deposit', () => {
// 7002-SORD-003
// warning should show immediately
cy.getByTestId('deal-ticket-warning-margin').should(
'contain.text',
'You may not have enough margin available to open this position'
);
cy.getByTestId('deal-ticket-warning-margin').should(
'contain.text',
'You may not have enough margin available to open this position. 5.00 tDAI is currently required. You have only 0.01001 tDAI available.'
);
cy.getByTestId('deal-ticket-deposit-dialog-button').click();
cy.getByTestId('sidebar-content')
.find('h2')
.eq(0)
.should('have.text', 'Deposit');
});
});
}
);
@@ -8,7 +8,7 @@ import {
import type { OrderSubmission } from '@vegaprotocol/wallet';
import { createOrder } from '../support/create-order';
describe(
describe.skip(
'account validation',
{ tags: '@regression', testIsolation: true },
() => {
@@ -447,8 +447,7 @@ describe('amend and cancel order', { tags: '@smoke' }, () => {
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[data-testid="edit"]')
.should('have.text', 'Edit')
.find('[data-testid="icon-edit"]')
.then(($btn) => {
cy.wrap($btn).click();
cy.getByTestId('dialog-title').should('have.text', 'Edit order');
@@ -476,8 +475,7 @@ describe('amend and cancel order', { tags: '@smoke' }, () => {
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="cancel"]`)
.should('have.text', 'Cancel')
.find(`[data-testid="icon-cross"]`)
.then(($btn) => {
cy.wrap($btn).click({ force: true });
const order: OrderCancellation = {
@@ -514,8 +512,7 @@ describe('amend and cancel order', { tags: '@smoke' }, () => {
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[data-testid="edit"]')
.should('have.text', 'Edit')
.find('[data-testid="icon-edit"]')
.then(($btn) => {
cy.wrap($btn).click({ force: true });
cy.getByTestId('dialog-title').should('have.text', 'Edit order');
@@ -1,372 +0,0 @@
import { checkSorting, aliasGQLQuery } from '@vegaprotocol/cypress';
import { marketsDataQuery } from '@vegaprotocol/mock';
import { positionsQuery } from '@vegaprotocol/mock';
// #region consts
const closePosition = 'close-position';
const dialogCloseX = 'dialog-close';
const dialogContent = 'dialog-content';
const dropDownMenu = 'dropdown-menu';
const marketActionsContent = 'position-actions-content';
const positions = 'Positions';
const tabPositions = 'tab-positions';
const toastContent = 'toast-content';
const tooltipContent = 'tooltip-content';
// #endregion
describe('positions', { tags: '@smoke', testIsolation: true }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
});
it('renders positions on trading page', () => {
visitAndClickPositions();
// 7004-POSI-001
// 7004-POSI-002
validatePositionsDisplayed();
});
// TODO: move this to sim, its flakey
it.skip('renders positions on portfolio page', () => {
cy.mockGQL((req) => {
const positions = positionsQuery();
if (positions.positions?.edges) {
positions.positions.edges.push(
...positions.positions.edges.map((edge) => ({
...edge,
node: {
...edge.node,
party: {
...edge.node.party,
id: 'vega-1',
},
},
}))
);
}
aliasGQLQuery(req, 'Positions', positions);
});
visitAndClickPositions();
// 7004-POSI-001
// 7004-POSI-002
validatePositionsDisplayed(true);
});
it('Close my position', () => {
visitAndClickPositions();
cy.getByTestId(closePosition).first().click();
// 7004-POSI-010
cy.getByTestId(toastContent).should(
'contain.text',
'Awaiting confirmation'
);
});
});
describe('positions', { tags: '@regression', testIsolation: true }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
});
it('rows should be displayed despite errors', () => {
const errors = [
{
message:
'no market data for market: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
path: ['marketsConnection', 'edges'],
extensions: {
code: 13,
type: 'Internal',
},
},
];
const marketData = marketsDataQuery();
const edges = marketData.marketsConnection?.edges.map((market) => {
const replace =
market.node.data?.market.id === 'market-2' ? null : market.node.data;
return { ...market, node: { ...market.node, data: replace } };
});
const overrides = {
...marketData,
marketsConnection: { ...marketData.marketsConnection, edges },
};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'MarketsData', overrides, errors);
});
cy.visit('/#/markets/market-0');
const emptyCells = [
'notional',
'markPrice',
'currentLeverage',
'averageEntryPrice',
];
cy.getByTestId(tabPositions)
.first()
.within(() => {
cy.get(
'[row-id="02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65-market-2"]'
)
.eq(0)
.within(() => {
emptyCells.forEach((cell) => {
cy.get(`[col-id="${cell}"]`).should('contain.text', '-');
});
});
});
});
it('error message should be displayed', () => {
const errors = [
{
message:
'no market data for asset: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
path: ['assets', 'edges'],
extensions: {
code: 13,
type: 'Internal',
},
},
];
const overrides = {
marketsConnection: { edges: [] },
};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'MarketsData', overrides, errors);
});
cy.visit('/#/markets/market-0');
cy.getByTestId(tabPositions).contains('no market data');
});
it('sorting by Market', () => {
visitAndClickPositions();
const marketsSortedDefault = [
'AAPL.MF21',
'BTCUSD.MF21',
'ETHBTC.QM21',
'SOLUSD',
];
const marketsSortedAsc = [
'AAPL.MF21',
'BTCUSD.MF21',
'ETHBTC.QM21',
'SOLUSD',
];
const marketsSortedDesc = [
'SOLUSD',
'ETHBTC.QM21',
'BTCUSD.MF21',
'AAPL.MF21',
];
cy.getByTestId(positions).click();
// 7004-POSI-003
checkSorting(
'marketName',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc,
' [data-testid="market-code"]'
);
});
// let elementWidth: number;
it('Resize column', () => {
visitAndClickPositions();
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="marketName"]`)
.find('.ag-header-cell-resize')
.realMouseDown()
.realMouseMove(250, 0)
.realMouseUp();
});
// 7004-POSI-006
cy.get(`[col-id="marketName"]`)
.invoke('width')
.should('be.greaterThan', 250);
});
// This test depends on the previous one
it('Has persisted column widths', () => {
const width = 400;
cy.window().then((win) => {
win.localStorage.setItem(
'vega_positions_store',
JSON.stringify({
state: {
gridStore: {
columnState: [{ colId: 'marketName', width }],
},
},
})
);
});
visitAndClickPositions();
// 7004-POSI-012
cy.get('.ag-center-cols-container .ag-row')
.first()
.find('[col-id="marketName"]')
.invoke('outerWidth')
.should('equal', width);
});
it('Scroll horizontally', () => {
visitAndClickPositions();
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="marketName"]`)
.find('.ag-header-cell-resize')
.realMouseDown()
.realMouseMove(400, 0)
.realMouseUp();
});
cy.get('[col-id="marketName"]').should('be.visible');
cy.get('.ag-body-horizontal-scroll-viewport').realMouseWheel({
deltaX: 500,
});
// 7004-POSI-004
cy.get('[col-id="unrealisedPNL"]').should('be.visible');
});
it('Drag and drop columns', () => {
visitAndClickPositions();
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
cy.get('[col-id="marketName"]')
.realMouseDown()
.realMouseMove(700, 15)
.realMouseUp();
// 7004-POSI-005
cy.get('[col-id="marketName"]').should(($element) => {
const attributeValue = $element.attr('aria-colindex');
expect(attributeValue).not.to.equal('1');
});
});
it('I can see warnings', () => {
visitAndClickPositions();
cy.get('[col-id="openVolume"]')
.eq(3)
.within(() => {
cy.get('[aria-label="warning-sign icon"]')
.should('be.visible')
.realHover();
});
// 7004-POSI-011
cy.getByTestId(tooltipContent).should('be.visible');
});
it('Positive and Negative color change', () => {
cy.visit('/#/markets/market-0');
cy.getByTestId(positions).click();
// 7004-POSI-007
cy.get('.ag-center-cols-container').within(() => {
assertPNLColor(
'[col-id="realisedPNL"]',
'text-market-green-600',
'text-market-red'
);
});
cy.get('.ag-center-cols-container').within(() => {
assertPNLColor(
'[col-id="unrealisedPNL"]',
'text-market-green-600',
'text-market-red'
);
});
cy.get('.ag-center-cols-container').within(() => {
assertPNLColor(
'[col-id="openVolume"]',
'text-market-green-600',
'text-market-red'
);
});
});
it('View settlement asset', () => {
visitAndClickPositions();
cy.get('[col-id="asset"]')
.eq(3)
.within(() => {
cy.get('button[type="button"]').click();
});
// 7004-POSI-008
cy.getByTestId(dialogContent).should('be.visible');
cy.getByTestId(dialogCloseX).click();
cy.getByTestId(dropDownMenu).first().click();
cy.getByTestId(marketActionsContent).click();
// 7004-POSI-009
cy.getByTestId(dialogContent).should('be.visible');
});
});
function validatePositionsDisplayed(multiKey = false) {
cy.getByTestId('tab-positions').should('be.visible');
cy.getByTestId('tab-positions')
.get('.ag-center-cols-container .ag-row')
.eq(multiKey ? 3 : 1)
.within(() => {
cy.get('[col-id="marketName"]')
.should('be.visible')
.invoke('text')
.should('not.be.empty');
cy.get('[col-id="openVolume"]').should('not.be.empty');
// includes average entry price, mark price, realised PNL & leverage
cy.getByTestId('flash-cell').should('not.be.empty');
if (!multiKey) {
cy.get('[col-id="currentLeverage"]').should('contain.text', '2,767.3');
cy.get('[col-id="marginAccountBalance"]') // margin allocated
.should('contain.text', '0.01');
}
cy.get('[col-id="unrealisedPNL"]').should('not.be.empty');
cy.get('[col-id="notional"]').should('contain.text', '276,761.40348'); // Total tDAI position
cy.get('[col-id="realisedPNL"]').should('contain.text', '2.30'); // Total Realised PNL
cy.get('[col-id="unrealisedPNL"]').should('contain.text', '8.95'); // Total Unrealised PNL
});
cy.get('.ag-header-row [col-id="notional"]')
.should('contain.text', 'Notional')
.realHover();
cy.get('.ag-popup').should('contain.text', 'Mark price x open volume');
cy.getByTestId('close-position').should('be.visible').and('have.length', 3);
}
function assertPNLColor(
pnlSelector: string,
positiveClass: string,
negativeClass: string
) {
cy.get(pnlSelector).each(($el) => {
const value = parseFloat($el.text());
if (value > 0) {
cy.wrap($el).invoke('attr', 'class').should('contain', positiveClass);
} else if (value < 0) {
cy.wrap($el).invoke('attr', 'class').should('contain', negativeClass);
} else if (value == 0) {
cy.wrap($el)
.invoke('attr', 'class')
.should('not.contain', negativeClass, positiveClass);
} else {
throw new Error('Unexpected value');
}
});
}
function visitAndClickPositions() {
cy.visit('/#/markets/market-0');
cy.getByTestId(positions).click();
}
@@ -89,17 +89,15 @@ const MarketData = ({
? addDecimalsFormatNumber(vol, market.positionDecimalPlaces)
: '0.00';
const productType = market.tradableInstrument.instrument.product.__typename;
return (
<>
<div className="w-2/5" role="gridcell">
<h3 className="text-ellipsis text-sm lg:text-base whitespace-nowrap overflow-hidden">
{market.tradableInstrument.instrument.code}{' '}
{allProducts && (
<MarketProductPill
productType={
market.tradableInstrument.instrument.product.__typename
}
/>
{allProducts && productType && (
<MarketProductPill productType={productType} />
)}
</h3>
{mode && (
@@ -2,7 +2,7 @@ import { t } from '@vegaprotocol/i18n';
import uniqBy from 'lodash/uniqBy';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
import {
Input,
TradingInput,
TinyScroll,
VegaIcon,
VegaIconNames,
@@ -57,7 +57,7 @@ export const MarketSelector = ({
/>
<div className="text-sm grid grid-cols-[2fr_1fr_1fr] gap-1 ">
<div className="flex-1">
<Input
<TradingInput
onChange={(e) =>
setFilter((curr) => ({ ...curr, searchTerm: e.target.value }))
}
@@ -1,4 +1,4 @@
import { Checkbox } from '@vegaprotocol/ui-toolkit';
import { TradingCheckbox } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval';
@@ -7,7 +7,7 @@ export const TelemetryApproval = ({ helpText }: { helpText: string }) => {
return (
<div className="flex flex-col py-3">
<div className="mr-4" role="form">
<Checkbox
<TradingCheckbox
label={<span className="text-lg pl-1">{t('Share usage data')}</span>}
checked={isApproved}
name="telemetry-approval"
+31 -23
View File
@@ -9,13 +9,13 @@ import {
import { t } from '@vegaprotocol/i18n';
import {
Button,
FormGroup,
Input,
InputError,
RichSelect,
Select,
TradingFormGroup,
TradingInput,
TradingInputError,
TradingRichSelect,
TradingSelect,
Tooltip,
Checkbox,
TradingCheckbox,
} from '@vegaprotocol/ui-toolkit';
import type { Transfer } from '@vegaprotocol/wallet';
import { normalizeTransfer } from '@vegaprotocol/wallet';
@@ -130,12 +130,16 @@ export const TransferForm = ({
className="text-sm"
data-testid="transfer-form"
>
<FormGroup label="Vega key" labelFor="to-address">
<TradingFormGroup label="Vega key" labelFor="to-address">
<AddressField
pubKeys={pubKeys}
onChange={() => setValue('toAddress', '')}
select={
<Select {...register('toAddress')} id="to-address" defaultValue="">
<TradingSelect
{...register('toAddress')}
id="to-address"
defaultValue=""
>
<option value="" disabled={true}>
{t('Please select')}
</option>
@@ -147,10 +151,10 @@ export const TransferForm = ({
{pk}
</option>
))}
</Select>
</TradingSelect>
}
input={
<Input
<TradingInput
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="to-address"
@@ -171,12 +175,12 @@ export const TransferForm = ({
}
/>
{errors.toAddress?.message && (
<InputError forInput="to-address">
<TradingInputError forInput="to-address">
{errors.toAddress.message}
</InputError>
</TradingInputError>
)}
</FormGroup>
<FormGroup label="Asset" labelFor="asset">
</TradingFormGroup>
<TradingFormGroup label="Asset" labelFor="asset">
<Controller
control={control}
name="asset"
@@ -186,7 +190,7 @@ export const TransferForm = ({
},
}}
render={({ field }) => (
<RichSelect
<TradingRichSelect
data-testid="select-asset"
id={field.name}
name={field.name}
@@ -208,15 +212,17 @@ export const TransferForm = ({
}
/>
))}
</RichSelect>
</TradingRichSelect>
)}
/>
{errors.asset?.message && (
<InputError forInput="asset">{errors.asset.message}</InputError>
<TradingInputError forInput="asset">
{errors.asset.message}
</TradingInputError>
)}
</FormGroup>
<FormGroup label="Amount" labelFor="amount">
<Input
</TradingFormGroup>
<TradingFormGroup label="Amount" labelFor="amount">
<TradingInput
id="amount"
autoComplete="off"
appendElement={
@@ -239,11 +245,13 @@ export const TransferForm = ({
})}
/>
{errors.amount?.message && (
<InputError forInput="amount">{errors.amount.message}</InputError>
<TradingInputError forInput="amount">
{errors.amount.message}
</TradingInputError>
)}
</FormGroup>
</TradingFormGroup>
<div className="mb-4">
<Checkbox
<TradingCheckbox
name="include-transfer-fee"
disabled={!transferAmount}
label={
+3 -3
View File
@@ -1,4 +1,4 @@
import { Option } from '@vegaprotocol/ui-toolkit';
import { TradingOption } from '@vegaprotocol/ui-toolkit';
import type { AssetFieldsFragment } from './__generated__/Asset';
import classNames from 'classnames';
import { t } from '@vegaprotocol/i18n';
@@ -28,7 +28,7 @@ export const Balance = ({
export const AssetOption = ({ asset, balance }: AssetOptionProps) => {
return (
<Option key={asset.id} value={asset.id}>
<TradingOption key={asset.id} value={asset.id}>
<div className="flex flex-col items-start">
<div className="flex flex-row align-baseline gap-2">
<span>{asset.name}</span>{' '}
@@ -49,6 +49,6 @@ export const AssetOption = ({ asset, balance }: AssetOptionProps) => {
</span>
</div>
</div>
</Option>
</TradingOption>
);
};
@@ -2,28 +2,27 @@ import type { MouseEvent } from 'react';
import { useCallback } from 'react';
import get from 'lodash/get';
import { Pill } from '@vegaprotocol/ui-toolkit';
import type { Market } from '@vegaprotocol/types';
const productTypeMap = {
Future: 'Futr',
FutureProduct: 'Futr',
Spot: 'Spot',
SpotProduct: 'Spot',
Perpetual: 'Perp',
PerpetualProduct: 'Perp',
} as const;
export type ProductType = keyof typeof productTypeMap | undefined;
import {
ProductTypeShortName,
type Market,
type ProductType,
ProductTypeMapping,
} from '@vegaprotocol/types';
export const MarketProductPill = ({
productType,
}: {
productType?: ProductType;
productType: ProductType;
}) => {
return productType ? (
<Pill size="xxs" className="uppercase ml-0.5" title={productType}>
{productTypeMap[productType] || productType}
return (
<Pill
size="xxs"
className="uppercase ml-0.5"
title={ProductTypeMapping[productType]}
>
{ProductTypeShortName[productType]}
</Pill>
) : null;
);
};
interface MarketNameCellProps {
@@ -66,7 +65,7 @@ export const MarketNameCell = ({
<span data-testid="market-code" data-market-id={id}>
{value}
</span>
<MarketProductPill productType={productType} />
{productType && <MarketProductPill productType={productType} />}
</>
);
return onMarketClick && id ? (
@@ -15,7 +15,7 @@ import {
} from 'date-fns';
import { formatForInput } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { InputError } from '@vegaprotocol/ui-toolkit';
import { TradingInputError } from '@vegaprotocol/ui-toolkit';
const defaultValue: Schema.DateRange = {};
export interface DateRangeFilterProps extends IFilterParams {
@@ -195,7 +195,7 @@ export const DateRangeFilter = forwardRef(
}, [value, props]);
const notification = useMemo(() => {
const not = error ? <InputError>{error}</InputError> : null;
const not = error ? <TradingInputError>{error}</TradingInputError> : null;
return (
<div className="ag-filter-apply-panel flex min-h-[2rem]">{not}</div>
);
@@ -1,4 +1,8 @@
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
import {
TradingFormGroup,
TradingInput,
TradingInputError,
} from '@vegaprotocol/ui-toolkit';
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { DealTicketAmountProps } from './deal-ticket-amount';
@@ -22,17 +26,17 @@ export const DealTicketLimitAmount = ({
const renderError = () => {
if (sizeError) {
return (
<InputError testId="deal-ticket-error-message-size-limit">
<TradingInputError testId="deal-ticket-error-message-size-limit">
{sizeError}
</InputError>
</TradingInputError>
);
}
if (priceError) {
return (
<InputError testId="deal-ticket-error-message-price-limit">
<TradingInputError testId="deal-ticket-error-message-price-limit">
{priceError}
</InputError>
</TradingInputError>
);
}
@@ -43,7 +47,7 @@ export const DealTicketLimitAmount = ({
<div className="mb-2">
<div className="flex items-start gap-4">
<div className="flex-1">
<FormGroup
<TradingFormGroup
label={t('Size')}
labelFor="input-order-size-limit"
className="!mb-0"
@@ -59,8 +63,8 @@ export const DealTicketLimitAmount = ({
},
validate: validateAmount(sizeStep, 'Size'),
}}
render={({ field }) => (
<Input
render={({ field, fieldState }) => (
<TradingInput
id="input-order-size-limit"
className="w-full"
type="number"
@@ -68,15 +72,16 @@ export const DealTicketLimitAmount = ({
min={sizeStep}
data-testid="order-size"
onWheel={(e) => e.currentTarget.blur()}
hasError={!!fieldState.error}
{...field}
/>
)}
/>
</FormGroup>
</TradingFormGroup>
</div>
<div className="pt-7 leading-10">@</div>
<div className="pt-5 leading-10">@</div>
<div className="flex-1">
<FormGroup
<TradingFormGroup
labelFor="input-price-quote"
label={t(`Price (${quoteName})`)}
labelAlign="right"
@@ -93,19 +98,20 @@ export const DealTicketLimitAmount = ({
},
validate: validateAmount(priceStep, 'Price'),
}}
render={({ field }) => (
<Input
render={({ field, fieldState }) => (
<TradingInput
id="input-price-quote"
className="w-full"
type="number"
step={priceStep}
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
hasError={!!fieldState.error}
{...field}
/>
)}
/>
</FormGroup>
</TradingFormGroup>
</div>
</div>
{renderError()}
@@ -4,7 +4,11 @@ import {
validateAmount,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { Input, InputError, Tooltip } from '@vegaprotocol/ui-toolkit';
import {
TradingInput,
TradingInputError,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { isMarketInAuction } from '@vegaprotocol/markets';
import type { DealTicketAmountProps } from './deal-ticket-amount';
import { Controller } from 'react-hook-form';
@@ -33,7 +37,7 @@ export const DealTicketMarketAmount = ({
<div className="mb-2">
<div className="flex items-start gap-4">
<div className="flex-1">
<div className="mb-2 text-sm">{t('Size')}</div>
<div className="mb-2 text-xs">{t('Size')}</div>
<Controller
name="size"
control={control}
@@ -45,8 +49,8 @@ export const DealTicketMarketAmount = ({
},
validate: validateAmount(sizeStep, 'Size'),
}}
render={({ field }) => (
<Input
render={({ field, fieldState }) => (
<TradingInput
id="input-order-size-market"
className="w-full"
type="number"
@@ -54,12 +58,13 @@ export const DealTicketMarketAmount = ({
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
data-testid="order-size"
hasError={!!fieldState.error}
{...field}
/>
)}
/>
</div>
<div className="pt-7 leading-10">@</div>
<div className="pt-5 leading-10">@</div>
<div className="flex-1 text-sm text-right">
{inAuction && (
<Tooltip
@@ -72,7 +77,7 @@ export const DealTicketMarketAmount = ({
)}
<div
data-testid="last-price"
className={classNames('leading-10', { 'pt-7': !inAuction })}
className={classNames('leading-10', { 'pt-5': !inAuction })}
>
{priceFormatted && quoteName ? (
<>
@@ -85,12 +90,12 @@ export const DealTicketMarketAmount = ({
</div>
</div>
{sizeError && (
<InputError
<TradingInputError
intent="danger"
testId="deal-ticket-error-message-size-market"
>
{sizeError}
</InputError>
</TradingInputError>
)}
</div>
);
@@ -4,9 +4,9 @@ import type { OrderFormValues } from '../../hooks/use-form-values';
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
FormGroup,
Input,
InputError,
TradingFormGroup,
TradingInput,
TradingInputError,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
@@ -32,9 +32,9 @@ export const DealTicketSizeIceberg = ({
const renderPeakSizeError = () => {
if (peakSizeError) {
return (
<InputError testId="deal-ticket-peak-error-message-size-limit">
<TradingInputError testId="deal-ticket-peak-error-message-size-limit">
{peakSizeError}
</InputError>
</TradingInputError>
);
}
@@ -44,9 +44,9 @@ export const DealTicketSizeIceberg = ({
const renderMinimumSizeError = () => {
if (minimumVisibleSizeError) {
return (
<InputError testId="deal-ticket-minimum-error-message-size-limit">
<TradingInputError testId="deal-ticket-minimum-error-message-size-limit">
{minimumVisibleSizeError}
</InputError>
</TradingInputError>
);
}
@@ -57,7 +57,7 @@ export const DealTicketSizeIceberg = ({
<div className="mb-2">
<div className="flex items-center gap-4">
<div className="flex-1">
<FormGroup
<TradingFormGroup
label={
<Tooltip
description={
@@ -93,7 +93,7 @@ export const DealTicketSizeIceberg = ({
validate: validateAmount(sizeStep, 'peakSize'),
}}
render={({ field }) => (
<Input
<TradingInput
id="input-order-peak-size"
className="w-full"
type="number"
@@ -106,14 +106,14 @@ export const DealTicketSizeIceberg = ({
/>
)}
/>
</FormGroup>
</TradingFormGroup>
</div>
<div className="flex-0 items-center">
<div className="flex"></div>
<div className="flex"></div>
</div>
<div className="flex-1">
<FormGroup
<TradingFormGroup
label={
<Tooltip
description={
@@ -151,7 +151,7 @@ export const DealTicketSizeIceberg = ({
validate: validateAmount(sizeStep, 'minimumVisibleSize'),
}}
render={({ field }) => (
<Input
<TradingInput
id="input-order-minimum-size"
className="w-full"
type="number"
@@ -164,7 +164,7 @@ export const DealTicketSizeIceberg = ({
/>
)}
/>
</FormGroup>
</TradingFormGroup>
</div>
</div>
{renderPeakSizeError()}
@@ -10,13 +10,13 @@ import {
import { useForm, Controller, useController } from 'react-hook-form';
import * as Schema from '@vegaprotocol/types';
import {
Radio,
RadioGroup,
Input,
Checkbox,
FormGroup,
InputError,
Select,
TradingRadio,
TradingRadioGroup,
TradingInput,
TradingCheckbox,
TradingFormGroup,
TradingInputError,
TradingSelect,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
@@ -187,9 +187,9 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
}}
/>
{errors.type && (
<InputError testId="stop-order-error-message-type">
<TradingInputError testId="stop-order-error-message-type">
{errors.type.message}
</InputError>
</TradingInputError>
)}
<Controller
@@ -199,21 +199,21 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
<SideSelector value={field.value} onValueChange={field.onChange} />
)}
/>
<FormGroup label={t('Trigger')} compact={true} labelFor="">
<TradingFormGroup label={t('Trigger')} compact={true} labelFor="">
<Controller
name="triggerDirection"
control={control}
render={({ field }) => {
const { onChange, value } = field;
return (
<RadioGroup
<TradingRadioGroup
name="triggerDirection"
onChange={onChange}
value={value}
orientation="horizontal"
className="mb-2"
>
<Radio
<TradingRadio
value={
Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_RISES_ABOVE
@@ -221,7 +221,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
id="triggerDirection-risesAbove"
label={'Rises above'}
/>
<Radio
<TradingRadio
value={
Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_FALLS_BELOW
@@ -229,7 +229,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
id="triggerDirection-fallsBelow"
label={'Falls below'}
/>
</RadioGroup>
</TradingRadioGroup>
);
}}
/>
@@ -246,16 +246,17 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
validate: validateAmount(priceStep, 'Price'),
}}
control={control}
render={({ field }) => {
render={({ field, fieldState }) => {
const { value, ...props } = field;
return (
<div className="mb-2">
<Input
<TradingInput
data-testid="triggerPrice"
type="number"
step={priceStep}
appendElement={asset.symbol}
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
</div>
@@ -263,9 +264,9 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
}}
/>
{errors.triggerPrice && (
<InputError testId="stop-order-error-message-trigger-price">
<TradingInputError testId="stop-order-error-message-trigger-price">
{errors.triggerPrice.message}
</InputError>
</TradingInputError>
)}
</div>
)}
@@ -294,16 +295,17 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
'Trailing percentage offset'
),
}}
render={({ field }) => {
render={({ field, fieldState }) => {
const { value, ...props } = field;
return (
<div className="mb-2">
<Input
<TradingInput
type="number"
step={trailingPercentOffsetStep}
appendElement="%"
data-testid="triggerTrailingPercentOffset"
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
</div>
@@ -311,9 +313,9 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
}}
/>
{errors.triggerTrailingPercentOffset && (
<InputError testId="stop-order-error-message-trigger-trailing-percent-offset">
<TradingInputError testId="stop-order-error-message-trigger-trailing-percent-offset">
{errors.triggerTrailingPercentOffset.message}
</InputError>
</TradingInputError>
)}
</div>
)}
@@ -324,25 +326,29 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
render={({ field }) => {
const { onChange, value } = field;
return (
<RadioGroup
<TradingRadioGroup
onChange={onChange}
value={value}
orientation="horizontal"
>
<Radio value="price" id="triggerType-price" label={'Price'} />
<Radio
<TradingRadio
value="price"
id="triggerType-price"
label={'Price'}
/>
<TradingRadio
value="trailingPercentOffset"
id="triggerType-trailingPercentOffset"
label={'Trailing Percent Offset'}
/>
</RadioGroup>
</TradingRadioGroup>
);
}}
/>
</FormGroup>
</TradingFormGroup>
<div className="mb-2">
<div className="flex items-start gap-4">
<FormGroup
<TradingFormGroup
labelFor="input-price-quote"
label={t(`Size`)}
className="!mb-0 flex-1"
@@ -358,10 +364,10 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
},
validate: validateAmount(sizeStep, 'Size'),
}}
render={({ field }) => {
render={({ field, fieldState }) => {
const { value, ...props } = field;
return (
<Input
<TradingInput
id="order-size"
className="w-full"
type="number"
@@ -370,16 +376,17 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
onWheel={(e) => e.currentTarget.blur()}
data-testid="order-size"
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
);
}}
/>
</FormGroup>
<div className="pt-7 leading-10">@</div>
</TradingFormGroup>
<div className="pt-5 leading-10">@</div>
<div className="flex-1">
{type === Schema.OrderType.TYPE_LIMIT ? (
<FormGroup
<TradingFormGroup
labelFor="input-price-quote"
label={t(`Price (${quoteName})`)}
labelAlign="right"
@@ -397,10 +404,10 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
},
validate: validateAmount(priceStep, 'Price'),
}}
render={({ field }) => {
render={({ field, fieldState }) => {
const { value, ...props } = field;
return (
<Input
<TradingInput
id="input-price-quote"
className="w-full"
type="number"
@@ -408,15 +415,16 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
);
}}
/>
</FormGroup>
</TradingFormGroup>
) : (
<div
className="text-sm text-right pt-7 leading-10"
className="text-sm text-right pt-5 leading-10"
data-testid="price"
>
{priceFormatted && quoteName
@@ -427,21 +435,21 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
</div>
</div>
{errors.size && (
<InputError testId="stop-order-error-message-size">
<TradingInputError testId="stop-order-error-message-size">
{errors.size.message}
</InputError>
</TradingInputError>
)}
{!errors.size &&
errors.price &&
type === Schema.OrderType.TYPE_LIMIT && (
<InputError testId="stop-order-error-message-price">
<TradingInputError testId="stop-order-error-message-price">
{errors.price.message}
</InputError>
</TradingInputError>
)}
</div>
<div className="mb-2">
<FormGroup
<TradingFormGroup
label={t('Time in force')}
labelFor="select-time-in-force"
compact={true}
@@ -449,11 +457,12 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
<Controller
name="timeInForce"
control={control}
render={({ field }) => (
<Select
render={({ field, fieldState }) => (
<TradingSelect
id="select-time-in-force"
className="w-full"
data-testid="order-tif"
hasError={!!fieldState.error}
{...field}
>
<option
@@ -468,14 +477,14 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
>
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_FOK)}
</option>
</Select>
</TradingSelect>
)}
/>
</FormGroup>
</TradingFormGroup>
{errors.timeInForce && (
<InputError testId="stop-error-message-tif">
<TradingInputError testId="stop-error-message-tif">
{errors.timeInForce.message}
</InputError>
</TradingInputError>
)}
</div>
<div className="flex gap-2 pb-2 justify-between">
@@ -485,29 +494,29 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
render={({ field }) => {
const { onChange: onCheckedChange, value } = field;
return (
<Checkbox
<TradingCheckbox
onCheckedChange={onCheckedChange}
checked={value}
name="expire"
label={<span className="text-xs">{t('Expire')}</span>}
label={t('Expire')}
/>
);
}}
/>
<Checkbox
<TradingCheckbox
name="reduce-only"
checked={true}
disabled={true}
label={
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
<span className="text-xs">{t('Reduce only')}</span>
<>{t('Reduce only')}</>
</Tooltip>
}
/>
</div>
{expire && (
<>
<FormGroup
<TradingFormGroup
label={t('Strategy')}
labelFor="expiryStrategy"
compact={true}
@@ -517,26 +526,26 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
control={control}
render={({ field }) => {
return (
<RadioGroup orientation="horizontal" {...field}>
<Radio
<TradingRadioGroup orientation="horizontal" {...field}>
<TradingRadio
value={
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
}
id="expiryStrategy-submit"
label={'Submit'}
/>
<Radio
<TradingRadio
value={
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
}
id="expiryStrategy-cancel"
label={'Cancel'}
/>
</RadioGroup>
</TradingRadioGroup>
);
}}
/>
</FormGroup>
</TradingFormGroup>
<div className="mb-2">
<Controller
name="expiresAt"
@@ -17,8 +17,8 @@ import type { OrderSubmission } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { mapFormValuesToOrderSubmission } from '../../utils/map-form-values-to-submission';
import {
Checkbox,
InputError,
TradingCheckbox,
TradingInputError,
Intent,
Notification,
Tooltip,
@@ -417,7 +417,7 @@ export const DealTicket = ({
name="postOnly"
control={control}
render={({ field }) => (
<Checkbox
<TradingCheckbox
name="post-only"
checked={!disablePostOnlyCheckbox && field.value}
disabled={disablePostOnlyCheckbox}
@@ -449,7 +449,7 @@ export const DealTicket = ({
name="reduceOnly"
control={control}
render={({ field }) => (
<Checkbox
<TradingCheckbox
name="reduce-only"
checked={!disableReduceOnlyCheckbox && field.value}
disabled={disableReduceOnlyCheckbox}
@@ -483,7 +483,7 @@ export const DealTicket = ({
name="iceberg"
control={control}
render={({ field }) => (
<Checkbox
<TradingCheckbox
name="iceberg"
checked={field.value}
onCheckedChange={field.onChange}
@@ -572,11 +572,11 @@ export const NoWalletWarning = ({
if (isReadOnly) {
return (
<div className="mb-2">
<InputError testId="deal-ticket-error-message-summary">
<TradingInputError testId="deal-ticket-error-message-summary">
{
'You need to connect your own wallet to start trading on this market'
}
</InputError>
</TradingInputError>
</div>
);
}
@@ -613,9 +613,9 @@ const SummaryMessage = memo(
if (error?.message) {
return (
<div className="mb-2">
<InputError testId="deal-ticket-error-message-summary">
<TradingInputError testId="deal-ticket-error-message-summary">
{error?.message}
</InputError>
</TradingInputError>
</div>
);
}
@@ -1,4 +1,8 @@
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
import {
TradingFormGroup,
TradingInput,
TradingInputError,
} from '@vegaprotocol/ui-toolkit';
import { formatForInput } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useRef } from 'react';
@@ -19,24 +23,25 @@ export const ExpirySelector = ({
const dateFormatted = formatForInput(date);
const minDate = formatForInput(date);
return (
<FormGroup
<TradingFormGroup
label={t('Expiry time/date')}
labelFor="expiration"
compact={true}
>
<Input
<TradingInput
data-testid="date-picker-field"
id="expiration"
type="datetime-local"
value={dateFormatted}
onChange={(e) => onSelect(e.target.value)}
min={minDate}
hasError={!!errorMessage}
/>
{errorMessage && (
<InputError testId="deal-ticket-error-message-expiry">
<TradingInputError testId="deal-ticket-error-message-expiry">
{errorMessage}
</InputError>
</TradingInputError>
)}
</FormGroup>
</TradingFormGroup>
);
};
@@ -1,7 +1,7 @@
import {
FormGroup,
InputError,
Select,
TradingFormGroup,
TradingInputError,
TradingSelect,
Tooltip,
SimpleGrid,
} from '@vegaprotocol/ui-toolkit';
@@ -90,12 +90,12 @@ export const TimeInForceSelector = ({
};
return (
<FormGroup
<TradingFormGroup
label={t('Time in force')}
labelFor="select-time-in-force"
compact={true}
>
<Select
<TradingSelect
id="select-time-in-force"
value={value}
onChange={(e) => {
@@ -103,18 +103,19 @@ export const TimeInForceSelector = ({
}}
className="w-full"
data-testid="order-tif"
hasError={!!errorMessage}
>
{options.map(([key, value]) => (
<option key={key} value={value}>
{timeInForceLabel(value)}
</option>
))}
</Select>
</TradingSelect>
{errorMessage && (
<InputError testId="deal-ticket-error-message-tif">
<TradingInputError testId="deal-ticket-error-message-tif">
{renderError(errorMessage)}
</InputError>
</TradingInputError>
)}
</FormGroup>
</TradingFormGroup>
);
};
@@ -1,5 +1,5 @@
import {
InputError,
TradingInputError,
SimpleGrid,
Tooltip,
TradingDropdown,
@@ -178,9 +178,9 @@ export const TypeSelector = ({
value={value}
/>
{errorMessage && (
<InputError testId="deal-ticket-error-message-type">
<TradingInputError testId="deal-ticket-error-message-type">
{renderError(errorMessage as MarketModeValidationType)}
</InputError>
</TradingInputError>
)}
</>
);
+28 -26
View File
@@ -14,14 +14,14 @@ import { t } from '@vegaprotocol/i18n';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import {
Button,
FormGroup,
Input,
InputError,
RichSelect,
TradingFormGroup,
TradingInput,
TradingInputError,
TradingRichSelect,
Notification,
Intent,
ButtonLink,
Select,
TradingSelect,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useWeb3React } from '@web3-react/core';
@@ -151,7 +151,7 @@ export const DepositForm = ({
noValidate={true}
data-testid="deposit-form"
>
<FormGroup
<TradingFormGroup
label={t('From (Ethereum address)')}
labelFor="ethereum-address"
>
@@ -197,15 +197,17 @@ export const DepositForm = ({
}}
/>
{errors.from?.message && (
<InputError intent="danger">{errors.from.message}</InputError>
<TradingInputError intent="danger">
{errors.from.message}
</TradingInputError>
)}
</FormGroup>
<FormGroup label={t('To (Vega key)')} labelFor="to">
</TradingFormGroup>
<TradingFormGroup label={t('To (Vega key)')} labelFor="to">
<AddressField
pubKeys={pubKeys}
onChange={() => setValue('to', '')}
select={
<Select {...register('to')} id="to" defaultValue="">
<TradingSelect {...register('to')} id="to" defaultValue="">
<option value="" disabled>
{t('Please select')}
</option>
@@ -215,10 +217,10 @@ export const DepositForm = ({
{pk}
</option>
))}
</Select>
</TradingSelect>
}
input={
<Input
<TradingInput
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="to"
@@ -233,12 +235,12 @@ export const DepositForm = ({
}
/>
{errors.to?.message && (
<InputError intent="danger" forInput="to">
<TradingInputError intent="danger" forInput="to">
{errors.to.message}
</InputError>
</TradingInputError>
)}
</FormGroup>
<FormGroup label={t('Asset')} labelFor="asset">
</TradingFormGroup>
<TradingFormGroup label={t('Asset')} labelFor="asset">
<Controller
control={control}
name="asset"
@@ -248,7 +250,7 @@ export const DepositForm = ({
},
}}
render={({ field }) => (
<RichSelect
<TradingRichSelect
data-testid="select-asset"
id={field.name}
name={field.name}
@@ -271,13 +273,13 @@ export const DepositForm = ({
}
/>
))}
</RichSelect>
</TradingRichSelect>
)}
/>
{errors.asset?.message && (
<InputError intent="danger" forInput="asset">
<TradingInputError intent="danger" forInput="asset">
{errors.asset.message}
</InputError>
</TradingInputError>
)}
{isActive && isFaucetable && selectedAsset && (
<UseButton onClick={submitFaucet}>
@@ -296,7 +298,7 @@ export const DepositForm = ({
{t('View asset details')}
</button>
)}
</FormGroup>
</TradingFormGroup>
<FaucetNotification
isActive={isActive}
selectedAsset={selectedAsset}
@@ -308,8 +310,8 @@ export const DepositForm = ({
</div>
)}
{approved && (
<FormGroup label={t('Amount')} labelFor="amount">
<Input
<TradingFormGroup label={t('Amount')} labelFor="amount">
<TradingInput
type="number"
autoComplete="off"
id="amount"
@@ -374,9 +376,9 @@ export const DepositForm = ({
})}
/>
{errors.amount?.message && (
<InputError intent="danger" forInput="amount">
<TradingInputError intent="danger" forInput="amount">
{errors.amount.message}
</InputError>
</TradingInputError>
)}
{selectedAsset && balances && (
<UseButton
@@ -390,7 +392,7 @@ export const DepositForm = ({
{t('Use maximum')}
</UseButton>
)}
</FormGroup>
</TradingFormGroup>
)}
<ApproveNotification
isActive={isActive}
@@ -4,10 +4,10 @@ import { t } from '@vegaprotocol/i18n';
import {
Button,
ButtonLink,
Input,
TradingInput,
Loader,
Radio,
RadioGroup,
TradingRadio,
TradingRadioGroup,
} from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '../../hooks';
import { CUSTOM_NODE_KEY } from '../../types';
@@ -76,7 +76,7 @@ export const NodeSwitcher = ({ closeDialog }: { closeDialog: () => void }) => {
`This app will only work on ${VEGA_ENV}. Select a node to connect to.`
)}
</p>
<RadioGroup
<TradingRadioGroup
value={nodeRadio}
onChange={(value) => setNodeRadio(value)}
>
@@ -112,7 +112,7 @@ export const NodeSwitcher = ({ closeDialog }: { closeDialog: () => void }) => {
/>
</div>
</div>
</RadioGroup>
</TradingRadioGroup>
<div className="mt-4">
<Button
fill={true}
@@ -161,7 +161,7 @@ const CustomRowWrapper = ({
<LayoutRow dataTestId="custom-row">
<div className="flex w-full mb-2">
{nodes.length > 0 && (
<Radio
<TradingRadio
id="node-url-custom"
value={CUSTOM_NODE_KEY}
label={nodeRadio === CUSTOM_NODE_KEY ? '' : t('Other')}
@@ -172,7 +172,7 @@ const CustomRowWrapper = ({
data-testid="custom-node"
className="flex items-center w-full gap-2"
>
<Input
<TradingInput
placeholder="https://"
value={inputText}
hasError={Boolean(error)}
@@ -2,7 +2,7 @@ import type { ApolloError } from '@apollo/client';
import { useHeaderStore } from '@vegaprotocol/apollo-client';
import { isValidUrl } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { Radio } from '@vegaprotocol/ui-toolkit';
import { TradingRadio } from '@vegaprotocol/ui-toolkit';
import { useEffect, useState } from 'react';
import { CUSTOM_NODE_KEY } from '../../types';
import {
@@ -138,7 +138,7 @@ export const RowData = ({
<>
{id !== CUSTOM_NODE_KEY && (
<div className="break-all" data-testid="node">
<Radio id={`node-url-${id}`} value={url} label={url} />
<TradingRadio id={`node-url-${id}`} value={url} label={url} />
</div>
)}
<LayoutCell
@@ -34,21 +34,21 @@ export type OrdersUpdateSubscription = { __typename?: 'Subscription', orders?: A
export type OrderSubmissionFieldsFragment = { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null };
export type StopOrderFieldsFragment = { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } };
export type StopOrderFieldsFragment = { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string }, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } };
export type StopOrdersQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type StopOrdersQuery = { __typename?: 'Query', stopOrders?: { __typename?: 'StopOrderConnection', edges?: Array<{ __typename?: 'StopOrderEdge', node?: { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } } | null }> | null } | null };
export type StopOrdersQuery = { __typename?: 'Query', stopOrders?: { __typename?: 'StopOrderConnection', edges?: Array<{ __typename?: 'StopOrderEdge', node?: { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string }, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } } | null }> | null } | null };
export type StopOrderByIdQueryVariables = Types.Exact<{
stopOrderId: Types.Scalars['ID'];
}>;
export type StopOrderByIdQuery = { __typename?: 'Query', stopOrder?: { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } } | null };
export type StopOrderByIdQuery = { __typename?: 'Query', stopOrder?: { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string }, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } } | null };
export const OrderFieldsFragmentDoc = gql`
fragment OrderFields on Order {
@@ -9,9 +9,9 @@ import { t } from '@vegaprotocol/i18n';
import { Size } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types';
import {
FormGroup,
Input,
InputError,
TradingFormGroup,
TradingInput,
TradingInputError,
Button,
Dialog,
Icon,
@@ -102,8 +102,12 @@ export const OrderEditDialog = ({
noValidate
>
<div className="flex flex-col md:flex-row gap-4">
<FormGroup label={t('Price')} labelFor="limitPrice" className="grow">
<Input
<TradingFormGroup
label={t('Price')}
labelFor="limitPrice"
className="grow"
>
<TradingInput
type="number"
step={step}
{...register('limitPrice', {
@@ -119,13 +123,13 @@ export const OrderEditDialog = ({
id="limitPrice"
/>
{errors.limitPrice?.message && (
<InputError intent="danger">
<TradingInputError intent="danger">
{errors.limitPrice.message}
</InputError>
</TradingInputError>
)}
</FormGroup>
<FormGroup label={t('Size')} labelFor="size" className="grow">
<Input
</TradingFormGroup>
<TradingFormGroup label={t('Size')} labelFor="size" className="grow">
<TradingInput
type="number"
step={stepSize}
{...register('size', {
@@ -139,9 +143,11 @@ export const OrderEditDialog = ({
id="size"
/>
{errors.size?.message && (
<InputError intent="danger">{errors.size.message}</InputError>
<TradingInputError intent="danger">
{errors.size.message}
</TradingInputError>
)}
</FormGroup>
</TradingFormGroup>
</div>
<Button variant="primary" size="md" type="submit">
{t('Update')}
+46 -37
View File
@@ -1,18 +1,18 @@
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { useEstimatePositionQuery } from './__generated__/Positions';
import { formatRange } from '@vegaprotocol/utils';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
export const LiquidationPrice = ({
marketId,
openVolume,
collateralAvailable,
decimalPlaces,
formatDecimals,
marketDecimalPlaces,
}: {
marketId: string;
openVolume: string;
collateralAvailable: string;
decimalPlaces: number;
formatDecimals: number;
marketDecimalPlaces: number;
}) => {
const { data: currentData, previousData } = useEstimatePositionQuery({
variables: {
@@ -23,38 +23,47 @@ export const LiquidationPrice = ({
fetchPolicy: 'no-cache',
skip: !openVolume || openVolume === '0',
});
const data = currentData || previousData;
let value = '-';
if (data) {
const bestCase =
data.estimatePosition?.liquidation?.bestCase.open_volume_only.replace(
/\..*/,
''
);
const worstCase =
data.estimatePosition?.liquidation?.worstCase.open_volume_only.replace(
/\..*/,
''
);
value =
bestCase && worstCase && BigInt(bestCase) < BigInt(worstCase)
? formatRange(
bestCase,
worstCase,
decimalPlaces,
undefined,
formatDecimals,
value
)
: formatRange(
worstCase,
bestCase,
decimalPlaces,
undefined,
formatDecimals,
value
);
const data = currentData || previousData;
if (!data?.estimatePosition?.liquidation) {
return <span>-</span>;
}
return <span data-testid="liquidation-price">{value}</span>;
let bestCase = '-';
let worstCase = '-';
bestCase =
data.estimatePosition?.liquidation?.bestCase.open_volume_only.replace(
/\..*/,
''
);
worstCase =
data.estimatePosition?.liquidation?.worstCase.open_volume_only.replace(
/\..*/,
''
);
worstCase = addDecimalsFormatNumber(worstCase, marketDecimalPlaces);
bestCase = addDecimalsFormatNumber(bestCase, marketDecimalPlaces);
return (
<Tooltip
description={
<table>
<tbody>
<tr>
<th>{t('Worst case')}</th>
<td className="text-right font-mono pl-2">{worstCase}</td>
</tr>
<tr>
<th>{t('Best case')}</th>
<td className="text-right font-mono pl-2">{bestCase}</td>
</tr>
</tbody>
</table>
}
>
<span data-testid="liquidation-price">{worstCase}</span>
</Tooltip>
);
};
@@ -180,12 +180,12 @@ describe('getMetrics && rejoinPositionData', () => {
expect(metrics[0].currentLeverage).toBeCloseTo(1.02);
expect(metrics[0].marketDecimalPlaces).toEqual(5);
expect(metrics[0].positionDecimalPlaces).toEqual(0);
expect(metrics[0].decimals).toEqual(5);
expect(metrics[0].assetDecimals).toEqual(5);
expect(metrics[0].markPrice).toEqual('9431775');
expect(metrics[0].marketId).toEqual(
'5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8'
);
expect(metrics[0].marketName).toEqual('AAVEDAI.MF21');
expect(metrics[0].marketCode).toEqual('AAVEDAI.MF21');
expect(metrics[0].marketTradingMode).toEqual(
'TRADING_MODE_MONITORING_AUCTION'
);
@@ -205,12 +205,12 @@ describe('getMetrics && rejoinPositionData', () => {
expect(metrics[1].currentLeverage).toBeCloseTo(0.097);
expect(metrics[1].marketDecimalPlaces).toEqual(5);
expect(metrics[1].positionDecimalPlaces).toEqual(0);
expect(metrics[1].decimals).toEqual(5);
expect(metrics[1].assetDecimals).toEqual(5);
expect(metrics[1].markPrice).toEqual('869762');
expect(metrics[1].marketId).toEqual(
'10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e'
);
expect(metrics[1].marketName).toEqual('UNIDAI.MF21');
expect(metrics[1].marketCode).toEqual('UNIDAI.MF21');
expect(metrics[1].marketTradingMode).toEqual('TRADING_MODE_CONTINUOUS');
expect(metrics[1].notional).toEqual('86976200');
expect(metrics[1].openVolume).toEqual('-100');
@@ -26,20 +26,20 @@ import {
PositionsDocument,
PositionsSubscriptionDocument,
} from './__generated__/Positions';
import type { PositionStatus } from '@vegaprotocol/types';
import type { PositionStatus, ProductType } from '@vegaprotocol/types';
export interface Position {
assetId: string;
assetSymbol: string;
averageEntryPrice: string;
currentLeverage: number | undefined;
decimals: number;
assetDecimals: number;
quantum: string;
lossSocializationAmount: string;
marginAccountBalance: string;
marketDecimalPlaces: number;
marketId: string;
marketName: string;
marketCode: string;
marketTradingMode: Schema.MarketTradingMode;
markPrice: string | undefined;
notional: string | undefined;
@@ -51,7 +51,7 @@ export interface Position {
totalBalance: string;
unrealisedPNL: string;
updatedAt: string | null;
productType?: string;
productType: ProductType;
}
export const getMetrics = (
@@ -71,15 +71,10 @@ export const getMetrics = (
const marginAccount = accounts?.find((account) => {
return account.market?.id === market?.id;
});
const {
decimals,
id: assetId,
symbol: assetSymbol,
quantum,
} = market.tradableInstrument.instrument.product.settlementAsset;
const asset = market.tradableInstrument.instrument.product.settlementAsset;
const generalAccount = accounts?.find(
(account) =>
account.asset.id === assetId &&
account.asset.id === asset.id &&
account.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL
);
@@ -89,11 +84,11 @@ export const getMetrics = (
const marginAccountBalance = toBigNum(
marginAccount?.balance ?? 0,
decimals
asset.decimals
);
const generalAccountBalance = toBigNum(
generalAccount?.balance ?? 0,
decimals
asset.decimals
);
const markPrice = marketData
@@ -112,17 +107,17 @@ export const getMetrics = (
: notional.dividedBy(totalBalance)
: undefined;
metrics.push({
assetId,
assetSymbol,
assetId: asset.id,
assetSymbol: asset.symbol,
averageEntryPrice: position.averageEntryPrice,
currentLeverage: currentLeverage ? currentLeverage.toNumber() : undefined,
decimals,
quantum,
assetDecimals: asset.decimals,
quantum: asset.quantum,
lossSocializationAmount: position.lossSocializationAmount || '0',
marginAccountBalance: marginAccount?.balance ?? '0',
marketDecimalPlaces,
marketId: market.id,
marketName: market.tradableInstrument.instrument.code,
marketCode: market.tradableInstrument.instrument.code,
marketTradingMode: market.tradingMode,
markPrice: marketData ? marketData.markPrice : undefined,
notional: notional
@@ -133,10 +128,11 @@ export const getMetrics = (
positionDecimalPlaces,
realisedPNL: position.realisedPNL,
status: position.positionStatus,
totalBalance: totalBalance.multipliedBy(10 ** decimals).toFixed(),
totalBalance: totalBalance.multipliedBy(10 ** asset.decimals).toFixed(),
unrealisedPNL: position.unrealisedPNL,
updatedAt: position.updatedAt || null,
productType: market?.tradableInstrument.instrument.product.__typename,
productType: market?.tradableInstrument.instrument.product
.__typename as ProductType,
});
});
return metrics;
@@ -288,7 +284,7 @@ export const positionsMetricsProvider = makeDerivedDataProvider<
([positions, accounts, marketsData]) => {
const positionsData = rejoinPositionData(positions, marketsData);
const metrics = getMetrics(positionsData, accounts as Account[] | null);
return sortBy(metrics, 'marketName');
return sortBy(metrics, 'marketCode');
},
(data, delta, previousData) =>
data.filter((row) => {
+1 -1
View File
@@ -15,7 +15,7 @@ interface PositionsManagerProps {
partyIds: string[];
onMarketClick?: (marketId: string) => void;
isReadOnly: boolean;
gridProps: ReturnType<typeof useDataGridEvents>;
gridProps?: ReturnType<typeof useDataGridEvents>;
}
export const PositionsManager = ({
+174 -186
View File
@@ -1,10 +1,9 @@
import type { RenderResult } from '@testing-library/react';
import { act, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import PositionsTable, { OpenVolumeCell, PNLCell } from './positions-table';
import { PositionsTable, OpenVolumeCell, PNLCell } from './positions-table';
import type { Position } from './positions-data-providers';
import * as Schema from '@vegaprotocol/types';
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import { PositionStatus } from '@vegaprotocol/types';
import type { ICellRendererParams } from 'ag-grid-community';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
@@ -20,13 +19,13 @@ const singleRow: Position = {
assetSymbol: 'BTC',
averageEntryPrice: '133',
currentLeverage: 1.1,
decimals: 2, // this is settlementAsset.decimals
assetDecimals: 2, // this is settlementAsset.decimals
quantum: '0.1',
lossSocializationAmount: '0',
marginAccountBalance: '12345600',
marketDecimalPlaces: 1,
marketId: 'string',
marketName: 'ETH/BTC (31 july 2022)',
marketCode: 'ETHBTC.QM21',
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
markPrice: '123',
notional: '12300',
@@ -40,9 +39,13 @@ const singleRow: Position = {
productType: 'Future',
};
const singleRowData = [singleRow];
describe('Positions', () => {
const renderComponent = async (rowData: Position) => {
await act(async () => {
render(<PositionsTable rowData={[rowData]} isReadOnly={false} />);
});
};
it('should render successfully', async () => {
await act(async () => {
const { baseElement } = render(
@@ -53,158 +56,132 @@ describe('Positions', () => {
});
it('render correct columns', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={true} />);
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(11);
expect(
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual([
const expectedHeaders = [
'Market',
'Notional',
'Open volume',
'Mark price',
'Liquidation price',
'Asset',
'Entry price',
'Leverage',
'Size / Notional',
'Entry / Mark',
'Margin',
'Liquidation',
'Realised PNL',
'Unrealised PNL',
]);
];
await renderComponent(singleRow);
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(expectedHeaders.length);
expect(
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual(expectedHeaders);
});
it('renders market name', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
expect(screen.getByText('ETH/BTC (31 july 2022)')).toBeTruthy();
it('renders market code', async () => {
await renderComponent(singleRow);
expect(screen.getByText(singleRow.marketCode)).toBeTruthy();
expect(screen.getByText('Futr')).toBeInTheDocument();
});
it('Does not fail if the market name does not match the split pattern', async () => {
const breakingMarketName = 'OP/USD AUG-SEP22 - Incentive';
const row = [
Object.assign({}, singleRow, { marketName: breakingMarketName }),
];
await act(async () => {
render(<PositionsTable rowData={row} isReadOnly={false} />);
});
await renderComponent({ ...singleRow, marketCode: breakingMarketName });
expect(screen.getByText(breakingMarketName)).toBeTruthy();
});
it('add color and sign to amount, displays positive notional value', async () => {
let result: RenderResult;
await act(async () => {
result = render(
<PositionsTable rowData={singleRowData} isReadOnly={false} />
);
});
let cells = screen.getAllByRole('gridcell');
it('displays size / notional correctly for long position', async () => {
await renderComponent(singleRow);
const cells = screen.getAllByRole('gridcell');
const cell = cells[1];
expect(cells[2].classList.contains('text-market-green-600')).toBeTruthy();
expect(cells[2].classList.contains('text-market-red')).toBeFalsy();
expect(cells[2].textContent).toEqual('+100');
expect(cells[1].textContent).toEqual('1,230.0');
await act(async () => {
result.rerender(
<PositionsTable
rowData={[{ ...singleRow, openVolume: '-100' }]}
isReadOnly={false}
/>
);
});
cells = screen.getAllByRole('gridcell');
expect(cells[2].classList.contains('text-market-green-600')).toBeFalsy();
expect(cells[2].classList.contains('text-market-red')).toBeTruthy();
expect(cells[2].textContent?.startsWith('-100')).toBeTruthy();
expect(cells[1].textContent).toEqual('1,230.0');
expect(cell).toHaveClass('text-market-green-600');
expect(cell).not.toHaveClass('text-market-red');
expect(within(cell).getByTestId('stack-cell-primary')).toHaveTextContent(
'+100'
);
expect(within(cell).getByTestId('stack-cell-secondary')).toHaveTextContent(
'1,230.0'
);
});
it('displays mark price', async () => {
let result: RenderResult;
await act(async () => {
result = render(
<PositionsTable rowData={singleRowData} isReadOnly={false} />
);
it('displays size / notional correctly for short position', async () => {
await renderComponent({ ...singleRow, openVolume: '-100' });
const cells = screen.getAllByRole('gridcell');
const cell = cells[1];
expect(cell).not.toHaveClass('text-market-green-600');
expect(cell).toHaveClass('text-market-red');
expect(within(cell).getByTestId('stack-cell-primary')).toHaveTextContent(
'-100'
);
expect(within(cell).getByTestId('stack-cell-secondary')).toHaveTextContent(
'1,230.0'
);
});
it('displays entry / mark price', async () => {
await renderComponent(singleRow);
const cells = screen.getAllByRole('gridcell');
const cell = within(cells[2]);
expect(cell.getByTestId('stack-cell-primary')).toHaveTextContent('13.3');
expect(cell.getByTestId('stack-cell-secondary')).toHaveTextContent('12.3');
});
it('doesnt render entry / mark if market is in opening auction', async () => {
await renderComponent({
...singleRow,
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
});
let cells = screen.getAllByRole('gridcell');
expect(cells[3].textContent).toEqual('12.3');
await act(async () => {
result.rerender(
<PositionsTable
rowData={[
{
...singleRow,
marketTradingMode:
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
},
]}
isReadOnly={false}
/>
);
});
cells = screen.getAllByRole('gridcell');
expect(cells[3].textContent).toEqual('-');
const cells = screen.getAllByRole('gridcell');
expect(cells[2].textContent).toEqual('-');
});
it('displays liquidation price', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
await renderComponent(singleRow);
const cells = screen.getAllByRole('gridcell');
expect(cells[4].textContent).toEqual('liquidation price');
});
it('displays leverage', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
it('displays margin and leverage', async () => {
await renderComponent(singleRow);
const cells = screen.getAllByRole('gridcell');
expect(cells[7].textContent).toEqual('1.1');
});
it('displays allocated margin', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[8];
expect(cell.textContent).toEqual('123,456.00');
// margin
expect(
within(cells[3]).getByTestId('stack-cell-primary')
).toHaveTextContent('123,456.00');
// leverage
expect(
within(cells[3]).getByTestId('stack-cell-secondary')
).toHaveTextContent('1.1');
});
it('displays realised and unrealised PNL', async () => {
// pnl cells should be rendered with asset dps
const expectedRealised = addDecimalsFormatNumber(
singleRow.realisedPNL,
singleRow.decimals
singleRow.assetDecimals
);
const expectedUnrealised = addDecimalsFormatNumber(
singleRow.unrealisedPNL,
singleRow.decimals
singleRow.assetDecimals
);
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
await renderComponent(singleRow);
const cells = screen.getAllByRole('gridcell');
expect(cells[9].textContent).toEqual(expectedRealised);
expect(cells[10].textContent).toEqual(expectedUnrealised);
expect(cells[5]).toHaveTextContent(expectedRealised);
expect(cells[6]).toHaveTextContent(expectedUnrealised);
});
it('displays close button', async () => {
await act(async () => {
render(
<PositionsTable
rowData={singleRowData}
pubKey={singleRowData[0].partyId}
rowData={[singleRow]}
pubKey={singleRow.partyId}
onClose={() => {
return;
}}
@@ -212,24 +189,15 @@ describe('Positions', () => {
/>
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[11].textContent).toEqual('');
expect(screen.getByTestId('close-position')).toBeInTheDocument();
});
it('do not display close button if openVolume is zero', async () => {
await act(async () => {
render(
<PositionsTable
rowData={[{ ...singleRow, openVolume: '0' }]}
onClose={() => {
return;
}}
isReadOnly={false}
/>
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[11].textContent).toEqual('');
await renderComponent({ ...singleRow, openVolume: '0' });
expect(
screen.queryByRole('button', { name: 'Close' })
).not.toBeInTheDocument();
});
describe('PNLCell', () => {
@@ -249,40 +217,27 @@ describe('Positions', () => {
lossSocialisationAmount: '0',
},
valueFormatted: '100',
};
render(<PNLCell {...(props as ICellRendererParams)} />);
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
expect(screen.queryByRole('img')).not.toBeInTheDocument();
} as ICellRendererParams;
render(<PNLCell {...props} />);
expect(
screen.getByText(props.valueFormatted as string)
).toBeInTheDocument();
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
});
it('renders value with warning tooltip if loss socialisation occurred', async () => {
it('renders value with warning icon if loss socialisation occurred', () => {
const props = {
data: {
...singleRow,
lossSocializationAmount: '500',
decimals: 2,
assetDecimals: 2,
},
valueFormatted: '100',
};
render(<PNLCell {...(props as ICellRendererParams)} />);
const content = screen.getByText(props.valueFormatted);
expect(content).toBeInTheDocument();
expect(screen.getByRole('img')).toBeInTheDocument();
await userEvent.hover(content);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
'Lifetime loss socialisation deductions: 5.00'
)
).toBeInTheDocument();
expect(
within(tooltip).getByText(
`You received less BTC in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`
)
).toBeInTheDocument();
expect(screen.getByTestId(/icon-/)).toBeInTheDocument();
});
});
@@ -290,9 +245,10 @@ describe('Positions', () => {
const props = {
data: undefined,
valueFormatted: '100',
};
} as ICellRendererParams;
it('renders a dash if no data', () => {
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
render(<OpenVolumeCell {...props} />);
expect(screen.getByText('-')).toBeInTheDocument();
});
@@ -306,36 +262,21 @@ describe('Positions', () => {
};
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
expect(screen.queryByRole('img')).not.toBeInTheDocument();
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
});
it('renders status with warning tooltip if orders were closed', async () => {
it('renders status with warning tooltip if orders were closed', () => {
const props = {
data: {
...singleRow,
status: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
},
valueFormatted: '100',
};
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
const content = screen.getByText(props.valueFormatted);
} as ICellRendererParams;
render(<OpenVolumeCell {...props} />);
const content = screen.getByText(props.valueFormatted as string);
expect(content).toBeInTheDocument();
expect(screen.getByRole('img')).toBeInTheDocument();
await userEvent.hover(content);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
`Status: ${PositionStatusMapping[props.data.status]}`
)
).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
)
).toBeInTheDocument();
expect(screen.getByTestId(/icon-/)).toBeInTheDocument();
});
it('renders status with warning tooltip if position was closed out', async () => {
@@ -345,24 +286,71 @@ describe('Positions', () => {
status: PositionStatus.POSITION_STATUS_CLOSED_OUT,
},
valueFormatted: '100',
};
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
const content = screen.getByText(props.valueFormatted);
} as ICellRendererParams;
render(<OpenVolumeCell {...props} />);
const content = screen.getByText(props.valueFormatted as string);
expect(content).toBeInTheDocument();
expect(screen.getByRole('img')).toBeInTheDocument();
await userEvent.hover(content);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(screen.getByTestId(/icon-/)).toBeInTheDocument();
});
});
describe('position status from size column', () => {
it('does not show if position status is normal', async () => {
await renderComponent({
...singleRow,
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[1];
await userEvent.hover(cell);
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
});
it.each([
{
status: PositionStatus.POSITION_STATUS_CLOSED_OUT,
text: 'Your position was closed.',
},
{
status: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
text: 'Your open orders were cancelled.',
},
{
status: PositionStatus.POSITION_STATUS_DISTRESSED,
text: 'Your position is distressed.',
},
])('renders content for $status', async (data) => {
await renderComponent({
...singleRow,
status: data.status,
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[1];
await userEvent.hover(cell);
const tooltip = within(await screen.findByRole('tooltip'));
expect(tooltip.getByText(data.text)).toBeInTheDocument();
});
});
describe('loss socialization from realised pnl column', () => {
it('renders', async () => {
await renderComponent({
...singleRow,
lossSocializationAmount: '500',
assetDecimals: 2,
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[5];
await userEvent.hover(cell);
const tooltip = within(await screen.findByRole('tooltip'));
expect(tooltip.getByText('Realised PNL: 1.23')).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
`Status: ${PositionStatusMapping[props.data.status]}`
)
tooltip.getByText('Lifetime loss socialisation deductions: 5.00')
).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
'You did not have enough BTC collateral to meet the maintenance margin requirements for your position, so it was closed by the network.'
tooltip.getByText(
`You received less BTC in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`
)
).toBeInTheDocument();
});
+306 -306
View File
@@ -1,46 +1,46 @@
import classNames from 'classnames';
import { useMemo } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import type { ColDef } from 'ag-grid-community';
import type { ColDef, ITooltipParams } from 'ag-grid-community';
import type {
VegaValueFormatterParams,
VegaValueGetterParams,
TypedDataAgGrid,
VegaICellRendererParams,
} from '@vegaprotocol/datagrid';
import { COL_DEFS } from '@vegaprotocol/datagrid';
import { ProgressBarCell } from '@vegaprotocol/datagrid';
import {
AgGridLazy as AgGrid,
COL_DEFS,
PriceFlashCell,
signedNumberCssClass,
signedNumberCssClassRules,
MarketNameCell,
ProgressBarCell,
MarketProductPill,
} from '@vegaprotocol/datagrid';
import {
ButtonLink,
Tooltip,
TooltipCellComponent,
ExternalLink,
Icon,
VegaIconNames,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import {
volumePrefix,
toBigNum,
formatNumber,
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { Position } from './positions-data-providers';
import * as Schema from '@vegaprotocol/types';
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import {
MarketTradingMode,
PositionStatus,
PositionStatusMapping,
} from '@vegaprotocol/types';
import { DocsLinks } from '@vegaprotocol/environment';
import { PositionActionsDropdown } from './position-actions-dropdown';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { LiquidationPrice } from './liquidation-price';
import { StackedCell } from './stacked-cell';
interface Props extends TypedDataAgGrid<Position> {
onClose?: (data: Position) => void;
@@ -48,44 +48,25 @@ interface Props extends TypedDataAgGrid<Position> {
style?: CSSProperties;
isReadOnly: boolean;
multipleKeys?: boolean;
pubKeys?: VegaWalletContextShape['pubKeys'];
pubKey?: VegaWalletContextShape['pubKey'];
pubKeys?: Array<{ name: string; publicKey: string }> | null;
pubKey?: string | null;
}
export interface AmountCellProps {
valueFormatted?: Pick<
Position,
'openVolume' | 'marketDecimalPlaces' | 'positionDecimalPlaces' | 'notional'
>;
}
export const AmountCell = ({ valueFormatted }: AmountCellProps) => {
if (!valueFormatted) {
return null;
}
const { openVolume, positionDecimalPlaces, marketDecimalPlaces, notional } =
valueFormatted;
return valueFormatted && notional ? (
<div className="leading-tight font-mono">
<div
className={classNames('text-right', signedNumberCssClass(openVolume))}
>
{volumePrefix(
addDecimalsFormatNumber(openVolume, positionDecimalPlaces)
)}
</div>
<div className="text-right">
{addDecimalsFormatNumber(notional, marketDecimalPlaces)}
</div>
</div>
) : null;
};
AmountCell.displayName = 'AmountCell';
export const getRowId = ({ data }: { data: Position }) =>
`${data.partyId}-${data.marketId}`;
const realisedPNLValueGetter = ({ data }: { data: Position }) => {
return !data
? undefined
: toBigNum(data.realisedPNL, data.assetDecimals).toNumber();
};
const unrealisedPNLValueGetter = ({ data }: { data: Position }) => {
return !data
? undefined
: toBigNum(data.unrealisedPNL, data.assetDecimals).toNumber();
};
const defaultColDef = {
sortable: true,
filter: true,
@@ -103,7 +84,6 @@ export const PositionsTable = ({
pubKey,
...props
}: Props) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return (
<AgGrid
overlayNoRowsTemplate={t('No positions')}
@@ -111,11 +91,11 @@ export const PositionsTable = ({
tooltipShowDelay={500}
defaultColDef={defaultColDef}
components={{
AmountCell,
PriceFlashCell,
ProgressBarCell,
MarketNameCell,
}}
rowHeight={45}
columnDefs={useMemo<ColDef[]>(() => {
const columnDefs: (ColDef | null)[] = [
multipleKeys
@@ -132,41 +112,37 @@ export const PositionsTable = ({
: null,
{
headerName: t('Market'),
field: 'marketName',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'marketId', onMarketClick },
},
{
headerName: t('Notional'),
headerTooltip: t('Mark price x open volume.'),
field: 'notional',
type: 'rightAligned',
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data?.notional
? undefined
: toBigNum(data.notional, data.marketDecimalPlaces).toNumber();
field: 'marketCode',
onCellClicked: ({ data }) => {
if (!onMarketClick) return;
onMarketClick(data.marketId);
},
valueFormatter: ({
cellRenderer: ({
value,
data,
}: VegaValueFormatterParams<Position, 'notional'>) => {
return !data || !data.notional
? '-'
: addDecimalsFormatNumber(
data.notional,
data.marketDecimalPlaces
);
}: VegaICellRendererParams<Position, 'marketCode'>) => {
if (!data || !value) return '-';
return (
<StackedCell
primary={value}
secondary={
<>
{data?.assetSymbol}
<MarketProductPill productType={data.productType} />
</>
}
/>
);
},
},
{
headerName: t('Open volume'),
headerName: t('Size / Notional'),
field: 'openVolume',
type: 'rightAligned',
cellClass: 'font-mono text-right',
cellClassRules: signedNumberCssClassRules,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
valueGetter: ({ data }: { data: Position }) => {
return data?.openVolume === undefined
? undefined
: toBigNum(
@@ -174,165 +150,205 @@ export const PositionsTable = ({
data.positionDecimalPlaces
).toNumber();
},
tooltipValueGetter: ({ data }: ITooltipParams<Position>) => {
if (
!data ||
data.status === PositionStatus.POSITION_STATUS_UNSPECIFIED
) {
return null;
}
return data.status;
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'openVolume'>): string => {
return data?.openVolume === undefined
? ''
: volumePrefix(
addDecimalsFormatNumber(
data.openVolume,
data.positionDecimalPlaces
)
if (!data?.openVolume) return '-';
const vol = volumePrefix(
addDecimalsFormatNumber(
data.openVolume,
data.positionDecimalPlaces
)
);
return vol;
},
tooltipComponent: (args: ITooltipParams<Position>) => {
if (!args.data) {
return null;
}
const POSITION_RESOLUTION_LINK =
DocsLinks?.POSITION_RESOLUTION ?? '';
let primaryTooltip;
switch (args.data.status) {
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
primaryTooltip = t('Your position was closed.');
break;
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
primaryTooltip = t('Your open orders were cancelled.');
break;
case PositionStatus.POSITION_STATUS_DISTRESSED:
primaryTooltip = t('Your position is distressed.');
break;
}
let secondaryTooltip;
switch (args.data.status) {
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
secondaryTooltip = t(
`You did not have enough %s collateral to meet the maintenance margin requirements for your position, so it was closed by the network.`,
args.data.assetSymbol
);
break;
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
secondaryTooltip = t(
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
);
break;
case PositionStatus.POSITION_STATUS_DISTRESSED:
secondaryTooltip = t(
'The position was distressed, but could not be closed out - orders were removed from the book, and the open volume will be closed out once there is sufficient volume on the book.'
);
break;
default:
secondaryTooltip = t('Maintained by network');
}
return (
<TooltipCellComponent
{...args}
value={
<>
<p className="mb-2">{primaryTooltip}</p>
<p className="mb-2">{secondaryTooltip}</p>
<p className="mb-2">
{t(
'Status: %s',
PositionStatusMapping[args.data.status]
)}
</p>
{POSITION_RESOLUTION_LINK && (
<ExternalLink href={POSITION_RESOLUTION_LINK}>
{t('Read more about position resolution')}
</ExternalLink>
)}
</>
}
/>
);
},
cellRenderer: OpenVolumeCell,
},
{
headerName: t('Mark price'),
headerName: t('Entry / Mark'),
field: 'markPrice',
type: 'rightAligned',
cellRenderer: PriceFlashCell,
cellClass: 'font-mono text-right',
cellRenderer: ({
data,
}: VegaICellRendererParams<Position, 'markPrice'>) => {
if (
!data?.averageEntryPrice ||
!data?.markPrice ||
!data?.marketDecimalPlaces
) {
return <>-</>;
}
if (
data.marketTradingMode ===
MarketTradingMode.TRADING_MODE_OPENING_AUCTION
) {
return <>-</>;
}
const entry = addDecimalsFormatNumber(
data.averageEntryPrice,
data.marketDecimalPlaces
);
const mark = addDecimalsFormatNumber(
data.markPrice,
data.marketDecimalPlaces
);
return (
<StackedCell
primary={entry}
secondary={
<PriceFlashCell
value={Number(data.markPrice)}
valueFormatted={mark}
/>
}
/>
);
},
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data ||
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
MarketTradingMode.TRADING_MODE_OPENING_AUCTION
? undefined
: toBigNum(data.markPrice, data.marketDecimalPlaces).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data) {
return '';
}
},
{
headerName: t('Margin'),
colId: 'margin',
type: 'rightAligned',
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(
data.marginAccountBalance,
data.assetDecimals
).toNumber();
},
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
!data ||
!data.marginAccountBalance ||
!data.marketDecimalPlaces
) {
return '-';
return null;
}
return addDecimalsFormatNumber(
data.markPrice,
data.marketDecimalPlaces
const margin = addDecimalsFormatNumberQuantum(
data.marginAccountBalance,
data.assetDecimals,
data.quantum
);
const lev = data?.currentLeverage ? data.currentLeverage : 1;
const leverage = formatNumber(Math.max(1, lev), 1);
return (
<StackedCell primary={margin} secondary={leverage + 'x'} />
);
},
},
{
headerName: t('Liquidation price'),
colId: 'liquidationPrice',
type: 'rightAligned',
headerName: 'Liquidation',
headerTooltip: t('Worst case liquidation price'),
cellClass: 'font-mono text-right',
type: 'rightAligned',
// Cannot be sortable as data is fetched within the cell
sortable: false,
filter: false,
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
if (!data) {
return '-';
}
return (
<LiquidationPrice
marketId={data.marketId}
openVolume={data.openVolume}
collateralAvailable={data.totalBalance}
decimalPlaces={data.decimals}
formatDecimals={data.marketDecimalPlaces}
marketDecimalPlaces={data.marketDecimalPlaces}
/>
);
},
},
{
headerName: t('Asset'),
field: 'assetSymbol',
colId: 'asset',
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<ButtonLink
title={t('View settlement asset details')}
onClick={(e) => {
openAssetDetailsDialog(
data.assetId,
e.target as HTMLElement
);
}}
>
{data?.assetSymbol}
</ButtonLink>
);
},
},
{
headerName: t('Entry price'),
field: 'averageEntryPrice',
type: 'rightAligned',
cellRenderer: PriceFlashCell,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return data?.markPrice === undefined || !data
? undefined
: toBigNum(
data.averageEntryPrice,
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
Position,
'averageEntryPrice'
>): string => {
if (!data) {
return '';
}
return addDecimalsFormatNumber(
data.averageEntryPrice,
data.marketDecimalPlaces
);
},
},
multipleKeys
? null
: {
headerName: t('Leverage'),
field: 'currentLeverage',
type: 'rightAligned',
filter: 'agNumberColumnFilter',
cellRenderer: PriceFlashCell,
valueFormatter: ({
value,
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined ? '' : formatNumber(value.toString(), 1),
},
multipleKeys
? null
: {
headerName: t('Margin'),
field: 'marginAccountBalance',
type: 'rightAligned',
filter: 'agNumberColumnFilter',
cellRenderer: PriceFlashCell,
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(
data.marginAccountBalance,
data.decimals
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
Position,
'marginAccountBalance'
>): string => {
if (!data) {
return '';
}
return addDecimalsFormatNumber(
data.marginAccountBalance,
data.decimals
);
},
},
{
headerName: t('Realised PNL'),
field: 'realisedPNL',
@@ -340,17 +356,71 @@ export const PositionsTable = ({
cellClassRules: signedNumberCssClassRules,
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.realisedPNL, data.decimals).toNumber();
valueGetter: realisedPNLValueGetter,
// @ts-ignore no type overlap, but the functions are identical
tooltipValueGetter: realisedPNLValueGetter,
tooltipComponent: (args: ITooltipParams) => {
const LOSS_SOCIALIZATION_LINK =
DocsLinks?.LOSS_SOCIALIZATION ?? '';
if (!args.data) {
return <>-</>;
}
const losses = parseInt(
args.data?.lossSocializationAmount ?? '0'
);
if (losses <= 0) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{args.valueFormatted}</>;
}
const lossesFormatted = addDecimalsFormatNumber(
args.data.lossSocializationAmount,
args.data.assetDecimals
);
return (
<TooltipCellComponent
{...args}
value={
<>
<p className="mb-2">
{t('Realised PNL: %s', args.value)}
</p>
<p className="mb-2">
{t(
'Lifetime loss socialisation deductions: %s',
lossesFormatted
)}
</p>
<p className="mb-2">
{t(
`You received less %s in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`,
args.data.assetSymbol
)}
</p>
{LOSS_SOCIALIZATION_LINK && (
<ExternalLink href={LOSS_SOCIALIZATION_LINK}>
{t('Read more about loss socialisation')}
</ExternalLink>
)}
</>
}
/>
);
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
return !data
? ''
: addDecimalsFormatNumber(data.realisedPNL, data.decimals);
: addDecimalsFormatNumberQuantum(
data.realisedPNL,
data.assetDecimals,
data.quantum
);
},
headerTooltip: t(
'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.'
@@ -364,21 +434,22 @@ export const PositionsTable = ({
cellClassRules: signedNumberCssClassRules,
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
},
valueGetter: unrealisedPNLValueGetter,
// @ts-ignore no type overlap but function can be identical
tooltipValueGetter: unrealisedPNLValueGetter,
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
!data
? ''
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals),
: addDecimalsFormatNumberQuantum(
data.unrealisedPNL,
data.assetDecimals,
data.quantum
),
headerTooltip: t(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
),
cellRenderer: PNLCell,
},
onClose && !isReadOnly
? {
@@ -410,28 +481,16 @@ export const PositionsTable = ({
return columnDefs.filter<ColDef>(
(colDef: ColDef | null): colDef is ColDef => colDef !== null
);
}, [
isReadOnly,
multipleKeys,
onClose,
onMarketClick,
openAssetDetailsDialog,
pubKey,
pubKeys,
])}
}, [isReadOnly, multipleKeys, onClose, onMarketClick, pubKey, pubKeys])}
{...props}
/>
);
};
export default PositionsTable;
export const PNLCell = ({
valueFormatted,
data,
}: VegaICellRendererParams<Position, 'realisedPNL'>) => {
const LOSS_SOCIALIZATION_LINK = DocsLinks?.LOSS_SOCIALIZATION ?? '';
if (!data) {
return <>-</>;
}
@@ -442,121 +501,62 @@ export const PNLCell = ({
return <>{valueFormatted}</>;
}
const lossesFormatted = addDecimalsFormatNumber(
data.lossSocializationAmount,
data.decimals
);
return (
<WarningCell
tooltipContent={
<>
<p className="mb-2">
{t('Lifetime loss socialisation deductions: %s', lossesFormatted)}
</p>
<p className="mb-2">
{t(
`You received less %s in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`,
[data.assetSymbol]
)}
</p>
{LOSS_SOCIALIZATION_LINK && (
<ExternalLink href={LOSS_SOCIALIZATION_LINK}>
{t('Read more about loss socialisation')}
</ExternalLink>
)}
</>
}
>
{valueFormatted}
</WarningCell>
);
return <WarningCell>{valueFormatted}</WarningCell>;
};
export const OpenVolumeCell = ({
valueFormatted,
data,
}: VegaICellRendererParams<Position, 'openVolume'>) => {
if (!data) {
if (!valueFormatted || !data || !data.notional) {
return <>-</>;
}
const POSITION_RESOLUTION_LINK = DocsLinks?.POSITION_RESOLUTION ?? '';
const notional = addDecimalsFormatNumber(
data.notional,
data.marketDecimalPlaces
);
let primaryTooltip;
switch (data.status) {
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
primaryTooltip = t('Your position was closed.');
break;
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
primaryTooltip = t('Your open orders were cancelled.');
break;
case PositionStatus.POSITION_STATUS_DISTRESSED:
primaryTooltip = t('Your position is distressed.');
break;
const cellContent = (
<StackedCell primary={valueFormatted} secondary={notional} />
);
if (data.status === PositionStatus.POSITION_STATUS_UNSPECIFIED) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{cellContent}</>;
}
let secondaryTooltip;
switch (data.status) {
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
secondaryTooltip = t(
`You did not have enough %s collateral to meet the maintenance margin requirements for your position, so it was closed by the network.`,
[data.assetSymbol]
);
break;
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
secondaryTooltip = t(
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
);
break;
case PositionStatus.POSITION_STATUS_DISTRESSED:
secondaryTooltip = t(
'The position was distressed, but could not be closed out - orders were removed from the book, and the open volume will be closed out once there is sufficient volume on the book.'
);
break;
default:
secondaryTooltip = t('Maintained by network');
}
return (
<WarningCell
showIcon={data.status !== PositionStatus.POSITION_STATUS_UNSPECIFIED}
tooltipContent={
<>
<p className="mb-2">{primaryTooltip}</p>
<p className="mb-2">{secondaryTooltip}</p>
<p className="mb-2">
{t('Status: %s', PositionStatusMapping[data.status])}
</p>
{POSITION_RESOLUTION_LINK && (
<ExternalLink href={POSITION_RESOLUTION_LINK}>
{t('Read more about position resolution')}
</ExternalLink>
)}
</>
showIcon={
// not sure why but data.status has become a union of all the enum values
// rather than just being the enum itself
(data.status as PositionStatus) !==
PositionStatus.POSITION_STATUS_UNSPECIFIED
}
>
{valueFormatted}
{cellContent}
</WarningCell>
);
};
const WarningCell = ({
children,
tooltipContent,
showIcon = true,
}: {
children: ReactNode;
tooltipContent: ReactNode;
showIcon?: boolean;
}) => {
return (
<Tooltip description={tooltipContent}>
<div className="w-full flex items-center justify-between underline decoration-dashed underline-offest-2">
<span className="text-black dark:text-white mr-1">
{showIcon && <Icon name="warning-sign" size={3} />}
<div className="flex justify-end items-center">
{showIcon && (
<span className="text-black dark:text-white mr-2">
<VegaIcon name={VegaIconNames.EXCLAIMATION_MARK} size={12} />
</span>
<span className="text-ellipsis overflow-hidden">{children}</span>
</div>
</Tooltip>
)}
<span className="overflow-hidden whitespace-nowrap text-ellipsis">
{children}
</span>
</div>
);
};
+18
View File
@@ -0,0 +1,18 @@
import type { ReactNode } from 'react';
export const StackedCell = ({
primary,
secondary,
}: {
primary: ReactNode;
secondary: ReactNode;
}) => {
return (
<div className="leading-4 text-ellipsis whitespace-nowrap overflow-hidden">
<div data-testid="stack-cell-primary">{primary}</div>
<div data-testid="stack-cell-secondary" className="text-muted">
{secondary}
</div>
</div>
);
};
@@ -5,7 +5,6 @@ import {
CenteredGridCellWrapper,
COL_DEFS,
DateRangeFilter,
MarketProductPill,
SetFilter,
} from '@vegaprotocol/datagrid';
import compact from 'lodash/compact';
@@ -20,13 +19,15 @@ import type {
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import type { InstrumentConfiguration } from '@vegaprotocol/types';
import { ProposalStateMapping } from '@vegaprotocol/types';
import { ExternalLink, Pill } from '@vegaprotocol/ui-toolkit';
import {
ProposalProductTypeMapping,
ProposalProductTypeShortName,
ProposalStateMapping,
} from '@vegaprotocol/types';
import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
import { VoteProgress } from '../voting-progress';
import { ProposalActionsDropdown } from '../proposal-actions-dropdown';
import { getMarketProductType } from '../../utils/get-market-product-type';
export const MarketNameProposalCell = ({
value,
@@ -38,13 +39,19 @@ export const MarketNameProposalCell = ({
const { VEGA_TOKEN_URL } = useEnvironment();
const { change } = data?.terms || {};
if (change?.__typename === 'NewMarket' && VEGA_TOKEN_URL) {
const productType = getMarketProductType(
change.instrument as InstrumentConfiguration
);
const type = change.instrument.futureProduct?.__typename;
const content = (
<>
<span data-testid="market-code">{value as string}</span>
<MarketProductPill productType={productType} />
{type && (
<Pill
size="xxs"
className="uppercase ml-0.5"
title={ProposalProductTypeMapping[type]}
>
{ProposalProductTypeShortName[type]}
</Pill>
)}
</>
);
if (data?.id) {
@@ -1,58 +0,0 @@
import type { InstrumentConfiguration } from '@vegaprotocol/types';
import { getMarketProductType } from './get-market-product-type';
describe('getMarketProductType', () => {
it('should resolve product type properly', () => {
expect(
getMarketProductType({
futureProduct: {
quoteName: 'Market 1',
},
} as InstrumentConfiguration)
).toEqual('Future');
expect(
getMarketProductType({
spotProduct: {
quoteName: 'Market 1',
},
} as unknown as InstrumentConfiguration)
).toEqual('Spot');
expect(
getMarketProductType({
perpetualProduct: {
quoteName: 'Market 1',
},
} as unknown as InstrumentConfiguration)
).toEqual('Perpetual');
expect(
getMarketProductType({
product: {
__typename: 'Perpetual',
},
futureProduct: {
quoteName: 'Market 1',
},
} as unknown as InstrumentConfiguration)
).toEqual('Perpetual');
expect(
getMarketProductType({
product: {
__typename: 'Spot',
},
futureProduct: {
quoteName: 'Market 1',
},
} as unknown as InstrumentConfiguration)
).toEqual('Spot');
expect(
getMarketProductType({
product: {
__typename: 'Future',
},
perpetualProduct: {
quoteName: 'Market 1',
},
} as unknown as InstrumentConfiguration)
).toEqual('Future');
});
});
@@ -1,16 +0,0 @@
import type { InstrumentConfiguration, Product } from '@vegaprotocol/types';
// it needs to be adjusted after deploy this https://github.com/vegaprotocol/vega/pull/9003
export const getMarketProductType = (
instrumentConfiguration: InstrumentConfiguration
) => {
return 'product' in instrumentConfiguration
? (instrumentConfiguration.product as Product).__typename
: 'futureProduct' in instrumentConfiguration
? 'Future'
: 'spotProduct' in instrumentConfiguration
? 'Spot'
: 'perpetualProduct' in instrumentConfiguration
? 'Perpetual'
: undefined;
};
+1 -1
View File
@@ -172,7 +172,7 @@ module.exports = {
900: '#F9FAFA',
},
},
danger: '#FF077F',
danger: '#EC003C',
warning: '#FF8700',
success: '#00F780',
},
+3 -1
View File
@@ -4364,6 +4364,8 @@ export type StopOrder = {
marketId: Scalars['ID'];
/** If OCO (one-cancels-other) order, the ID of the associated order. */
ocoLinkId?: Maybe<Scalars['ID']>;
/** The order that was created when triggered. */
order?: Maybe<Order>;
/** Party that submitted the stop order. */
partyId: Scalars['ID'];
/** Status of the stop order */
@@ -4371,7 +4373,7 @@ export type StopOrder = {
/** Order to submit when the stop order is triggered. */
submission: OrderSubmission;
/** Price movement that will trigger the stop order */
trigger?: Maybe<StopOrderTrigger>;
trigger: StopOrderTrigger;
/** Direction the price is moving to trigger the stop order. */
triggerDirection: StopOrderTriggerDirection;
/** Time the stop order was last updated. */
+18
View File
@@ -25,6 +25,7 @@ import type {
DispatchMetric,
StopOrderStatus,
} from './__generated__/types';
import type { ProductType, ProposalProductType } from './product';
export const AccountTypeMapping: {
[T in AccountType]: string;
@@ -513,3 +514,20 @@ export const PeggedReferenceMapping: { [R in PeggedReference]: string } = {
PEGGED_REFERENCE_BEST_BID: 'Bid',
PEGGED_REFERENCE_MID: 'Mid',
};
export const ProductTypeMapping: Record<ProductType, string> = {
Future: 'Future',
};
export const ProductTypeShortName: Record<ProductType, string> = {
Future: 'Futr',
};
export const ProposalProductTypeMapping: Record<ProposalProductType, string> = {
FutureProduct: 'Future',
};
export const ProposalProductTypeShortName: Record<ProposalProductType, string> =
{
FutureProduct: 'Futr',
};
+1
View File
@@ -1,3 +1,4 @@
export * from './__generated__/types';
export * from './candle';
export * from './global-types-mappings';
export * from './product';
+7
View File
@@ -0,0 +1,7 @@
import type { Product } from './__generated__/types';
export type ProductType = NonNullable<Product['__typename']>;
// TODO: Update to be dynamically created for ProductionConfiguration union when schema
// changes make it to stagnet1
export type ProposalProductType = 'FutureProduct';
+11 -3
View File
@@ -16,8 +16,8 @@ export * from './form-group';
export * from './healthbar';
export * from './icon';
export * from './indicator';
export * from './input-error';
export * from './input';
export * from './input-error';
export * from './key-value-table';
export * from './link';
export * from './loader';
@@ -48,10 +48,18 @@ export * from './tiny-scroll';
export * from './toast';
export * from './toggle';
export * from './tooltip';
export * from './trading-button';
export * from './trading-dropdown';
export * from './traffic-light';
export * from './vega-icons';
export * from './vega-logo';
export * from './viewing-as-user';
export * from './pill';
// Trading specific components
export * from './trading-button';
export * from './trading-checkbox';
export * from './trading-dropdown';
export * from './trading-form-group';
export * from './trading-input-error';
export * from './trading-input';
export * from './trading-radio-group';
export * from './trading-select';
@@ -60,5 +60,9 @@ export const Tooltip = ({
);
export const TooltipCellComponent = (props: ITooltipParams) => {
return <p className={tooltipContentClasses}>{props.value}</p>;
return (
<div className={tooltipContentClasses} role="tooltip">
{props.value}
</div>
);
};
@@ -0,0 +1,40 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { TradingCheckbox } from './checkbox';
describe('Checkbox', () => {
it('should render checkbox with label successfully', () => {
render(<TradingCheckbox label="test" />);
expect(screen.getByText('test')).toBeInTheDocument();
});
it('should render a checked checkbox if specified in state', () => {
render(<TradingCheckbox label="label" checked={true} />);
expect(screen.getByTestId(/icon-/)).toBeInTheDocument();
});
it('should render an unchecked checkbox if specified in state', () => {
render(<TradingCheckbox label="unchecked" checked={false} />);
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
});
it('should render an indeterminate checkbox if specified in state', () => {
render(<TradingCheckbox label="indeterminate" checked="indeterminate" />);
expect(screen.getByTestId('indeterminate-icon')).toBeInTheDocument();
});
it('fires callback on change if provided', () => {
const callback = jest.fn();
render(
<TradingCheckbox
name="test"
label="onchange"
onCheckedChange={callback}
/>
);
const checkbox = screen.getByText('onchange');
fireEvent.click(checkbox);
expect(callback).toHaveBeenCalled();
});
});
@@ -0,0 +1,38 @@
import type { Meta, StoryFn } from '@storybook/react';
import type { TradingCheckboxProps } from './checkbox';
import { TradingCheckbox } from './checkbox';
export default {
component: TradingCheckbox,
title: 'Checkbox',
} as Meta<typeof TradingCheckbox>;
const Template: StoryFn<TradingCheckboxProps> = (args) => (
<TradingCheckbox {...args} />
);
export const Default = Template.bind({});
Default.args = {
name: 'default',
label: 'Regular checkbox',
};
export const Overflow = Template.bind({});
Overflow.args = {
name: 'overflow',
label:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
};
export const Disabled = Template.bind({});
Disabled.args = {
disabled: true,
label: 'Disabled',
};
export const Indeterminate = Template.bind({});
Indeterminate.args = {
name: 'default',
checked: 'indeterminate',
label: 'Indeterminate checkbox',
};
@@ -0,0 +1,63 @@
import { VegaIcon, VegaIconNames } from '../icon';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import classNames from 'classnames';
import type { ReactNode } from 'react';
type CheckedState = boolean | 'indeterminate';
export interface TradingCheckboxProps {
checked?: CheckedState;
label?: ReactNode;
name?: string;
onCheckedChange?: (checked: CheckedState) => void;
disabled?: boolean;
}
export const TradingCheckbox = ({
checked,
label,
name,
onCheckedChange,
disabled = false,
}: TradingCheckboxProps) => {
const rootClasses = classNames(
'relative flex justify-center items-center w-3 h-3',
'border rounded-sm overflow-hidden',
'border-vega-clight-500 dark:border-vega-cdark-500',
'aria-checked:border-vega-clight-400 dark:aria-checked:border-vega-cdark-400',
'disabled:border-vega-clight-600 dark:disabled:border-vega-cdark-600',
'bg-vega-clight-700 dark:bg-vega-cdark-700'
);
return (
<div className="flex gap-1.5 items-center">
<CheckboxPrimitive.Root
name={name}
id={name}
className={rootClasses}
checked={checked}
onCheckedChange={onCheckedChange}
disabled={disabled}
data-testid={name}
>
<CheckboxPrimitive.CheckboxIndicator className="flex justify-center items-center w-3 h-3">
{checked === 'indeterminate' ? (
<span
data-testid="indeterminate-icon"
className="absolute w-[8px] h-[2px] bg-vega-clight-50 dark:bg-vega-cdark-50"
/>
) : (
<VegaIcon name={VegaIconNames.TICK} size={10} />
)}
</CheckboxPrimitive.CheckboxIndicator>
</CheckboxPrimitive.Root>
<label
htmlFor={name}
className={classNames('text-xs flex-1', {
'text-vega-clight-200 dark:text-vega-cdark-200': disabled,
})}
>
{label}
</label>
</div>
);
};
@@ -0,0 +1 @@
export * from './checkbox';
@@ -0,0 +1,23 @@
import { render, screen } from '@testing-library/react';
import { TradingFormGroup } from './form-group';
describe('FormGroup', () => {
it('should render label if given a label', () => {
render(
<TradingFormGroup label="label" labelFor="test">
<input id="test"></input>
</TradingFormGroup>
);
expect(screen.getByLabelText('label')).toBeInTheDocument();
});
it('should render children', () => {
render(
<TradingFormGroup label="label" labelFor="test">
<input data-testid="foo" id="test"></input>
</TradingFormGroup>
);
expect(screen.getByTestId('foo')).toBeInTheDocument();
});
});
@@ -0,0 +1,53 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
export interface TradingFormGroupProps {
children: ReactNode;
className?: string;
label: string | ReactNode; // For accessibility reasons this must always be set for screen readers. If you want it to not show, then use the hideLabel prop"
labelFor: string; // Same as above
hideLabel?: boolean;
disabled?: boolean;
labelDescription?: string;
labelAlign?: 'left' | 'right';
compact?: boolean;
}
export const TradingFormGroup = ({
children,
className,
label,
labelFor,
labelDescription,
labelAlign = 'left',
hideLabel = false,
compact = false,
disabled = false,
}: TradingFormGroupProps) => {
const wrapperClasses = classNames(
'relative',
{
'mb-2': compact,
'mb-4': !compact,
},
className
);
const labelClasses = classNames('block mb-2 text-xs', {
'text-right': labelAlign === 'right',
'sr-only': hideLabel,
'text-muted': disabled,
});
return (
<div data-testid="form-group" className={wrapperClasses}>
{label && (
<label htmlFor={labelFor} className={labelClasses}>
{label}
{labelDescription && (
<div className="font-light mt-1">{labelDescription}</div>
)}
</label>
)}
{children}
</div>
);
};
@@ -0,0 +1,47 @@
import type { StoryFn, Meta } from '@storybook/react';
import { TradingInput } from '../trading-input';
import type { TradingFormGroupProps } from './form-group';
import { TradingFormGroup } from './form-group';
export default {
component: TradingFormGroup,
title: 'FormGroup',
argTypes: {
label: {
type: 'string',
},
labelFor: {
type: 'string',
},
labelDescription: {
type: 'string',
},
className: {
type: 'string',
},
hasError: {
type: 'boolean',
},
disabled: {
type: 'boolean',
},
},
} as Meta;
const Template: StoryFn<TradingFormGroupProps> = (args) => (
<TradingFormGroup {...args}>
<TradingInput id="labelFor" />
</TradingFormGroup>
);
export const Default = Template.bind({});
Default.args = {
label: 'Label',
labelFor: 'labelFor',
};
export const WithLabelDescription = Template.bind({});
WithLabelDescription.args = {
label: 'Label',
labelFor: 'labelFor',
labelDescription: 'Description text',
};
@@ -0,0 +1 @@
export * from './form-group';
@@ -0,0 +1 @@
export * from './input-error';
@@ -0,0 +1,10 @@
import { render } from '@testing-library/react';
import { TradingInputError } from './input-error';
describe('InputError', () => {
it('should render successfully', () => {
const { baseElement } = render(<TradingInputError />);
expect(baseElement).toBeTruthy();
});
});
@@ -0,0 +1,20 @@
import type { StoryFn, Meta } from '@storybook/react';
import { TradingInputError } from './input-error';
export default {
component: TradingInputError,
title: 'InputError',
} as Meta;
const Template: StoryFn = (args) => <TradingInputError {...args} />;
export const Danger = Template.bind({});
Danger.args = {
children: 'An error that might have happened',
};
export const Warning = Template.bind({});
Warning.args = {
intent: 'warning',
children: 'Something that might be an issue',
};
@@ -0,0 +1,42 @@
import classNames from 'classnames';
import type { HTMLAttributes } from 'react';
interface TradingInputErrorProps extends HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
intent?: 'danger' | 'warning';
forInput?: string;
testId?: string;
}
export const TradingInputError = ({
intent = 'danger',
children,
forInput,
testId,
className,
...props
}: TradingInputErrorProps) => {
const effectiveClassName = classNames(
'text-xs flex items-center first-letter:uppercase',
'mt-2',
{
'border-danger': intent === 'danger',
'border-warning': intent === 'warning',
},
{
'text-warning': intent === 'warning',
'text-danger': intent === 'danger',
}
);
return (
<div
data-testid={testId || 'input-error-text'}
aria-describedby={forInput}
className={classNames(effectiveClassName, className)}
{...props}
role="alert"
>
{children}
</div>
);
};
@@ -0,0 +1 @@
export * from './input';
@@ -0,0 +1,10 @@
import { render } from '@testing-library/react';
import { TradingInput } from './input';
describe('Input', () => {
it('should render successfully', () => {
const { baseElement } = render(<TradingInput />);
expect(baseElement).toBeTruthy();
});
});
@@ -0,0 +1,83 @@
import type { StoryFn, Meta } from '@storybook/react';
import { TradingInput } from './input';
import { FormGroup } from '../form-group';
export default {
component: TradingInput,
title: 'Input',
} as Meta;
const Template: StoryFn = (args) => (
<FormGroup label="Hello" labelFor={args.id}>
<TradingInput value="I type words" {...args} />
</FormGroup>
);
const customElementPlaceholder = (
<span
style={{
fontFamily: 'monospace',
backgroundColor: 'grey',
padding: '4px',
}}
>
Ω
</span>
);
export const Default = Template.bind({});
Default.args = {
id: 'input-default',
};
export const WithError = Template.bind({});
WithError.args = {
hasError: true,
id: 'input-has-error',
};
export const Disabled = Template.bind({});
Disabled.args = {
disabled: true,
id: 'input-disabled',
};
export const TypeDate = Template.bind({});
TypeDate.args = {
type: 'date',
id: 'input-date',
};
export const TypeDateTime = Template.bind({});
TypeDateTime.args = {
type: 'datetime-local',
id: 'input-datetime-local',
min: '2022-09-05T11:29:17',
max: '2023-09-05T10:29:49',
};
export const IconPrepend = Template.bind({});
IconPrepend.args = {
prependIconName: 'search',
id: 'input-icon-prepend',
};
export const IconAppend = Template.bind({});
IconAppend.args = {
value: 'I type words and even more words',
appendIconName: 'search',
id: 'input-icon-append',
};
export const ElementPrepend = Template.bind({});
ElementPrepend.args = {
value: '<- custom element',
prependElement: customElementPlaceholder,
id: 'input-element-prepend',
};
export const ElementAppend = Template.bind({});
ElementAppend.args = {
value: 'custom element ->',
appendElement: customElementPlaceholder,
id: 'input-element-append',
};
@@ -0,0 +1,174 @@
import type { InputHTMLAttributes, ReactNode } from 'react';
import { forwardRef } from 'react';
import classNames from 'classnames';
import type { IconName } from '../icon';
import { Icon } from '../icon';
import { defaultFormElement } from '../../utils/shared';
type InputRootProps = InputHTMLAttributes<HTMLInputElement> & {
hasError?: boolean;
disabled?: boolean;
className?: string;
};
type NoPrepend = {
prependIconName?: never;
prependIconDescription?: string;
prependElement?: never;
};
type NoAppend = {
appendIconName?: never;
appendIconDescription?: string;
appendElement?: never;
};
type InputPrepend = NoAppend &
(
| NoPrepend
| {
prependIconName: IconName;
prependIconDescription?: string;
prependElement?: never;
}
| {
prependIconName?: never;
prependIconDescription?: never;
prependElement: ReactNode;
}
);
type InputAppend = NoPrepend &
(
| NoAppend
| {
appendIconName: IconName;
appendIconDescription?: string;
appendElement?: never;
}
| {
appendIconName?: never;
appendIconDescription?: never;
appendElement: ReactNode;
}
);
type AffixProps = InputPrepend | InputAppend;
export type TradingInputProps = InputRootProps & AffixProps;
export const tradingInputStyle = ({
style,
disabled,
}: {
style?: React.CSSProperties;
disabled?: boolean;
}) =>
disabled
? {
...style,
backgroundImage:
'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAAXNSR0IArs4c6QAAACNJREFUGFdjtLS0/M8ABcePH2eEsRlJl4BpBdHIuuFmEi0BABqjEQVjx/LTAAAAAElFTkSuQmCC)',
}
: style;
const getAffixElement = ({
prependElement,
prependIconName,
prependIconDescription,
appendElement,
appendIconName,
appendIconDescription,
}: Pick<TradingInputProps, keyof AffixProps>) => {
const position = prependIconName || prependElement ? 'pre' : 'post';
const className = classNames(
['fill-black dark:fill-white', 'absolute', 'z-10'],
{
'left-3': position === 'pre',
'right-3': position === 'post',
}
);
const element = prependElement || appendElement;
const iconName = prependIconName || appendIconName;
const iconDescription = prependIconDescription || appendIconDescription;
if (element) {
return <div className={className}>{element}</div>;
}
if (iconName) {
return (
<Icon
name={iconName}
className={className}
aria-label={iconDescription}
aria-hidden={!iconDescription}
/>
);
}
return null;
};
export const TradingInput = forwardRef<HTMLInputElement, TradingInputProps>(
(
{
prependIconName,
prependIconDescription,
appendIconName,
appendIconDescription,
prependElement,
appendElement,
className,
hasError,
...props
},
ref
) => {
const hasPrepended = !!(prependIconName || prependElement);
const hasAppended = !!(appendIconName || appendElement);
const inputClassName = classNames(
'appearance-none dark:color-scheme-dark px-3 h-8',
className,
{
'pl-9': hasPrepended,
'pr-9': hasAppended,
}
);
const input = (
<input
{...props}
ref={ref}
className={classNames(
defaultFormElement(hasError, props.disabled),
inputClassName
)}
/>
);
const element = getAffixElement({
prependIconName,
prependIconDescription,
appendIconName,
appendIconDescription,
prependElement,
appendElement,
});
if (element) {
return (
<div className="flex items-center relative">
{hasPrepended && element}
{input}
{hasAppended && element}
</div>
);
}
return input;
}
);
@@ -0,0 +1 @@
export * from './radio-group';
@@ -0,0 +1,22 @@
import type { StoryFn, Meta } from '@storybook/react';
import type { TradingRadioGroupProps } from './radio-group';
import { TradingRadioGroup, TradingRadio } from './radio-group';
export default {
component: TradingRadioGroup,
title: 'RadioGroup',
} as Meta;
const Template: StoryFn<TradingRadioGroupProps> = (args) => (
<TradingRadioGroup {...args}>
<TradingRadio id="item-1" value="1" label="Item 1" />
<TradingRadio id="item-2" value="2" label="Item 2" />
<TradingRadio id="item-3" value="3" label="Disabled item" disabled={true} />
</TradingRadioGroup>
);
export const Vertical = Template.bind({});
export const Horizontal = Template.bind({});
Horizontal.args = {
orientation: 'horizontal',
};
@@ -0,0 +1,99 @@
import { forwardRef } from 'react';
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import classNames from 'classnames';
import type { ReactNode } from 'react';
export interface TradingRadioGroupProps {
name?: string;
children: ReactNode;
defaultValue?: string;
value?: string;
orientation?: 'horizontal' | 'vertical';
onChange?: (value: string) => void;
className?: string;
}
export const TradingRadioGroup = forwardRef<
HTMLDivElement,
TradingRadioGroupProps
>(
(
{
children,
name,
value,
orientation = 'vertical',
onChange,
className,
}: TradingRadioGroupProps,
ref
) => {
const groupClasses = classNames(
'flex text-sm',
{
'flex-col gap-2': orientation === 'vertical',
'flex-row gap-4': orientation === 'horizontal',
},
className
);
return (
<RadioGroupPrimitive.Root
ref={ref}
name={name}
value={value}
onValueChange={onChange}
orientation={orientation}
className={groupClasses}
>
{children}
</RadioGroupPrimitive.Root>
);
}
);
interface RadioProps {
id: string;
value: string;
label: string;
disabled?: boolean;
}
export const TradingRadio = ({ id, value, label, disabled }: RadioProps) => {
const wrapperClasses = classNames('flex items-center gap-1.5 text-xs');
const itemClasses = classNames(
'flex justify-center items-center',
'w-3 h-3 rounded-full border',
'border-vega-clight-500 dark:border-vega-cdark-500',
'aria-checked:border-vega-clight-400 dark:aria-checked:border-vega-cdark-400',
'disabled:border-vega-clight-600 dark:disabled:border-vega-cdark-600',
'bg-vega-clight-700 dark:bg-vega-cdark-700'
);
const indicatorClasses = classNames(
'block w-2.5 h-2.5 border-2 rounded-full',
'bg-vega-clight-50 dark:bg-vega-cdark-50',
'border-vega-clight-700 dark:border-vega-cdark-700'
);
return (
<div className={wrapperClasses}>
<RadioGroupPrimitive.Item
value={value}
className={itemClasses}
id={id}
data-testid={id}
disabled={disabled}
>
<RadioGroupPrimitive.Indicator className={indicatorClasses} />
</RadioGroupPrimitive.Item>
<label
htmlFor={id}
className={
disabled
? 'text-vega-clight-200 dark:text-vega-cdark-200'
: 'cursor-pointer'
}
>
{label}
</label>
</div>
);
};
@@ -0,0 +1 @@
export * from './select';
@@ -0,0 +1,37 @@
import { render } from '@testing-library/react';
import { TradingRichSelect, TradingSelect, TradingOption } from './select';
describe('Select', () => {
it('should render successfully', () => {
const { baseElement } = render(<TradingSelect />);
expect(baseElement).toBeTruthy();
});
});
describe('RichSelect', () => {
it('should render select element with placeholder when no value is pre-selected', async () => {
const { findByTestId } = render(
<TradingRichSelect placeholder={'Select'}>
<TradingOption value={'1'}>1</TradingOption>
<TradingOption value={'2'}>2</TradingOption>
</TradingRichSelect>
);
const btn = (await findByTestId(
'rich-select-trigger'
)) as HTMLButtonElement;
expect(btn.textContent).toEqual('Select');
});
it('should render select element with pre-selected value', async () => {
const { findByTestId } = render(
<TradingRichSelect placeholder={'Select'} value={'1'}>
<TradingOption value={'1'}>1</TradingOption>
<TradingOption value={'2'}>2</TradingOption>
</TradingRichSelect>
);
const btn = (await findByTestId(
'rich-select-trigger'
)) as HTMLButtonElement;
expect(btn.textContent).toEqual('1');
});
});
@@ -0,0 +1,92 @@
import type { StoryFn, Meta } from '@storybook/react';
import { TradingOption, TradingSelect, TradingRichSelect } from './select';
import { FormGroup } from '../form-group';
export default {
component: TradingSelect,
title: 'Select',
} as Meta;
const Template: StoryFn = (args) => (
<FormGroup label="Select an option" labelFor={args.id}>
<TradingSelect {...args}>
<option value="Option 1">Option 1</option>
<option value="Option 2">Option 2</option>
<option value="Option 3">Option 3</option>
</TradingSelect>
</FormGroup>
);
const RichSelectTemplate: StoryFn = ({ placeholder, ...props }) => (
<FormGroup label="Select an option" labelFor={props.id}>
<TradingRichSelect placeholder={placeholder} {...props} />
</FormGroup>
);
export const Default = Template.bind({});
Default.args = {
id: 'select-default',
};
export const WithError = Template.bind({});
WithError.args = {
id: 'select-has-error',
hasError: true,
};
export const Disabled = Template.bind({});
Disabled.args = {
id: 'select-disabled',
disabled: true,
};
export const RichDefaultSelect = RichSelectTemplate.bind({});
RichDefaultSelect.args = {
id: 'rich',
name: 'rich',
placeholder: 'Select an option',
onValueChange: (v: string) => {
// eslint-disable-next-line no-console
console.log(v);
},
children: (
<>
<TradingOption value="1">
<div className="flex flex-col justify-start items-start">
<span>Option One</span>
<span className="text-xs">First option</span>
</div>
</TradingOption>
<TradingOption value="2">
<div className="flex flex-col justify-start items-start">
<span>Option Two</span>
<span className="text-xs">Second option</span>
</div>
</TradingOption>
<TradingOption value="3">
<div className="flex flex-col justify-start items-start">
<span>Option Three</span>
<span className="text-xs">Third option</span>
</div>
</TradingOption>
<TradingOption value="4">
<div className="flex flex-col justify-start items-start">
<span>Option Four</span>
<span className="text-xs">Fourth option</span>
</div>
</TradingOption>
<TradingOption value="5">
<div className="flex flex-col justify-start items-start">
<span>Option Five</span>
<span className="text-xs">Fifth option</span>
</div>
</TradingOption>
<TradingOption value="6">
<div className="flex flex-col justify-start items-start">
<span>Option Six</span>
<span className="text-xs">Sixth option</span>
</div>
</TradingOption>
</>
),
};
@@ -0,0 +1,129 @@
import type { Ref, SelectHTMLAttributes } from 'react';
import { useRef } from 'react';
import { forwardRef } from 'react';
import classNames from 'classnames';
import { Icon } from '..';
import { defaultSelectElement } from '../../utils/shared';
import * as SelectPrimitive from '@radix-ui/react-select';
export interface TradingSelectProps
extends SelectHTMLAttributes<HTMLSelectElement> {
hasError?: boolean;
className?: string;
value?: string | number;
children?: React.ReactNode;
}
export const TradingSelect = forwardRef<HTMLSelectElement, TradingSelectProps>(
({ className, hasError, ...props }, ref) => (
<div className="flex items-center relative">
<select
ref={ref}
{...props}
className={classNames(
defaultSelectElement(hasError, props.disabled),
className,
'appearance-none rounded-md'
)}
/>
<Icon
name="chevron-down"
className="absolute right-4 z-10 pointer-events-none"
/>
</div>
)
);
export type TradingRichSelectProps = React.ComponentProps<
typeof SelectPrimitive.Root
> & {
placeholder: string;
hasError?: boolean;
id?: string;
'data-testid'?: string;
};
export const TradingRichSelect = forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
TradingRichSelectProps
>(({ id, children, placeholder, hasError, ...props }, forwardedRef) => {
const containerRef = useRef<HTMLDivElement>();
const contentRef = useRef<HTMLDivElement>();
return (
<div
ref={containerRef as Ref<HTMLDivElement>}
className="flex items-center relative"
>
<SelectPrimitive.Root {...props} defaultOpen={false}>
<SelectPrimitive.Trigger
data-testid={props['data-testid'] || 'rich-select-trigger'}
className={classNames(
defaultSelectElement(hasError, props.disabled),
'rounded-md pl-2 pr-11',
'max-w-full overflow-hidden break-all'
)}
id={id}
ref={forwardedRef}
>
<SelectPrimitive.Value placeholder={placeholder} />
<SelectPrimitive.Icon className={classNames('absolute right-4')}>
<Icon name="chevron-down" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
<SelectPrimitive.Portal container={containerRef.current}>
<SelectPrimitive.Content
ref={contentRef as Ref<HTMLDivElement>}
className={classNames(
'relative',
'z-20',
'bg-white dark:bg-black',
'border border-neutral-500 focus:border-black dark:focus:border-white rounded',
'overflow-hidden',
'shadow-lg'
)}
position={'item-aligned'}
side={'bottom'}
align={'center'}
>
<SelectPrimitive.ScrollUpButton className="flex items-center justify-center py-1 absolute w-full h-6 z-20 bg-gradient-to-t from-transparent to-neutral-50 dark:to-neutral-900">
<Icon name="chevron-up" />
</SelectPrimitive.ScrollUpButton>
<SelectPrimitive.Viewport>{children}</SelectPrimitive.Viewport>
<SelectPrimitive.ScrollDownButton className="flex items-center justify-center py-1 absolute bottom-0 w-full h-6 z-20 bg-gradient-to-b from-transparent to-neutral-50 dark:to-neutral-900">
<Icon name="chevron-down" />
</SelectPrimitive.ScrollDownButton>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
</SelectPrimitive.Root>
</div>
);
});
export const TradingOption = forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentProps<typeof SelectPrimitive.Item>
>(({ children, className, ...props }, forwardedRef) => (
<SelectPrimitive.Item
data-testid="rich-select-option"
className={classNames(
'relative',
'text-black dark:text-white',
'cursor-pointer outline-none',
'hover:bg-neutral-100 dark:hover:bg-neutral-800',
'focus:bg-neutral-100 dark:focus:bg-neutral-800',
'pl-2 py-2',
'pr-12',
'w-full',
'text-sm',
'data-selected:bg-vega-yellow dark:data-selected:text-black dark:data-selected:bg-vega-yellow',
className
)}
{...props}
ref={forwardedRef}
>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator className="absolute right-4 top-[50%] translate-y-[-50%]">
<Icon name="tick" />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
));
+10 -8
View File
@@ -1,18 +1,20 @@
import classnames from 'classnames';
export const defaultSelectElement = (hasError?: boolean) =>
classnames(defaultFormElement(hasError), 'pr-10 dark:bg-black');
export const defaultSelectElement = (hasError?: boolean, disabled?: boolean) =>
classnames(defaultFormElement(hasError, disabled), 'pr-10 min-h-8 py-1');
export const defaultFormElement = (hasError?: boolean) =>
export const defaultFormElement = (hasError?: boolean, disabled?: boolean) =>
classnames(
'flex items-center w-full text-sm',
'p-2 rounded whitespace-nowrap text-ellipsis overflow-hidden',
'bg-transparent',
'border',
'focus:border-vega-light-300 dark:focus:border-vega-dark-300',
'disabled:opacity-60',
'focus:border-vega-clight-400 dark:focus:border-vega-cdark-400',
{
'border-vega-pink text-vega-pink': hasError,
'border-vega-light-200 dark:border-vega-dark-200': !hasError,
'bg-vega-clight-700 dark:bg-vega-cdark-700': !disabled && !hasError,
'bg-transparent': disabled || hasError,
'border-vega-clight-600 dark:border-vega-cdark-600': disabled,
'border-vega-red-500': !disabled && hasError,
'border-vega-clight-500 dark:border-vega-cdark-500':
!disabled && !hasError,
}
);
+2
View File
@@ -44,6 +44,8 @@ describe('number utils', () => {
o: '12,345,678.91234568',
q: '1',
},
// USDT / USDC
{ v: new BigNumber(12345678), d: 6, o: '12.35', q: 1000000 },
])(
'formats with addDecimalsFormatNumberQuantum given number correctly',
({ v, d, o, q }) => {
@@ -2,11 +2,11 @@ import classNames from 'classnames';
import { create } from 'zustand';
import {
Dialog,
FormGroup,
Input,
Intent,
Pill,
TradingButton,
TradingFormGroup,
TradingInput,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
@@ -421,17 +421,17 @@ const CustomUrlInput = ({
<VegaIcon name={VegaIconNames.ARROW_LEFT} /> {t('Go back')}
</button>
</div>
<FormGroup
<TradingFormGroup
labelFor="wallet-url"
label={t('Custom wallet location')}
hideLabel
>
<Input
<TradingInput
value={walletUrl}
onChange={(e) => setWalletUrl(e.target.value)}
name="wallet-url"
/>
</FormGroup>
</TradingFormGroup>
<ConnectionOption
disabled={!isDesktopWalletRunning}
type="jsonRpc"
@@ -1,8 +1,8 @@
import { t } from '@vegaprotocol/i18n';
import {
FormGroup,
Input,
InputError,
TradingFormGroup,
TradingInput,
TradingInputError,
Intent,
TradingButton,
VegaIcon,
@@ -60,8 +60,8 @@ export function ViewConnectorForm({
'Browse from the perspective of another Vega user in read-only mode.'
)}
</p>
<FormGroup label={t('Vega Pubkey')} labelFor="address">
<Input
<TradingFormGroup label={t('Vega Pubkey')} labelFor="address">
<TradingInput
{...register('address', {
required: t('Required'),
validate: validatePubkey,
@@ -71,9 +71,11 @@ export function ViewConnectorForm({
type="text"
/>
{errors.address?.message && (
<InputError intent="danger">{errors.address.message}</InputError>
<TradingInputError intent="danger">
{errors.address.message}
</TradingInputError>
)}
</FormGroup>
</TradingFormGroup>
<TradingButton
data-testid="connect"
intent={Intent.Info}
+23 -17
View File
@@ -12,11 +12,11 @@ import { t } from '@vegaprotocol/i18n';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import {
Button,
FormGroup,
Input,
InputError,
TradingFormGroup,
TradingInput,
TradingInputError,
Notification,
RichSelect,
TradingRichSelect,
ExternalLink,
Intent,
} from '@vegaprotocol/ui-toolkit';
@@ -150,7 +150,7 @@ export const WithdrawForm = ({
field: ControllerRenderProps<FormFields, 'asset'>;
}) => {
return (
<RichSelect
<TradingRichSelect
data-testid="select-asset"
id="asset"
name="asset"
@@ -170,7 +170,7 @@ export const WithdrawForm = ({
balance={<AssetBalance asset={a} />}
/>
))}
</RichSelect>
</TradingRichSelect>
);
};
@@ -193,7 +193,7 @@ export const WithdrawForm = ({
noValidate={true}
data-testid="withdraw-form"
>
<FormGroup label={t('Asset')} labelFor="asset">
<TradingFormGroup label={t('Asset')} labelFor="asset">
<Controller
control={control}
name="asset"
@@ -205,10 +205,12 @@ export const WithdrawForm = ({
render={renderAssetsSelector}
/>
{errors.asset?.message && (
<InputError intent="danger">{errors.asset.message}</InputError>
<TradingInputError intent="danger">
{errors.asset.message}
</TradingInputError>
)}
</FormGroup>
<FormGroup
</TradingFormGroup>
<TradingFormGroup
label={t('To (Ethereum address)')}
labelFor="ethereum-address"
>
@@ -218,15 +220,17 @@ export const WithdrawForm = ({
clearErrors('to');
}}
/>
<Input
<TradingInput
id="ethereum-address"
data-testid="eth-address-input"
{...register('to', { validate: { required, ethereumAddress } })}
/>
{errors.to?.message && (
<InputError intent="danger">{errors.to.message}</InputError>
<TradingInputError intent="danger">
{errors.to.message}
</TradingInputError>
)}
</FormGroup>
</TradingFormGroup>
{selectedAsset && threshold && (
<div className="mb-4">
<WithdrawLimits
@@ -238,8 +242,8 @@ export const WithdrawForm = ({
/>
</div>
)}
<FormGroup label={t('Amount')} labelFor="amount">
<Input
<TradingFormGroup label={t('Amount')} labelFor="amount">
<TradingInput
data-testid="amount-input"
type="number"
autoComplete="off"
@@ -259,7 +263,9 @@ export const WithdrawForm = ({
})}
/>
{errors.amount?.message && (
<InputError intent="danger">{errors.amount.message}</InputError>
<TradingInputError intent="danger">
{errors.amount.message}
</TradingInputError>
)}
{selectedAsset && (
<UseButton
@@ -282,7 +288,7 @@ export const WithdrawForm = ({
/>
</div>
)}
</FormGroup>
</TradingFormGroup>
<Button
data-testid="submit-withdrawal"
type="submit"
+4
View File
@@ -3,6 +3,7 @@
## Closed Markets
- **Must** see market's instrument code (<a name="6001-MARK-001" href="#6001-MARK-001">6001-MARK-001</a>)
- **Must** see market's product type (<a name="6001-MARK-071" href="#6001-MARK-071">6001-MARK-071</a>)
- **Must** see market's instrument name (sometimes labelled 'description') (<a name="6001-MARK-002" href="#6001-MARK-002">6001-MARK-002</a>)
- **Must** see status (<a name="6001-MARK-003" href="#6001-MARK-003">6001-MARK-003</a>)
- **Must** see the settlement date (<a name="6001-MARK-004" href="#6001-MARK-004">6001-MARK-004</a>)
@@ -45,10 +46,12 @@
- **Must** be able to close and open the market selector (<a name="6001-MARK-066" href="#6001-MARK-066">6001-MARK-066</a>)
- **Must** must change color and have + or negative suffix of the price change and change color for the sparkline (<a name="6001-MARK-067" href="#6001-MARK-067">6001-MARK-067</a>)
- **Must** be default tab "All" where there's no filtering by product. (<a name="6001-MARK-070" href="#6001-MARK-070">6001-MARK-070</a>)
- If tab "All" is selected **Must** see product type (<a name="6001-MARK-072" href="#6001-MARK-072">6001-MARK-072</a>)
## All Markets
- **Must** see market's instrument code (<a name="6001-MARK-035" href="#6001-MARK-035">6001-MARK-035</a>)
- **Must** see product type (<a name="6001-MARK-073" href="#6001-MARK-073">6001-MARK-073</a>)
- **Must** see market's instrument name (sometimes labelled 'description') (<a name="6001-MARK-036" href="#6001-MARK-036">6001-MARK-036</a>)
- **Must** see Trading mode (<a name="6001-MARK-037" href="#6001-MARK-037">6001-MARK-037</a>)
- **Must** see status (<a name="6001-MARK-038" href="#6001-MARK-038">6001-MARK-038</a>)
@@ -71,6 +74,7 @@
## Proposed markets
- **Must** see market's instrument code (<a name="6001-MARK-049" href="#6001-MARK-049">6001-MARK-049</a>)
- **Must** see product type (<a name="6001-MARK-074" href="#6001-MARK-074">6001-MARK-074</a>)
- **Must** see market's instrument name (sometimes labelled 'description') (<a name="6001-MARK-050" href="#6001-MARK-050">6001-MARK-050</a>)
- **Must** show the settlement asset (<a name="6001-MARK-051" href="#6001-MARK-051">6001-MARK-051</a>)
- **Must** see state (<a name="6001-MARK-052" href="#6001-MARK-052">6001-MARK-052</a>)
+25 -10
View File
@@ -13,16 +13,31 @@
- **Must** show the asset symbol (<a name="7001-COLL-007" href="#7001-COLL-007">7001-COLL-007</a>)
- **Must** provide a way to see the [full asset details](6501-ASSE-assets.md) (<a name="7001-COLL-008" href="#7001-COLL-008">7001-COLL-008</a>)
- **Must** provide a way to see all accounts, their type, and their balance for a single asset (<a name="7001-COLL-009" href="#7001-COLL-009">7001-COLL-009</a>)
- **Could** have default sort order (<a name="7001-COLL-010" href="#7001-COLL-010">7001-COLL-010</a>)
- General
- Margin
- Bond
- Fees - Maker
- Fees - Liquidity
- Rewards - Maker Paid
- Rewards - Maker Received
- Rewards - Liquidity Provision Received Fees
- Rewards - Market Proposers
- **Could** have default sort order (<a name="7001-COLL-010" href="#7001-COLL-010">7001-COLL-010</a>)
- General
- Margin
- Bond
- Fees - Maker
- Fees - Liquidity
- Rewards - Maker Paid
- Rewards - Maker Received
- Rewards - Liquidity Provision Received Fees
- Rewards - Market Proposers
## Accounts breakdown
- **Must** be able to see a breakdown of accounts for a single asset (<a name="7001-COLL-013" href="#7001-COLL-013">7001-COLL-013</a>)
- **Must** be able to see the total amount of the selected asset (<a name="7001-COLL-014" href="#7001-COLL-014">7001-COLL-014</a>)
- **Must** be able to see market code of margin account (<a name="7001-COLL-015" href="#7001-COLL-015">7001-COLL-015</a>)
- **Must** be able to see product type of the margin account's associated market (<a name="7001-COLL-016" href="#7001-COLL-016">7001-COLL-016</a>)
- **Must** be able to see account type (<a name="7001-COLL-017" href="#7001-COLL-017">7001-COLL-017</a>)
- **Must** be able to see account balance (<a name="7001-COLL-018" href="#7001-COLL-018">7001-COLL-018</a>)
- **Must** be able to see what percentage of that assets total is used by a margin account (<a name="7001-COLL-019" href="#7001-COLL-019">7001-COLL-019</a>)
- **Must** be able to see margin health if its a margin account (<a name="7001-COLL-020" href="#7001-COLL-020">7001-COLL-020</a>)
### Margin health
TODO
## Deal Ticket
+1
View File
@@ -51,6 +51,7 @@ When looking at a list of orders, I...
- **must** see what [market](9001-DATA-data_display.md#market) an order is related to (either code, ID or name, preferable name) (<a name="7003-MORD-002" href="#7003-MORD-002">7003-MORD-002</a>)
- **should** see what the `status` is of the market (particularly if it is not "normal")
- **must** see product type of market's instrument (<a name="7003-MORD-020" href="#7003-MORD-020">7003-MORD-020</a>)
- **must** see the [size](9001-DATA-data_display.md#size) of the order (<a name="7003-MORD-003" href="#7003-MORD-003">7003-MORD-003</a>)
- **must** see the [direction/side](9001-DATA-data_display.md#direction--side) (Long or Short) of the order (this can be implied with a + or negative suffix on the size, + for Long, - for short) (<a name="7003-MORD-004" href="#7003-MORD-004">7003-MORD-004</a>)
- **must** see [order type](9001-DATA-data_display.md#order-type) (<a name="7003-MORD-005" href="#7003-MORD-005">7003-MORD-005</a>)
+28 -13
View File
@@ -4,30 +4,37 @@
- **Must** be able to view the following columns (<a name="7004-POSI-002" href="#7004-POSI-002">7004-POSI-002</a>):
- market
- notional
- open volume
- mark price
- settlement asset
- entry price
- leverage
- margin allocated
- realised PNL
- unrealised PNL
- Updated
- Market
- Code
- Settlement asset symbol
- Product type
- Size / Notional
- Size
- Notional
- Entry / Mark
- Entry
- Mark
- Margin
- Allocated
- Leverage
- Liquidation
- Realised PNL
- Unrealised PNL
- **Must** be able to sort each column by asc and dsc (<a name="7004-POSI-003" href="#7004-POSI-003">7004-POSI-003</a>)
- If the cell shows two values the primary (top value) is used to sort (<a name="7004-POSI-014" href="#7004-POSI-014">7004-POSI-014</a>)
- **Must** be able to scroll horizontally to see all columns if screen resolution isn't wide enough (<a name="7004-POSI-004" href="#7004-POSI-004">7004-POSI-004</a>)
- **Must** be able to drag and drop column names to re order (<a name="7004-POSI-005" href="#7004-POSI-005">7004-POSI-005</a>)
- **Must** be able to remove columns by dragging headers out of the table (<a name="7004-POSI-015" href="#7004-POSI-015">7004-POSI-015</a>)
- **Must** be able to resize the width of the columns (<a name="7004-POSI-006" href="#7004-POSI-006">7004-POSI-006</a>)
- **Must** The columns 'Open volume', 'realised PNL' and 'Unrealised PNL' must change color and have a + or - suffix depending on being positive/negative (<a name="7004-POSI-007" href="#7004-POSI-007">7004-POSI-007</a>)
- **Must** be able to select settlement asset to view asset details (<a name="7004-POSI-008" href="#7004-POSI-008">7004-POSI-008</a>)
- **Must** be able to select the 3 dots to view asset details (<a name="7004-POSI-009" href="#7004-POSI-009">7004-POSI-009</a>)
- **Must** be able to select the close button to close my position (<a name="7004-POSI-010" href="#7004-POSI-010">7004-POSI-010</a>)
@@ -37,3 +44,11 @@
- **Must** retain previous column sizing on refresh (<a name="7004-POSI-012" href="#7004-POSI-012">7004-POSI-012</a>)
- **Must** notional column has a tooltip on hover (<a name="7004-POSI-013" href="#7004-POSI-013">7004-POSI-013</a>)
- **Must** be able to see worst case liquidation price (<a name="7004-POSI-016" href="#7004-POSI-016">7004-POSI-016</a>)
- **Must** be able to see best and worst case liquidation price when hovering (<a name="7004-POSI-017" href="#7004-POSI-017">7004-POSI-017</a>)
- **Must** be able to see if your realised PnL was affected by loss socialisation (<a name="7004-POSI-018" href="#7004-POSI-018">7004-POSI-018</a>)
- **Must** Must be able to see what type of product the position was opened on (<a name="7004-POSI-019" href="#7004-POSI-019">7004-POSI-019</a>)
+4
View File
@@ -57,9 +57,13 @@ The quantum is a value that is used to define "The minimum economically meaningf
## Market
Markets do not have names, technically it is the instrument within a market that has the name. Theoretically the same instrument can be traded in multiple markets. if/when this happens a user needs to be able to disambiguate between markets. Each market does have a unique ID, Note: this is a hash of the definition of the market when it was created.
Instruments have both a Name and Code, see [market framework](../protocol/0001-MKTF-market_framework.md) for how these are used. Generally the Code can save space once a user is familiar with the market. The Name is more descriptive and should be the default when discovering markets. It remains to be seen how the community will use these exactly.
Markets can have several statuses and it may be sensible when listing markets to highlight their status. e.g. if a market is usually in continuous trading mode, but is currently in an auction due to low liquidity. The market name field could be augmented to show the status (add an icon etc).
Near to instrument code or name its product type should be shown using the short name (Futr, Spot, Perp) or if space allows for it, then the long name may be used (Future, Spot, Perpetual).
## Public keys
> aka Party