Compare commits

..
36 changed files with 580 additions and 1131 deletions
+18 -39
View File
@@ -173,7 +173,7 @@ jobs:
uses: docker/build-push-action@v3
continue-on-error: true
id: dockerhub-push
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && endsWith(github.ref, 'main') }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
@@ -238,31 +238,21 @@ jobs:
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
run: |
if echo ${{ github.ref }} | grep -q main; then
# display info about app
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
https://api.fleek.co/graphql
fleek_id='f8f2e051-f18e-49e6-b876-0a39369dc0d8'
elif echo ${{ github.ref }} | grep -q release/testnet; then
# display info about app
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"79baaeca-1952-4ae7-a256-f668cfc1d68e\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"79baaeca-1952-4ae7-a256-f668cfc1d68e\"){id status}}"}' \
https://api.fleek.co/graphql
fleek_id='79baaeca-1952-4ae7-a256-f668cfc1d68e'
fi
# display info about app
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"$fleek_id\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"$fleek_id\"){id status}}"}' \
https://api.fleek.co/graphql
- name: Check out ipfs-redirect
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && endsWith(github.ref, 'main') }}
@@ -329,27 +319,16 @@ jobs:
new_console_url_domain=cf-ipfs.com
# new_console_url_domain=dweb.link
new_console_url="http://${new_cid}.${new_console_url_type}.${new_console_url_domain}/"
new_console_url="https://${new_cid}.${new_console_url_type}.${new_console_url_domain}/"
echo "new_console_url=${new_console_url}"
# Update record in DNSimple
# docs: https://developer.dnsimple.com/v2/zones/records/#updateZoneRecord
dnsimple_account_id=84895
dnsimple_zone_name=fairground.wtf
dnsimple_record_id=45300615
# see: https://dnsimple.com/a/84895/domains/fairground.wtf/records/45300615/edit
curl --fail -H 'Authorization: Bearer ${{ secrets.DNSIMPLE_API_TOKEN }}' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-X PATCH \
-d "{
\"content\": \"${new_console_url}\"
}" \
https://api.dnsimple.com/v2/${dnsimple_account_id}/zones/${dnsimple_zone_name}/records/${dnsimple_record_id}
# trading.fairground.wtf
dnsimple_record_id=45302531
curl --fail -H 'Authorization: Bearer ${{ secrets.DNSIMPLE_API_TOKEN }}' \
dnsimple_record_id=44571545
# see: https://dnsimple.com/a/84895/domains/fairground.wtf/records/44571545/edit
curl -H 'Authorization: Bearer ${{ secrets.DNSIMPLE_API_TOKEN }}' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-X PATCH \
@@ -77,7 +77,7 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
hide={window.innerWidth <= BREAKPOINT_MD}
valueGetter={({
data,
}: VegaValueGetterParams<MarketFieldsFragment>) => {
}: VegaValueGetterParams<MarketFieldsFragment, 'state'>) => {
return data?.state ? MarketStateMapping[data?.state] : '-';
}}
/>
@@ -596,8 +596,6 @@
"noPercentage": "No percentage",
"proposalJson": "Full proposal JSON",
"proposalDetails": "Proposal details",
"marketSpecification": "Market specification",
"viewMarketJson": "View market JSON",
"proposalDescription": "Description",
"currentlySetTo": "Currently expected to ",
"currently": "currently",
@@ -731,11 +729,7 @@
"ThisWillSetValidationDeadlineTo": "This will set the validation deadline to",
"Hours": "hours",
"ThisWillAdd2MinutesToAllowTimeToConfirmInWallet": "Note: 2 minutes of extra time are added when you choose the minimum value. This gives you time to confirm the proposal in your wallet.",
"ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline": "The proposal will fail if enactment is earlier than the voting deadline",
"ProposalWillFailIfEnactmentIsBelowTheMinimumDeadline": "The proposal will fail if enactment deadline is below the minimum",
"ProposalWillFailIfVotingIsBelowTheMinimumDeadline": "The proposal will fail if voting deadline is below the minimum",
"ProposalWillFailIfEnactmentIsAboveTheMaximumDeadline": "The proposal will fail if enactment deadline is above the maximum",
"ProposalWillFailIfVotingIsAboveTheMaximumDeadline": "The proposal will fail if voting deadline is above the maximum",
"ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline": "Proposal will fail if enactment is earlier than the voting deadline",
"SelectAMarketToChange": "Select a market to change",
"MarketName": "Market name",
"MarketCode": "Market code",
@@ -1,6 +0,0 @@
import classnames from 'classnames';
export const collapsibleToggleStyles = (toggleState: boolean) =>
classnames('mb-4 transition-transform ease-in-out duration-300', {
'rotate-180': toggleState,
});
@@ -1,9 +1,9 @@
import ReactMarkdown from 'react-markdown';
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
export const ProposalDescription = ({
description,
@@ -12,6 +12,9 @@ export const ProposalDescription = ({
}) => {
const { t } = useTranslation();
const [showDescription, setShowDescription] = useState(false);
const showDescriptionIconClasses = classnames('mb-4', {
'rotate-180': showDescription,
});
return (
<section data-testid="proposal-description">
@@ -21,7 +24,7 @@ export const ProposalDescription = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('proposalDescription')} />
<div className={collapsibleToggleStyles(showDescription)}>
<div className={showDescriptionIconClasses}>
<Icon name="chevron-down" size={8} />
</div>
</div>
@@ -1,8 +1,8 @@
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
@@ -13,6 +13,9 @@ export const ProposalJson = ({
}) => {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
const showDetailsIconClasses = classnames('mb-4', {
'rotate-180': showDetails,
});
return (
<section data-testid="proposal-json">
@@ -22,7 +25,7 @@ export const ProposalJson = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('proposalJson')} />
<div className={collapsibleToggleStyles(showDetails)}>
<div className={showDetailsIconClasses}>
<Icon name="chevron-down" size={8} />
</div>
</div>
@@ -1 +0,0 @@
export * from './proposal-market-data';
@@ -1,222 +0,0 @@
import isEqual from 'lodash/isEqual';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
InstrumentInfoPanel,
KeyDetailsInfoPanel,
LiquidityMonitoringParametersInfoPanel,
LiquidityPriceRangeInfoPanel,
MetadataInfoPanel,
OracleInfoPanel,
PriceMonitoringBoundsInfoPanel,
RiskFactorsInfoPanel,
RiskModelInfoPanel,
RiskParametersInfoPanel,
SettlementAssetInfoPanel,
} from '@vegaprotocol/markets';
import {
Accordion,
AccordionItem,
Button,
CopyWithTooltip,
Dialog,
Icon,
SyntaxHighlighter,
} from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { MarketInfoWithData } from '@vegaprotocol/markets';
import type { DataSourceDefinition } from '@vegaprotocol/types';
import { create } from 'zustand';
type MarketDataDialogState = {
isOpen: boolean;
open: () => void;
close: () => void;
};
export const useMarketDataDialogStore = create<MarketDataDialogState>(
(set) => ({
isOpen: false,
open: () => set({ isOpen: true }),
close: () => set({ isOpen: false }),
})
);
export const ProposalMarketData = ({
marketData,
}: {
marketData: MarketInfoWithData;
}) => {
const { t } = useTranslation();
const { isOpen, open, close } = useMarketDataDialogStore();
const [showDetails, setShowDetails] = useState(false);
if (!marketData) {
return null;
}
const settlementData = marketData.tradableInstrument.instrument.product
.dataSourceSpecForSettlementData.data as DataSourceDefinition;
const terminationData = marketData.tradableInstrument.instrument.product
.dataSourceSpecForTradingTermination.data as DataSourceDefinition;
const getSigners = (data: DataSourceDefinition) => {
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
const signers = data.sourceType.sourceType.signers || [];
return signers.map(({ signer }) => {
return (
(signer.__typename === 'ETHAddress' && signer.address) ||
(signer.__typename === 'PubKey' && signer.key)
);
});
}
return [];
};
return (
<section className="relative" data-testid="proposal-market-data">
<button
onClick={() => setShowDetails(!showDetails)}
data-testid="proposal-market-data-toggle"
>
<div className="flex items-center gap-3">
<SubHeading title={t('marketSpecification')} />
<div className={collapsibleToggleStyles(showDetails)}>
<Icon name="chevron-down" size={8} />
</div>
</div>
</button>
{showDetails && (
<>
<div className="float-right">
<Button onClick={open} data-testid="view-market-json">
{t('viewMarketJson')}
</Button>
</div>
<div className="mb-10">
<Accordion>
<AccordionItem
itemId="key-details"
title={t('Key details')}
content={<KeyDetailsInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="instrument"
title={t('Instrument')}
content={<InstrumentInfoPanel market={marketData} />}
/>
{isEqual(
getSigners(settlementData),
getSigners(terminationData)
) ? (
<AccordionItem
itemId="oracles"
title={t('Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
/>
}
/>
) : (
<>
<AccordionItem
itemId="settlement-oracle"
title={t('Settlement Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
/>
}
/>
<AccordionItem
itemId="termination-oracle"
title={t('Termination Oracle')}
content={
<OracleInfoPanel market={marketData} type="termination" />
}
/>
</>
)}
<AccordionItem
itemId="settlement-asset"
title={t('Settlement asset')}
content={<SettlementAssetInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="metadata"
title={t('Metadata')}
content={<MetadataInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-model"
title={t('Risk model')}
content={<RiskModelInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-parameters"
title={t('Risk parameters')}
content={<RiskParametersInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-factors"
title={t('Risk factors')}
content={<RiskFactorsInfoPanel market={marketData} />}
/>
{(
marketData.priceMonitoringSettings?.parameters?.triggers || []
).map((_, triggerIndex) => (
<AccordionItem
itemId={`trigger-${triggerIndex}`}
title={t(`Price monitoring bounds ${triggerIndex + 1}`)}
content={
<PriceMonitoringBoundsInfoPanel
market={marketData}
triggerIndex={triggerIndex}
/>
}
/>
))}
<AccordionItem
itemId="liqudity-monitoring-parameters"
title={t('Liquidity monitoring parameters')}
content={
<LiquidityMonitoringParametersInfoPanel market={marketData} />
}
/>
<AccordionItem
itemId="liquidity-price-range"
title={t('Liquidity price range')}
content={<LiquidityPriceRangeInfoPanel market={marketData} />}
/>
</Accordion>
</div>
</>
)}
<Dialog
title={marketData.tradableInstrument.instrument.code}
open={isOpen}
onChange={(isOpen) => (isOpen ? open() : close())}
size="medium"
dataTestId="market-json-dialog"
>
<CopyWithTooltip text={JSON.stringify(marketData)}>
<button className="bg-vega-dark-100 rounded-sm py-2 px-3 mb-4 text-white">
<span>
<Icon name="duplicate" />
</span>
<span className="ml-2">Copy</span>
</button>
</CopyWithTooltip>
<SyntaxHighlighter data={marketData} />
</Dialog>
</section>
);
};
@@ -1,3 +1,4 @@
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -12,7 +13,6 @@ import { SubHeading } from '../../../../components/heading';
import { useVoteInformation } from '../../hooks';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { ProposalType } from '../proposal/proposal';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
@@ -57,6 +57,10 @@ export const ProposalVotesTable = ({
? t('byTokenVote')
: t('byLiquidityVote');
const showDetailsIconClasses = classnames('mb-4', {
'rotate-180': showDetails,
});
return (
<>
<button
@@ -65,7 +69,7 @@ export const ProposalVotesTable = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('voteBreakdown')} />
<div className={collapsibleToggleStyles(showDetails)}>
<div className={showDetailsIconClasses}>
<Icon name="chevron-down" size={8} />
</div>
</div>
@@ -29,6 +29,9 @@ jest.mock('../proposal-change-table', () => ({
jest.mock('../proposal-json', () => ({
ProposalJson: () => <div data-testid="proposal-json"></div>,
}));
jest.mock('../proposal-terms/proposal-terms', () => ({
ProposalTerms: () => <div data-testid="proposal-terms"></div>,
}));
jest.mock('../proposal-votes-table', () => ({
ProposalVotesTable: () => <div data-testid="proposal-votes-table"></div>,
}));
@@ -71,6 +74,7 @@ it('renders each section', async () => {
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument();
expect(screen.getByTestId('proposal-json')).toBeInTheDocument();
expect(screen.getByTestId('proposal-terms')).toBeInTheDocument();
expect(screen.getByTestId('proposal-votes-table')).toBeInTheDocument();
expect(screen.getByTestId('proposal-vote-details')).toBeInTheDocument();
expect(screen.queryByTestId('proposal-list-asset')).not.toBeInTheDocument();
@@ -1,22 +1,22 @@
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { AsyncRenderer, Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalDescription } from '../proposal-description';
import { ProposalChangeTable } from '../proposal-change-table';
import { ProposalJson } from '../proposal-json';
import { ProposalTerms } from '../proposal-terms';
import { ProposalVotesTable } from '../proposal-votes-table';
import { VoteDetails } from '../vote-details';
import { ListAsset } from '../list-asset';
import { Link } from 'react-router-dom';
import Routes from '../../../routes';
import { ProposalMarketData } from '../proposal-market-data';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MarketInfoWithData } from '@vegaprotocol/markets';
import React from 'react';
import { useTranslation } from 'react-i18next';
export enum ProposalType {
PROPOSAL_NEW_MARKET = 'PROPOSAL_NEW_MARKET',
@@ -28,16 +28,11 @@ export enum ProposalType {
}
export interface ProposalProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
newMarketData?: MarketInfoWithData | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
restData: any;
}
export const Proposal = ({
proposal,
restData,
newMarketData,
}: ProposalProps) => {
export const Proposal = ({ proposal, restData }: ProposalProps) => {
const { t } = useTranslation();
const { params, loading, error } = useNetworkParams([
NetworkParams.governance_proposal_market_minVoterBalance,
@@ -102,58 +97,51 @@ export const Proposal = ({
</div>
<ProposalHeader proposal={proposal} isListItem={false} />
<div id="details">
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={
proposal.terms.change.source.withdrawThreshold
}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={proposal.terms.change.source.withdrawThreshold}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
{newMarketData && (
{proposal.terms.change.__typename !== 'NewMarket' &&
proposal.terms.change.__typename !== 'UpdateMarket' &&
proposal.terms.change.__typename !== 'NewFreeform' && (
<div className="mb-4">
<ProposalMarketData marketData={newMarketData} />
<ProposalTerms data={proposal.terms} />
</div>
)}
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
</div>
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
</div>
<div id="voting">
<div className="mb-10">
<RoundedWrapper paddingBottom={true}>
<VoteDetails
proposal={proposal}
proposalType={proposalType}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
params?.spam_protection_voting_min_tokens
}
/>
</RoundedWrapper>
</div>
<div className="mb-4">
<ProposalVotesTable
<div className="mb-10">
<RoundedWrapper paddingBottom={true}>
<VoteDetails
proposal={proposal}
proposalType={proposalType}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
params?.spam_protection_voting_min_tokens
}
/>
</div>
</RoundedWrapper>
</div>
<div className="mb-4">
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
</div>
</section>
</AsyncRenderer>
@@ -224,47 +224,7 @@ describe('Proposal form vote, validation and enactment deadline', () => {
expect(
screen.getByTestId('enactment-before-voting-deadline')
).toHaveTextContent(
'The proposal will fail if enactment is earlier than the voting deadline'
);
});
it('displays error text if the vote deadline is set earlier than the minimum allowed', () => {
renderComponent();
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
fireEvent.change(voteDeadlineInput, { target: { value: 0.01 } });
expect(screen.getByTestId('voting-less-than-min')).toHaveTextContent(
'The proposal will fail if voting deadline is below the minimum'
);
});
it('displays error text if the vote deadline is set later than the maximum allowed', () => {
renderComponent();
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
fireEvent.change(voteDeadlineInput, { target: { value: 100000 } });
expect(screen.getByTestId('voting-greater-than-max')).toHaveTextContent(
'The proposal will fail if voting deadline is above the maximum'
);
});
it('displays error text if the enactment deadline is set earlier than the minimum allowed', () => {
renderComponent();
const enactmentDeadlineInput = screen.getByTestId(
'proposal-enactment-deadline'
);
fireEvent.change(enactmentDeadlineInput, { target: { value: 0.01 } });
expect(screen.getByTestId('enactment-less-than-min')).toHaveTextContent(
'The proposal will fail if enactment deadline is below the minimum'
);
});
it('displays error text if the enactment deadline is set later than the maximum allowed', () => {
renderComponent();
const enactmentDeadlineInput = screen.getByTestId(
'proposal-enactment-deadline'
);
fireEvent.change(enactmentDeadlineInput, { target: { value: 100000 } });
expect(screen.getByTestId('enactment-greater-than-max')).toHaveTextContent(
'The proposal will fail if enactment deadline is above the maximum'
'Proposal will fail if enactment is earlier than the voting deadline'
);
});
});
@@ -220,41 +220,21 @@ const EnactmentForm = ({
<span data-testid="enactment-date" className="pl-2">
{getDateTimeFormat().format(deadlineDates.enactment)}
</span>
{deadlines.enactment && (
<>
{deadlines.enactment === minEnactmentHours && (
<span
data-testid="enactment-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
{deadlines.enactment < deadlines.vote && (
<span
data-testid="enactment-before-voting-deadline"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline')}
</span>
)}
{deadlines.enactment < minEnactmentHours && (
<span
data-testid="enactment-less-than-min"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsBelowTheMinimumDeadline')}
</span>
)}
{deadlines.enactment > maxEnactmentHours && (
<span
data-testid="enactment-greater-than-max"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsAboveTheMaximumDeadline')}
</span>
)}
</>
{deadlines.enactment === minEnactmentHours && (
<span
data-testid="enactment-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
{deadlines.enactment && deadlines.enactment < deadlines.vote && (
<span
data-testid="enactment-before-voting-deadline"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline')}
</span>
)}
</p>
)}
@@ -520,33 +500,13 @@ export function ProposalFormVoteAndEnactmentDeadline({
<span data-testid="voting-date" className="pl-2">
{getDateTimeFormat().format(deadlineDates.vote)}
</span>
{deadlines.vote && (
<>
{deadlines.vote === minVoteHours && (
<span
data-testid="voting-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
{deadlines.vote < minVoteHours && (
<span
data-testid="voting-less-than-min"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfVotingIsBelowTheMinimumDeadline')}
</span>
)}
{deadlines.vote > maxVoteHours && (
<span
data-testid="voting-greater-than-max"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfVotingIsAboveTheMaximumDeadline')}
</span>
)}
</>
{deadlines.vote === minVoteHours && (
<span
data-testid="voting-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
</p>
)}
@@ -1,10 +1,11 @@
import { captureMessage } from '@sentry/minimal';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { VoteValue } from '@vegaprotocol/types';
import { useEffect, useState } from 'react';
import { useUserVoteQuery } from './__generated__/Vote';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import type { FinalizedVote } from '@vegaprotocol/proposals';
import { removePaginationWrapper } from '@vegaprotocol/utils';
export enum VoteState {
NotCast = 'NotCast',
@@ -44,23 +45,21 @@ export function useUserVote(
);
useEffect(() => {
if (finalizedVote?.vote.value && finalizedVote.pubKey === pubKey) {
if (finalizedVote?.vote.value) {
setUserVote(finalizedVote);
} else if (data?.party?.votesConnection?.edges && pubKey) {
const vote = removePaginationWrapper(
data?.party?.votesConnection?.edges
).find(({ proposalId: pId }) => proposalId === pId);
if (vote) {
setUserVote({ ...vote, pubKey });
}
} else if (data?.party?.votesConnection?.edges) {
// This sets the vote (if any) when the user first loads the page
setUserVote(
removePaginationWrapper(data?.party?.votesConnection?.edges).find(
({ proposalId: pId }) => proposalId === pId
)
);
}
}, [
finalizedVote?.vote.value,
data?.party?.votesConnection?.edges,
finalizedVote,
proposalId,
pubKey,
]);
// If user vote changes update the vote state
@@ -95,8 +94,6 @@ export function useUserVote(
return {
voteState,
userVote,
voteDatetime: userVote?.vote?.datetime
? new Date(userVote.vote.datetime)
: null,
voteDatetime: userVote ? new Date(userVote.vote.datetime) : null,
};
}
@@ -7,8 +7,6 @@ import { ProposalNotFound } from '../components/proposal-not-found';
import { useProposalQuery } from './__generated__/Proposal';
import { useFetch } from '@vegaprotocol/react-helpers';
import { ENV } from '../../../config';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketInfoWithDataProvider } from '@vegaprotocol/markets';
export const ProposalContainer = () => {
const params = useParams<{ proposalId: string }>();
@@ -22,36 +20,15 @@ export const ProposalContainer = () => {
skip: !params.proposalId,
});
const {
data: newMarketData,
loading: newMarketLoading,
error: newMarketError,
} = useDataProvider({
dataProvider: marketInfoWithDataProvider,
skipUpdates: true,
variables: {
marketId: data?.proposal?.id || '',
skip: !data?.proposal?.id,
},
});
useEffect(() => {
const interval = setInterval(refetch, 2000);
const interval = setInterval(refetch, 1000);
return () => clearInterval(interval);
}, [refetch]);
return (
<AsyncRenderer
loading={loading || newMarketLoading}
error={error || newMarketError}
data={newMarketData ? { newMarketData, data } : data}
>
<AsyncRenderer loading={loading} error={error} data={data}>
{data?.proposal ? (
<Proposal
proposal={data.proposal}
restData={restData}
newMarketData={newMarketData}
/>
<Proposal proposal={data.proposal} restData={restData} />
) : (
<ProposalNotFound />
)}
@@ -1,8 +1,10 @@
const dialogContent = 'dialog-content';
const nodeHealth = 'node-health';
const statusIncidentsLink = 'footer [data-testid=external-link]';
describe('home', { tags: '@regression' }, () => {
before(() => {
cy.clearAllLocalStorage();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/');
@@ -74,14 +76,23 @@ describe('home', { tags: '@regression' }, () => {
cy.visit('/');
});
// 0006-NETW-002
// 0006-NETW-003
// 0006-NETW-011
it('switch to fairground network and check status & incidents link', () => {
// 0006-NETW-002
// 0006-NETW-003
cy.getByTestId('navigation')
.find('[data-testid="network-switcher"]')
.should('have.text', 'Custom')
.click();
cy.getByTestId('network-item').contains('Fairground testnet');
cy.getByTestId('network-item').contains('Fairground testnet').click();
cy.get('[aria-haspopup="menu"]').should('contain.text', 'Fairground');
cy.url().should('include', 'fairground.wtf');
cy.contains('Continue').click();
cy.get(statusIncidentsLink)
.children('span')
.should('have.text', 'Mainnet status & incidents');
cy.get(statusIncidentsLink)
.should('have.attr', 'href')
.and('contain', 'https://blog.vega.xyz/tagged/vega-incident-reports');
});
});
});
@@ -1,10 +1,6 @@
import * as Schema from '@vegaprotocol/types';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import {
accountsQuery,
amendGeneralAccountBalance,
amendMarginAccountBalance,
} from '@vegaprotocol/mock';
import { accountsQuery, amendGeneralAccountBalance } from '@vegaprotocol/mock';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import { createOrder } from '../support/create-order';
@@ -16,9 +12,8 @@ describe(
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
let accounts = accountsQuery();
accounts = amendMarginAccountBalance(accounts, 'market-0', '1000');
accounts = amendGeneralAccountBalance(accounts, 'market-0', '0');
const accounts = accountsQuery();
amendGeneralAccountBalance(accounts, 'market-0', '0');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
@@ -28,11 +23,6 @@ describe(
});
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
@@ -50,9 +40,8 @@ describe(
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
let accounts = accountsQuery();
accounts = amendMarginAccountBalance(accounts, 'market-0', '1000');
accounts = amendGeneralAccountBalance(accounts, 'market-0', '1');
const accounts = accountsQuery();
amendGeneralAccountBalance(accounts, 'market-0', '1');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
@@ -5,10 +5,6 @@ import {
toggleMarket,
} from '../support/deal-ticket';
const tooltipContent = 'tooltip-content';
const reduceOnly = 'reduce-only';
const postOnly = 'post-only';
describe('time in force validation', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
@@ -126,18 +122,13 @@ describe('time in force validation', { tags: '@smoke' }, () => {
// 7002-SORD-026
it(`post and reduce order market for ${tif.code}`, function () {
// 7003-SORD-054
// 7003-SORD-055
// 7003-SORD-056
// 7003-SORD-057
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
cy.getByTestId(postOnly).should('be.disabled');
cy.getByTestId(reduceOnly).should('be.enabled');
cy.getByTestId('post-only').should('be.disabled');
cy.getByTestId('reduce-only').should('be.enabled');
});
});
});
@@ -153,33 +144,14 @@ describe('time in force validation', { tags: '@smoke' }, () => {
validTIFLimit.forEach((tif) => {
it(`post and reduce order for limit ${tif.code}`, function () {
// 7003-SORD-054
// 7003-SORD-055
// 7003-SORD-056
// 7003-SORD-057
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
cy.getByTestId(postOnly).should('be.enabled');
cy.getByTestId(reduceOnly).should('be.disabled');
cy.getByTestId('post-only').should('be.enabled');
cy.getByTestId('reduce-only').should('be.disabled');
});
});
it(`can see explanation of what post only and reduce only is/does`, function () {
// 7003-SORD-058
cy.get('[for="post-only"]').should('have.text', 'Post only').realHover();
cy.getByTestId(tooltipContent).should(
'contain.text',
`"Post only" will ensure the order is not filled immediately but is placed on the order book as a passive order. When the order is processed it is either stopped (if it would not be filled immediately), or placed in the order book as a passive order until the price taker matches with it.`
);
cy.get('[for="reduce-only"]')
.should('have.text', 'Reduce only')
.realHover();
cy.getByTestId(tooltipContent).should(
'contain.text',
`"Reduce only" can be used only with non-persistent orders, such as "Fill or Kill" or "Immediate or Cancel".`
);
});
});
});
@@ -1,79 +1,32 @@
const colHeader = '.ag-header-cell-text';
const colIdPrice = '[col-id=price]';
const colIdSize = '[col-id=size]';
const colIdCreatedAt = '[col-id=createdAt]';
const tradesTab = 'Trades';
const tradesTable = 'tab-trades';
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
});
describe('trades', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
});
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.getByTestId(tradesTab).click();
});
const colIdPrice = 'price';
const colIdSize = 'size';
const colIdCreatedAt = 'createdAt';
it('show trades', () => {
// 6005-THIS-001
// 6005-THIS-002
cy.getByTestId(tradesTab).should('be.visible');
cy.getByTestId(tradesTable).should('be.visible');
cy.getByTestId(tradesTable).should('not.be.empty');
});
it('renders trades', () => {
cy.getByTestId('Trades').click();
cy.getByTestId('tab-trades').should('be.visible');
it('show trades prices', () => {
// 6005-THIS-003
cy.get(`${colIdPrice} ${colHeader}`).first().should('have.text', 'Price');
cy.get(colIdPrice).each(($tradePrice) => {
cy.get(`[col-id=${colIdPrice}]`).each(($tradePrice) => {
cy.wrap($tradePrice).invoke('text').should('not.be.empty');
});
});
it('show trades sizes', () => {
// 6005-THIS-004
cy.get(`${colIdSize} ${colHeader}`).first().should('have.text', 'Size');
cy.get(colIdSize).each(($tradeSize) => {
cy.get(`[col-id=${colIdSize}]`).each(($tradeSize) => {
cy.wrap($tradeSize).invoke('text').should('not.be.empty');
});
});
it('show trades date and time', () => {
// 6005-THIS-005
cy.get(`${colIdCreatedAt} ${colHeader}`).should('have.text', 'Created at');
const dateTimeRegex =
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
cy.get(colIdCreatedAt).each(($tradeDateTime, index) => {
cy.get(`[col-id=${colIdCreatedAt}]`).each(($tradeDateTime, index) => {
if (index != 0) {
//ignore header
cy.wrap($tradeDateTime).invoke('text').should('match', dateTimeRegex);
}
});
});
it('trades are sorted descending by datetime', () => {
// 6005-THIS-006
const dateTimes: Date[] = [];
cy.get(colIdCreatedAt)
.each(($tradeDateTime, index) => {
if (index != 0) {
//ignore header
dateTimes.push(new Date($tradeDateTime.text()));
}
})
.then(() => {
expect(dateTimes).to.deep.equal(
dateTimes.sort((a, b) => b.getTime() - a.getTime())
);
});
});
it('copy price to deal ticket form', () => {
// 6005-THIS-007
cy.get(colIdPrice).last().click();
cy.getByTestId('order-price').should('have.value', '171.16898');
});
});
@@ -22,7 +22,7 @@ export const DepositsContainer = () => {
<div className="h-full">
<div className="h-full relative">
<DepositsTable
rowData={data}
rowData={data || []}
ref={gridRef}
{...bottomPlaceholderProps}
overlayNoRowsTemplate={error ? error.message : t('No deposits')}
+13 -47
View File
@@ -151,53 +151,19 @@ export const amendGeneralAccountBalance = (
marketId: string,
balance: string
) => {
if (!accounts.party?.accountsConnection?.edges) {
return accounts;
}
const marginAccount = accounts.party?.accountsConnection?.edges?.find(
(edge) => edge?.node.market?.id === marketId
);
if (marginAccount) {
const edges = accounts.party.accountsConnection.edges.map((edge) =>
edge?.node.asset.id === marginAccount.node.asset.id && !edge?.node.market
? { ...edge, node: { ...edge.node, balance } }
: edge
if (accounts.party?.accountsConnection?.edges) {
const marginAccount = accounts.party.accountsConnection.edges.find(
(edge) => edge?.node.market?.id === marketId
);
return {
...accounts,
party: {
...accounts.party,
accountsConnection: {
...accounts.party.accountsConnection,
edges,
},
},
};
if (marginAccount) {
const generalAccount = accounts.party.accountsConnection.edges.find(
(edge) =>
edge?.node.asset.id === marginAccount.node.asset.id &&
!edge?.node.market
);
if (generalAccount) {
generalAccount.node.balance = balance;
}
}
}
return accounts;
};
export const amendMarginAccountBalance = (
accounts: AccountsQuery,
marketId: string,
balance: string
) => {
if (!accounts.party?.accountsConnection?.edges) {
return accounts;
}
const edges = accounts.party?.accountsConnection?.edges?.map((edge) =>
edge?.node.market?.id === marketId
? { ...edge, node: { ...edge?.node, balance } }
: edge
);
return {
...accounts,
party: {
...accounts.party,
accountsConnection: {
...accounts.party.accountsConnection,
edges,
},
},
};
};
+4 -6
View File
@@ -24,13 +24,11 @@ export type VegaValueFormatterParams<TRow, TField extends Field> = RowHelper<
TField
>;
export type VegaValueGetterParams<TRow> = Omit<
export type VegaValueGetterParams<TRow, TField extends Field> = RowHelper<
ValueGetterParams,
'data' | 'node'
> & {
data?: TRow;
node: (Omit<RowNode, 'data'> & { data?: TRow }) | null;
};
TRow,
TField
>;
export type VegaICellRendererParams<TRow, TField extends Field = string> = Omit<
RowHelper<ICellRendererParams, TRow, TField>,
@@ -1,6 +1,6 @@
import { formatRange, formatValue } from './range';
import { formatRange, formatValue } from './deal-ticket-fee-details';
describe('formatValue', () => {
describe('formatRange, formatValue', () => {
it.each([
{ v: 123000, d: 5, o: '1.23' },
{ v: 123000, d: 3, o: '123.00' },
@@ -35,8 +35,7 @@ describe('formatValue', () => {
expect(formatValue(v.toString(), d, q)).toStrictEqual(o);
}
);
});
describe('formatRange', () => {
it.each([
{ min: 123000, max: 12300011111, d: 5, o: '1.23 - 123,000.111', q: '0.1' },
{
@@ -11,7 +11,11 @@ import type { EstimatePositionQuery } from '@vegaprotocol/positions';
import type { EstimateFeesQuery } from '../../hooks/__generated__/EstimateOrder';
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
import { formatRange, formatValue } from '@vegaprotocol/utils';
import {
addDecimalsFormatNumber,
isNumeric,
addDecimalsFormatNumberQuantum,
} from '@vegaprotocol/utils';
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider';
@@ -27,6 +31,32 @@ import {
const emptyValue = '-';
export const formatValue = (
value: string | number | null | undefined,
formatDecimals: number,
quantum?: string
): string => {
if (!isNumeric(value)) return emptyValue;
if (!quantum) return addDecimalsFormatNumber(value, formatDecimals);
return addDecimalsFormatNumberQuantum(value, formatDecimals, quantum);
};
export const formatRange = (
min: string | number | null | undefined,
max: string | number | null | undefined,
formatDecimals: number,
quantum?: string
) => {
const minFormatted = formatValue(min, formatDecimals, quantum);
const maxFormatted = formatValue(max, formatDecimals, quantum);
if (minFormatted !== maxFormatted) {
return `${minFormatted} - ${maxFormatted}`;
}
if (minFormatted !== emptyValue) {
return minFormatted;
}
return maxFormatted;
};
export interface DealTicketFeeDetailPros {
label: string;
value?: string | null | undefined;
@@ -202,6 +232,7 @@ export const DealTicketFeeDetails = ({
}
let liquidationPriceEstimate = emptyValue;
let liquidationPriceEstimateFormatted;
if (liquidationEstimate) {
const liquidationEstimateBestCaseIncludingBuyOrders = BigInt(
@@ -228,6 +259,17 @@ export const DealTicketFeeDetails = ({
? liquidationEstimateWorstCaseIncludingBuyOrders
: liquidationEstimateWorstCaseIncludingSellOrders;
liquidationPriceEstimate = formatRange(
(liquidationEstimateBestCase < liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
(liquidationEstimateBestCase > liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
assetDecimals
);
liquidationPriceEstimateFormatted = formatRange(
(liquidationEstimateBestCase < liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
@@ -237,8 +279,7 @@ export const DealTicketFeeDetails = ({
: liquidationEstimateWorstCase
).toString(),
assetDecimals,
undefined,
market.decimalPlaces
quantum
);
}
@@ -335,7 +376,8 @@ export const DealTicketFeeDetails = ({
{projectedMargin}
<DealTicketFeeDetail
label={t('Liquidation price estimate')}
formattedValue={liquidationPriceEstimate}
value={liquidationPriceEstimate}
formattedValue={liquidationPriceEstimateFormatted}
symbol={assetSymbol}
labelDescription={LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT}
/>
@@ -195,7 +195,7 @@ export const DealTicket = ({
}
const hasNoBalance =
!BigInt(generalAccountBalance) && !BigInt(marginAccountBalance);
!generalAccountBalance || !BigInt(generalAccountBalance);
if (hasNoBalance) {
setError('summary', {
message: SummaryValidationType.NoCollateral,
@@ -219,7 +219,6 @@ export const DealTicket = ({
marketState,
marketTradingMode,
generalAccountBalance,
marginAccountBalance,
pubKey,
setError,
clearErrors,
+2
View File
@@ -6,6 +6,7 @@ import {
truncateByChars,
isNumeric,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type {
@@ -24,6 +25,7 @@ export const DepositsTable = forwardRef<
return (
<AgGrid
ref={ref}
overlayNoRowsTemplate={t('No deposits')}
defaultColDef={{ resizable: true }}
style={{ width: '100%', height: '100%' }}
suppressCellFocus={true}
@@ -76,7 +76,9 @@ export const useColumnDefs = ({ onMarketClick }: Props) => {
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.bestBidPrice'>) => {
return data?.data?.bestBidPrice === undefined
? undefined
: toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber();
@@ -100,7 +102,12 @@ export const useColumnDefs = ({ onMarketClick }: Props) => {
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
valueGetter: ({
data,
}: VegaValueGetterParams<
MarketMaybeWithData,
'data.bestOfferPrice'
>) => {
return data?.data?.bestOfferPrice === undefined
? undefined
: toBigNum(
@@ -127,7 +134,9 @@ export const useColumnDefs = ({ onMarketClick }: Props) => {
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.markPrice'>) => {
return data?.data?.markPrice === undefined
? undefined
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();
@@ -1,60 +0,0 @@
import { useEstimatePositionQuery } from './__generated__/Positions';
import { formatRange } from '@vegaprotocol/utils';
export const LiquidationPrice = ({
marketId,
openVolume,
collateralAvailable,
decimalPlaces,
formatDecimals,
}: {
marketId: string;
openVolume: string;
collateralAvailable: string;
decimalPlaces: number;
formatDecimals: number;
}) => {
const { data: currentData, previousData } = useEstimatePositionQuery({
variables: {
marketId,
openVolume,
collateralAvailable,
},
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
);
}
return <span data-testid="liquidation-price">{value}</span>;
};
@@ -34,7 +34,6 @@ export interface Position {
averageEntryPrice: string;
currentLeverage: number | undefined;
decimals: number;
quantum: string;
lossSocializationAmount: string;
marginAccountBalance: string;
marketDecimalPlaces: number;
@@ -74,7 +73,6 @@ export const getMetrics = (
decimals,
id: assetId,
symbol: assetSymbol,
quantum,
} = market.tradableInstrument.instrument.product.settlementAsset;
const generalAccount = accounts?.find(
(account) =>
@@ -116,7 +114,6 @@ export const getMetrics = (
averageEntryPrice: position.averageEntryPrice,
currentLeverage: currentLeverage ? currentLeverage.toNumber() : undefined,
decimals,
quantum,
lossSocializationAmount: position.lossSocializationAmount || '0',
marginAccountBalance: marginAccount?.balance ?? '0',
marketDecimalPlaces,
@@ -7,12 +7,6 @@ import * as Schema from '@vegaprotocol/types';
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import type { ICellRendererParams } from 'ag-grid-community';
jest.mock('./liquidation-price', () => ({
LiquidationPrice: () => (
<span data-testid="liquidation-price">liquidation price</span>
),
}));
const singleRow: Position = {
partyId: 'partyId',
assetId: 'asset-id',
@@ -20,7 +14,6 @@ const singleRow: Position = {
averageEntryPrice: '133',
currentLeverage: 1.1,
decimals: 2,
quantum: '0.1',
lossSocializationAmount: '0',
marginAccountBalance: '12345600',
marketDecimalPlaces: 1,
@@ -55,7 +48,7 @@ it('render correct columns', async () => {
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(12);
expect(headers).toHaveLength(11);
expect(
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual([
@@ -63,7 +56,6 @@ it('render correct columns', async () => {
'Notional',
'Open volume',
'Mark price',
'Liquidation price',
'Settlement asset',
'Entry price',
'Leverage',
@@ -151,20 +143,12 @@ it('displays mark price', async () => {
expect(cells[3].textContent).toEqual('-');
});
it('displays liquidation price', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
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} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[7].textContent).toEqual('1.1');
expect(cells[6].textContent).toEqual('1.1');
});
it('displays allocated margin', async () => {
@@ -172,7 +156,7 @@ it('displays allocated margin', async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[8];
const cell = cells[7];
expect(cell.textContent).toEqual('123,456.00');
});
@@ -181,7 +165,7 @@ it('displays realised and unrealised PNL', async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[10].textContent).toEqual('4.56');
expect(cells[9].textContent).toEqual('4.56');
});
it('displays close button', async () => {
@@ -198,7 +182,7 @@ it('displays close button', async () => {
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[12].textContent).toEqual('Close');
expect(cells[11].textContent).toEqual('Close');
});
it('do not display close button if openVolume is zero', async () => {
@@ -214,7 +198,7 @@ it('do not display close button if openVolume is zero', async () => {
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[12].textContent).toEqual('');
expect(cells[11].textContent).toEqual('');
});
describe('PNLCell', () => {
+325 -336
View File
@@ -1,7 +1,7 @@
import classNames from 'classnames';
import { forwardRef, useMemo } from 'react';
import { forwardRef } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import type { ColDef } from 'ag-grid-community';
import type { CellRendererSelectorResult } from 'ag-grid-community';
import type {
VegaValueFormatterParams,
VegaValueGetterParams,
@@ -33,6 +33,7 @@ import {
addDecimalsFormatNumber,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react';
import type { Position } from './positions-data-providers';
import * as Schema from '@vegaprotocol/types';
@@ -42,7 +43,6 @@ import { DocsLinks } from '@vegaprotocol/environment';
import { PositionTableActions } from './position-actions-dropdown';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { LiquidationPrice } from './liquidation-price';
interface Props extends TypedDataAgGrid<Position> {
onClose?: (data: Position) => void;
@@ -121,344 +121,333 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
MarketNameCell,
}}
{...props}
columnDefs={useMemo<ColDef[]>(() => {
const columnDefs: (ColDef | null)[] = [
multipleKeys
? {
headerName: t('Vega key'),
field: 'partyId',
valueGetter: ({ data }: VegaValueGetterParams<Position>) =>
(data?.partyId &&
pubKeys &&
pubKeys.find((key) => key.publicKey === data.partyId)
?.name) ||
data?.partyId,
minWidth: 190,
}
: null,
{
headerName: t('Market'),
field: 'marketName',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'marketId', onMarketClick },
minWidth: 190,
},
{
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();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'notional'>) => {
return !data || !data.notional
? '-'
: addDecimalsFormatNumber(
data.notional,
data.marketDecimalPlaces
);
},
minWidth: 80,
},
{
headerName: t('Open volume'),
field: 'openVolume',
type: 'rightAligned',
cellClass: 'font-mono text-right',
cellClassRules: signedNumberCssClassRules,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return data?.openVolume === undefined
? undefined
: toBigNum(
data?.openVolume,
data.positionDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'openVolume'>): string => {
return data?.openVolume === undefined
? ''
: volumePrefix(
addDecimalsFormatNumber(
data.openVolume,
data.positionDecimalPlaces
)
);
},
cellRenderer: OpenVolumeCell,
minWidth: 100,
},
{
headerName: t('Mark price'),
field: 'markPrice',
type: 'rightAligned',
cellRenderer: PriceFlashCell,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data ||
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
? undefined
: toBigNum(
data.markPrice,
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data) {
return '';
}
if (
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
) {
return '-';
}
return addDecimalsFormatNumber(
data.markPrice,
>
{multipleKeys ? (
<AgGridColumn
headerName={t('Vega key')}
field="partyId"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'partyId'>) =>
(data?.partyId &&
pubKeys &&
pubKeys.find((key) => key.publicKey === data.partyId)?.name) ||
data?.partyId
}
minWidth={190}
/>
) : null}
<AgGridColumn
headerName={t('Market')}
field="marketName"
cellRenderer="MarketNameCell"
cellRendererParams={{ idPath: 'marketId', onMarketClick }}
minWidth={190}
/>
<AgGridColumn
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, 'notional'>) => {
return !data?.notional
? undefined
: toBigNum(data.notional, data.marketDecimalPlaces).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'notional'>) => {
return !data || !data.notional
? '-'
: addDecimalsFormatNumber(
data.notional,
data.marketDecimalPlaces
);
},
minWidth: 100,
},
{
headerName: t('Liquidation price'),
colId: 'liquidationPrice',
type: 'rightAligned',
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<LiquidationPrice
marketId={data.marketId}
openVolume={data.openVolume}
collateralAvailable={data.totalBalance}
decimalPlaces={data.decimals}
formatDecimals={data.marketDecimalPlaces}
/>
}}
minWidth={80}
/>
<AgGridColumn
headerName={t('Open volume')}
field="openVolume"
type="rightAligned"
cellClass="font-mono text-right"
cellClassRules={signedNumberCssClassRules}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'openVolume'>) => {
return data?.openVolume === undefined
? undefined
: toBigNum(
data?.openVolume,
data.positionDecimalPlaces
).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'openVolume'>):
| string
| undefined => {
return data?.openVolume === undefined
? undefined
: volumePrefix(
addDecimalsFormatNumber(
data.openVolume,
data.positionDecimalPlaces
)
);
},
},
{
headerName: t('Settlement asset'),
field: 'assetSymbol',
colId: 'asset',
minWidth: 100,
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<ButtonLink
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(
}}
cellRenderer={OpenVolumeCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Mark price')}
field="markPrice"
type="rightAligned"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'markPrice'>) => {
return !data ||
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
? undefined
: toBigNum(data.markPrice, data.marketDecimalPlaces).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data) {
return undefined;
}
if (
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
) {
return '-';
}
return addDecimalsFormatNumber(
data.markPrice,
data.marketDecimalPlaces
);
}}
minWidth={100}
/>
<AgGridColumn
headerName={t('Settlement asset')}
field="assetSymbol"
colId="asset"
minWidth={100}
cellRenderer={({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(data.assetId, e.target as HTMLElement);
}}
>
{data?.assetSymbol}
</ButtonLink>
);
}}
/>
<AgGridColumn
headerName={t('Entry price')}
field="averageEntryPrice"
type="rightAligned"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'averageEntryPrice'>) => {
return data?.markPrice === undefined || !data
? undefined
: toBigNum(
data.averageEntryPrice,
data.marketDecimalPlaces
);
},
minWidth: 100,
},
multipleKeys
? null
: {
headerName: t('Leverage'),
field: 'currentLeverage',
type: 'rightAligned',
filter: 'agNumberColumnFilter',
cellRenderer: PriceFlashCell,
valueFormatter: ({
value,
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined
? ''
: formatNumber(value.toString(), 1),
minWidth: 100,
},
multipleKeys
? null
: {
headerName: t('Margin allocated'),
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
);
},
minWidth: 100,
},
{
headerName: t('Realised PNL'),
field: 'realisedPNL',
type: 'rightAligned',
cellClassRules: signedNumberCssClassRules,
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.realisedPNL, data.decimals).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
return !data
? ''
: addDecimalsFormatNumber(data.realisedPNL, data.decimals);
},
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.'
),
cellRenderer: PNLCell,
minWidth: 100,
},
{
headerName: t('Unrealised PNL'),
field: 'unrealisedPNL',
type: 'rightAligned',
cellClassRules: signedNumberCssClassRules,
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
!data
? ''
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals),
headerTooltip: t(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
),
cellRenderer: PNLCell,
minWidth: 100,
},
{
headerName: t('Updated'),
field: 'updatedAt',
type: 'rightAligned',
filter: DateRangeFilter,
valueFormatter: ({
value,
}: VegaValueFormatterParams<Position, 'updatedAt'>) => {
if (!value) {
return '';
}
return getDateTimeFormat().format(new Date(value));
},
minWidth: 150,
},
onClose && !isReadOnly
? {
...COL_DEFS.actions,
cellRenderer: ({
data,
}: VegaICellRendererParams<Position>) => {
return (
<div className="flex gap-2 items-center justify-end">
{data?.openVolume &&
data?.openVolume !== '0' &&
data.partyId === pubKey ? (
<ButtonLink
data-testid="close-position"
onClick={() => data && onClose(data)}
>
{t('Close')}
</ButtonLink>
) : null}
{data?.assetId && (
<PositionTableActions assetId={data?.assetId} />
)}
</div>
);
},
minWidth: 90,
maxWidth: 90,
}
: null,
];
return columnDefs.filter<ColDef>(
(colDef: ColDef | null): colDef is ColDef => colDef !== null
);
}, [
isReadOnly,
multipleKeys,
onClose,
onMarketClick,
openAssetDetailsDialog,
pubKey,
pubKeys,
])}
/>
).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'averageEntryPrice'>):
| string
| undefined => {
if (!data) {
return undefined;
}
return addDecimalsFormatNumber(
data.averageEntryPrice,
data.marketDecimalPlaces
);
}}
minWidth={100}
/>
{multipleKeys ? null : (
<AgGridColumn
headerName={t('Leverage')}
field="currentLeverage"
type="rightAligned"
filter="agNumberColumnFilter"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
valueFormatter={({
value,
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined
? undefined
: formatNumber(value.toString(), 1)
}
minWidth={100}
/>
)}
{multipleKeys ? null : (
<AgGridColumn
headerName={t('Margin allocated')}
field="marginAccountBalance"
type="rightAligned"
filter="agNumberColumnFilter"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'marginAccountBalance'>) => {
return !data
? undefined
: toBigNum(data.marginAccountBalance, data.decimals).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>):
| string
| undefined => {
if (!data) {
return undefined;
}
return addDecimalsFormatNumber(
data.marginAccountBalance,
data.decimals
);
}}
minWidth={100}
/>
)}
<AgGridColumn
headerName={t('Realised PNL')}
field="realisedPNL"
type="rightAligned"
cellClassRules={signedNumberCssClassRules}
cellClass="font-mono text-right"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'realisedPNL'>) => {
return !data
? undefined
: toBigNum(data.realisedPNL, data.decimals).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
return !data
? undefined
: addDecimalsFormatNumber(data.realisedPNL, data.decimals);
}}
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.'
)}
cellRenderer={PNLCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Unrealised PNL')}
field="unrealisedPNL"
type="rightAligned"
cellClassRules={signedNumberCssClassRules}
cellClass="font-mono text-right"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'unrealisedPNL'>) => {
return !data
? undefined
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
!data
? undefined
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals)
}
headerTooltip={t(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
)}
cellRenderer={PNLCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Updated')}
field="updatedAt"
type="rightAligned"
filter={DateRangeFilter}
valueFormatter={({
value,
}: VegaValueFormatterParams<Position, 'updatedAt'>) => {
if (!value) {
return value;
}
return getDateTimeFormat().format(new Date(value));
}}
minWidth={150}
/>
{onClose && !isReadOnly ? (
<AgGridColumn
{...COL_DEFS.actions}
cellRenderer={({ data }: VegaICellRendererParams<Position>) => {
return (
<div className="flex gap-2 items-center justify-end">
{data?.openVolume &&
data?.openVolume !== '0' &&
data.partyId === pubKey ? (
<ButtonLink
data-testid="close-position"
onClick={() => data && onClose(data)}
>
{t('Close')}
</ButtonLink>
) : null}
{data?.assetId && (
<PositionTableActions assetId={data?.assetId} />
)}
</div>
);
}}
minWidth={90}
maxWidth={90}
/>
) : null}
</AgGrid>
);
}
);
@@ -1,2 +1 @@
export * from './use-vote-submit';
export * from './__generated__/VoteSubsciption';
@@ -5,16 +5,15 @@ import { useVoteEvent } from './use-vote-event';
import type { VoteValue } from '@vegaprotocol/types';
import type { VoteEventFieldsFragment } from './__generated__/VoteSubsciption';
export type FinalizedVote = VoteEventFieldsFragment & { pubKey: string };
export type FinalizedVote = VoteEventFieldsFragment;
export const useVoteSubmit = () => {
const { pubKey } = useVegaWallet();
const { send, transaction, setComplete, Dialog } = useVegaTransaction();
const waitForVoteEvent = useVoteEvent(transaction);
const [finalizedVote, setFinalizedVote] = useState<FinalizedVote | null>(
null
);
const [finalizedVote, setFinalizedVote] =
useState<VoteEventFieldsFragment | null>(null);
const submit = useCallback(
async (voteValue: VoteValue, proposalId: string | null) => {
@@ -34,7 +33,7 @@ export const useVoteSubmit = () => {
if (res) {
waitForVoteEvent(proposalId, pubKey, (v) => {
setFinalizedVote({ ...v, pubKey });
setFinalizedVote(v);
setComplete();
});
}
+8 -8
View File
@@ -53,10 +53,10 @@ export const tradesUpdateSubscription = (
const trades: TradeFieldsFragment[] = [
{
id: 'FFFFAD1BF47AA2853E5C375B6B3A62375F62D5B10807583D32EF3119CC455CD1',
price: '17106734',
size: '18',
createdAt: '2022-04-07T17:56:47.997938583Z',
id: 'FFFFBC80005C517A10ACF481F7E6893769471098E696D0CC407F18134044CB16',
price: '17116898',
size: '24',
createdAt: '2022-04-06T16:19:42.692598951Z',
aggressor: Side.SIDE_BUY,
market: {
id: 'market-0',
@@ -77,10 +77,10 @@ const trades: TradeFieldsFragment[] = [
__typename: 'Trade',
},
{
id: 'FFFFBC80005C517A10ACF481F7E6893769471098E696D0CC407F18134044CB16',
price: '17116898',
size: '24',
createdAt: '2022-04-06T16:19:42.692598951Z',
id: 'FFFFAD1BF47AA2853E5C375B6B3A62375F62D5B10807583D32EF3119CC455CD1',
price: '17106734',
size: '18',
createdAt: '2022-04-07T17:56:47.997938583Z',
aggressor: Side.SIDE_BUY,
market: {
id: 'market-0',
-1
View File
@@ -1,5 +1,4 @@
export * from './date';
export * from './number';
export * from './range';
export * from './size';
export * from './strings';
-38
View File
@@ -1,38 +0,0 @@
import {
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
isNumeric,
} from './number';
export const formatValue = (
value: string | number | null | undefined,
decimalPlaces: number,
quantum?: string,
formatDecimals?: number,
emptyValue = '-'
): string => {
if (!isNumeric(value)) return emptyValue;
if (!quantum) {
return addDecimalsFormatNumber(value, decimalPlaces, formatDecimals);
}
return addDecimalsFormatNumberQuantum(value, decimalPlaces, quantum);
};
export const formatRange = (
min: string | number | null | undefined,
max: string | number | null | undefined,
decimalPlaces: number,
quantum?: string,
formatDecimals?: number,
emptyValue = '-'
) => {
const minFormatted = formatValue(min, decimalPlaces, quantum, formatDecimals);
const maxFormatted = formatValue(max, decimalPlaces, quantum, formatDecimals);
if (minFormatted !== maxFormatted) {
return `${minFormatted} - ${maxFormatted}`;
}
if (minFormatted !== emptyValue) {
return minFormatted;
}
return maxFormatted;
};