feat(governance): proposal details page improvements (#3611)

This commit is contained in:
Sam Keen 2023-05-08 13:41:55 +01:00 committed by GitHub
parent 7f949a276c
commit 313eff1c95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 671 additions and 512 deletions

View File

@ -101,11 +101,11 @@ context(
); );
cy.getByTestId('protocol-upgrade-proposal-release-tag').should( cy.getByTestId('protocol-upgrade-proposal-release-tag').should(
'have.text', 'have.text',
'Vega release tagv1' 'Vega release tag: v1'
); );
cy.getByTestId('protocol-upgrade-proposal-block-height').should( cy.getByTestId('protocol-upgrade-proposal-block-height').should(
'have.text', 'have.text',
'Upgrade block height2015942' 'Upgrade block height: 2015942'
); );
cy.getByTestId('protocol-upgrade-proposal-status').should( cy.getByTestId('protocol-upgrade-proposal-status').should(
'have.text', 'have.text',

View File

@ -23,6 +23,7 @@ export const ConnectToVega = () => {
openVegaWalletDialog(); openVegaWalletDialog();
}} }}
data-testid="connect-to-vega-wallet-btn" data-testid="connect-to-vega-wallet-btn"
variant="primary"
> >
{t('connectVegaWallet')} {t('connectVegaWallet')}
</Button> </Button>

View File

@ -23,7 +23,7 @@ export const Heading = ({
})} })}
> >
<h1 <h1
className={classNames('font-alpha calt text-5xl', { className={classNames('font-alpha calt text-5xl break-words', {
'mt-0': !marginTop, 'mt-0': !marginTop,
'mb-0': !marginBottom, 'mb-0': !marginBottom,
})} })}

View File

@ -201,7 +201,7 @@
"NewFreeform": "Freeform", "NewFreeform": "Freeform",
"tokenVotes": "Token votes", "tokenVotes": "Token votes",
"liquidityVotes": "Liquidity votes", "liquidityVotes": "Liquidity votes",
"yourVote": "Your vote", "castYourVote": "Cast your vote",
"for": "For", "for": "For",
"against": "Against", "against": "Against",
"majorityRequired": "Majority Required", "majorityRequired": "Majority Required",
@ -587,7 +587,7 @@
"tokensAgainstProposal": "Tokens against proposal", "tokensAgainstProposal": "Tokens against proposal",
"participationRequired": "Participation required", "participationRequired": "Participation required",
"numberOfVotingParties": "Number of voting parties", "numberOfVotingParties": "Number of voting parties",
"totalTokensVotes": "Total yes tokens", "totalTokensVotes": "Total tokens voted",
"totalTokenVotedPercentage": "Total tokens voted percentage", "totalTokenVotedPercentage": "Total tokens voted percentage",
"numberOfForVotes": "Number of votes for", "numberOfForVotes": "Number of votes for",
"numberOfAgainstVotes": "Number of votes against", "numberOfAgainstVotes": "Number of votes against",
@ -631,7 +631,10 @@
"New market": "New market", "New market": "New market",
"Market change": "Market change", "Market change": "Market change",
"Network parameter": "Network parameter", "Network parameter": "Network parameter",
"Change": "Change",
"Unknown proposal": "Unknown proposal", "Unknown proposal": "Unknown proposal",
"ERC20ContractAddress": "ERC20 contract address",
"MaxFaucetAmountMint": "Max faucet amount mint",
"Code": "Code", "Code": "Code",
"settled future": "settled future", "settled future": "settled future",
"Symbol": "Symbol", "Symbol": "Symbol",
@ -677,6 +680,7 @@
"NewProposal": "New proposal", "NewProposal": "New proposal",
"ProposalTypeQuestion": "What type of proposal would you like to make?", "ProposalTypeQuestion": "What type of proposal would you like to make?",
"NetworkParameterProposal": "Update network parameter proposal", "NetworkParameterProposal": "Update network parameter proposal",
"parameter": "parameter",
"NewMarketProposal": "New market proposal", "NewMarketProposal": "New market proposal",
"UpdateMarketProposal": "Update market proposal", "UpdateMarketProposal": "Update market proposal",
"NewAssetProposal": "New asset proposal", "NewAssetProposal": "New asset proposal",
@ -694,6 +698,7 @@
"UpdateMarket": "Update market", "UpdateMarket": "Update market",
"NewAsset": "New asset", "NewAsset": "New asset",
"UpdateAsset": "Update asset", "UpdateAsset": "Update asset",
"AssetID": "Asset ID",
"Freeform": "Freeform", "Freeform": "Freeform",
"RawProposal": "Let me choose (raw proposal)", "RawProposal": "Let me choose (raw proposal)",
"UseMin": "Use minimum", "UseMin": "Use minimum",
@ -737,6 +742,7 @@
"ProposalNotFound": "Proposal not found", "ProposalNotFound": "Proposal not found",
"ProposalNotFoundDetails": "The proposal you are looking for is not here, it may have been enacted before the last chain restore. You could check the Vega forums/discord instead for information about it.", "ProposalNotFoundDetails": "The proposal you are looking for is not here, it may have been enacted before the last chain restore. You could check the Vega forums/discord instead for information about it.",
"FreeformProposal": "Freeform proposal", "FreeformProposal": "Freeform proposal",
"Id": "ID",
"unknownReason": "unknown reason", "unknownReason": "unknown reason",
"votingEnded": "Voting has ended.", "votingEnded": "Voting has ended.",
"STATUS": "STATUS", "STATUS": "STATUS",
@ -791,5 +797,7 @@
"67% voting power required": "67% voting power required", "67% voting power required": "67% voting power required",
"Token": "Token", "Token": "Token",
"associateVegaNow": "Associate $VEGA now", "associateVegaNow": "Associate $VEGA now",
"disconnectedNotice": "You have been disconnected. Connect your ETH wallet to the {{correctNetwork}} network to use this app." "disconnectedNotice": "You have been disconnected. Connect your ETH wallet to the {{correctNetwork}} network to use this app.",
"connectAVegaWalletToVote": "Connect a Vega wallet with $VEGA tokens to vote on a proposal.",
"findOutMoreAboutHowToVote": "Find out more about how to vote on Vega"
} }

View File

@ -1,7 +1,11 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import * as Schema from '@vegaprotocol/types'; import { Icon } from '@vegaprotocol/ui-toolkit';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalState } from '@vegaprotocol/types';
import { ProposalInfoLabel } from '../proposal-info-label';
import type { ReactNode } from 'react';
import type { ProposalInfoLabelVariant } from '../proposal-info-label';
export const CurrentProposalState = ({ export const CurrentProposalState = ({
proposal, proposal,
@ -9,19 +13,63 @@ export const CurrentProposalState = ({
proposal: ProposalFieldsFragment | ProposalQuery['proposal']; proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
let className = 'text-white'; let proposalStatus: ReactNode;
let variant = 'tertiary' as ProposalInfoLabelVariant;
if ( switch (proposal?.state) {
proposal?.state === Schema.ProposalState.STATE_DECLINED || case ProposalState.STATE_ENACTED: {
proposal?.state === Schema.ProposalState.STATE_FAILED || proposalStatus = (
proposal?.state === Schema.ProposalState.STATE_REJECTED <>
) { <span className="mr-2">{t('voteState_Enacted')}</span>
className = 'text-danger'; <Icon name={'tick'} />
} else if ( </>
proposal?.state === Schema.ProposalState.STATE_ENACTED || );
proposal?.state === Schema.ProposalState.STATE_PASSED break;
) { }
className = 'text-white'; case ProposalState.STATE_PASSED: {
proposalStatus = (
<>
<span className="mr-2">{t('voteState_Passed')}</span>
<Icon name={'tick'} />
</>
);
break;
}
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
proposalStatus = (
<>
<span className="mr-2">{t('voteState_WaitingForNodeVote')}</span>
<Icon name={'time'} />
</>
);
break;
}
case ProposalState.STATE_OPEN: {
variant = 'primary' as ProposalInfoLabelVariant;
proposalStatus = <>{t('voteState_Open')}</>;
break;
}
case ProposalState.STATE_DECLINED: {
proposalStatus = (
<>
<span className="mr-2">{t('voteState_Declined')}</span>
<Icon name={'cross'} />
</>
);
break;
}
case ProposalState.STATE_REJECTED: {
proposalStatus = (
<>
<span className="mr-2">{t('voteState_Rejected')}</span>
<Icon name={'warning-sign'} />
</>
);
break;
}
} }
return <span className={className}>{t(`${proposal?.state}`)}</span>;
return (
<ProposalInfoLabel variant={variant}>{proposalStatus}</ProposalInfoLabel>
);
}; };

View File

@ -16,17 +16,12 @@ it('Renders all data for table', () => {
render(<ProposalChangeTable proposal={proposal} />); render(<ProposalChangeTable proposal={proposal} />);
expect(screen.getByText('ID')).toBeInTheDocument(); expect(screen.getByText('ID')).toBeInTheDocument();
expect(screen.getByText(proposal?.id as string)).toBeInTheDocument(); expect(screen.getByText(proposal?.id as string)).toBeInTheDocument();
expect(screen.getByText('State')).toBeInTheDocument();
expect(screen.getByText('Open')).toBeInTheDocument();
expect(screen.getByText('Closes on')).toBeInTheDocument(); expect(screen.getByText('Closes on')).toBeInTheDocument();
expect( expect(
screen.getByText( screen.getByText(
formatDateWithLocalTimezone(new Date(proposal?.terms.closingDatetime)) formatDateWithLocalTimezone(new Date(proposal?.terms.closingDatetime))
) )
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByText('Proposed enactment')).toBeInTheDocument(); expect(screen.getByText('Proposed enactment')).toBeInTheDocument();
expect( expect(
screen.getByText( screen.getByText(
@ -35,17 +30,12 @@ it('Renders all data for table', () => {
) )
) )
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByText('Proposed by')).toBeInTheDocument(); expect(screen.getByText('Proposed by')).toBeInTheDocument();
expect(screen.getByText(proposal?.party.id ?? '')).toBeInTheDocument(); expect(screen.getByText(proposal?.party.id ?? '')).toBeInTheDocument();
expect(screen.getByText('Proposed on')).toBeInTheDocument(); expect(screen.getByText('Proposed on')).toBeInTheDocument();
expect( expect(
screen.getByText(formatDateWithLocalTimezone(new Date(proposal?.datetime))) screen.getByText(formatDateWithLocalTimezone(new Date(proposal?.datetime)))
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByText('Type')).toBeInTheDocument();
expect(screen.getByText('Network parameter')).toBeInTheDocument();
}); });
it('Changes data based on if data is in future or past', () => { it('Changes data based on if data is in future or past', () => {
@ -53,17 +43,12 @@ it('Changes data based on if data is in future or past', () => {
state: ProposalState.STATE_ENACTED, state: ProposalState.STATE_ENACTED,
}); });
render(<ProposalChangeTable proposal={proposal} />); render(<ProposalChangeTable proposal={proposal} />);
expect(screen.getByText('State')).toBeInTheDocument();
expect(screen.getByText('Enacted')).toBeInTheDocument();
expect(screen.getByText('Closed on')).toBeInTheDocument(); expect(screen.getByText('Closed on')).toBeInTheDocument();
expect( expect(
screen.getByText( screen.getByText(
formatDateWithLocalTimezone(new Date(proposal?.terms.closingDatetime)) formatDateWithLocalTimezone(new Date(proposal?.terms.closingDatetime))
) )
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByText('Enacted on')).toBeInTheDocument(); expect(screen.getByText('Enacted on')).toBeInTheDocument();
expect( expect(
screen.getByText( screen.getByText(
@ -104,7 +89,6 @@ it('Renders error details and rejection reason if present', () => {
render(<ProposalChangeTable proposal={proposal} />); render(<ProposalChangeTable proposal={proposal} />);
expect(screen.getByText('Error details')).toBeInTheDocument(); expect(screen.getByText('Error details')).toBeInTheDocument();
expect(screen.getByText(errorDetails)).toBeInTheDocument(); expect(screen.getByText(errorDetails)).toBeInTheDocument();
expect(screen.getByText('Rejection reason')).toBeInTheDocument(); expect(screen.getByText('Rejection reason')).toBeInTheDocument();
expect( expect(
screen.getByText(ProposalRejectionReason.PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE) screen.getByText(ProposalRejectionReason.PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE)

View File

@ -6,7 +6,6 @@ import {
KeyValueTableRow, KeyValueTableRow,
RoundedWrapper, RoundedWrapper,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { CurrentProposalState } from '../current-proposal-state';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
@ -20,16 +19,12 @@ export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
const terms = proposal?.terms; const terms = proposal?.terms;
return ( return (
<RoundedWrapper> <RoundedWrapper paddingBottom={true}>
<KeyValueTable data-testid="proposal-change-table"> <KeyValueTable data-testid="proposal-change-table">
<KeyValueTableRow> <KeyValueTableRow>
{t('id')} {t('id')}
{proposal?.id} {proposal?.id}
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow>
{t('state')}
<CurrentProposalState proposal={proposal} />
</KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow>
{isFuture(new Date(terms?.closingDatetime)) {isFuture(new Date(terms?.closingDatetime))
? t('closesOn') ? t('closesOn')
@ -50,26 +45,24 @@ export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
{t('proposedBy')} {t('proposedBy')}
<span style={{ wordBreak: 'break-word' }}>{proposal?.party.id}</span> <span style={{ wordBreak: 'break-word' }}>{proposal?.party.id}</span>
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow
noBorder={!proposal?.rejectionReason && !proposal?.errorDetails}
>
{t('proposedOn')} {t('proposedOn')}
{formatDateWithLocalTimezone(new Date(proposal?.datetime))} {formatDateWithLocalTimezone(new Date(proposal?.datetime))}
</KeyValueTableRow> </KeyValueTableRow>
{proposal?.rejectionReason ? ( {proposal?.rejectionReason ? (
<KeyValueTableRow> <KeyValueTableRow noBorder={!proposal?.errorDetails}>
{t('rejectionReason')} {t('rejectionReason')}
{proposal.rejectionReason} {proposal.rejectionReason}
</KeyValueTableRow> </KeyValueTableRow>
) : null} ) : null}
{proposal?.errorDetails ? ( {proposal?.errorDetails ? (
<KeyValueTableRow> <KeyValueTableRow noBorder={true}>
{t('errorDetails')} {t('errorDetails')}
{proposal.errorDetails} {proposal.errorDetails}
</KeyValueTableRow> </KeyValueTableRow>
) : null} ) : null}
<KeyValueTableRow>
{t('type')}
{t(`${proposal?.terms.change.__typename}`)}
</KeyValueTableRow>
</KeyValueTable> </KeyValueTable>
</RoundedWrapper> </RoundedWrapper>
); );

View File

@ -1,41 +1,45 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { generateProposal } from '../../test-helpers/generate-proposals'; import {
generateNoVotes,
generateProposal,
generateYesVotes,
} from '../../test-helpers/generate-proposals';
import { ProposalHeader } from './proposal-header'; import { ProposalHeader } from './proposal-header';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
import { lastWeek, nextWeek } from '../../test-helpers/mocks';
const renderComponent = ( const renderComponent = (
proposal: ProposalQuery['proposal'], proposal: ProposalQuery['proposal'],
isListItem = true isListItem = true
) => <ProposalHeader proposal={proposal} isListItem={isListItem} />; ) => render(<ProposalHeader proposal={proposal} isListItem={isListItem} />);
describe('Proposal header', () => { describe('Proposal header', () => {
it('Renders New market proposal', () => { it('Renders New market proposal', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ rationale: {
rationale: { title: 'New some market',
title: 'New some market', description: 'A new some market',
description: 'A new some market', },
}, terms: {
terms: { change: {
change: { __typename: 'NewMarket',
__typename: 'NewMarket', instrument: {
instrument: { __typename: 'InstrumentConfiguration',
__typename: 'InstrumentConfiguration', name: 'Some market',
name: 'Some market', code: 'FX:BTCUSD/DEC99',
code: 'FX:BTCUSD/DEC99', futureProduct: {
futureProduct: { __typename: 'FutureProduct',
__typename: 'FutureProduct', settlementAsset: {
settlementAsset: { __typename: 'Asset',
__typename: 'Asset', symbol: 'tGBP',
symbol: 'tGBP',
},
}, },
}, },
}, },
}, },
}) },
) })
); );
expect(screen.getByTestId('proposal-title')).toHaveTextContent( expect(screen.getByTestId('proposal-title')).toHaveTextContent(
'New some market' 'New some market'
@ -47,20 +51,18 @@ describe('Proposal header', () => {
}); });
it('Renders Update market proposal', () => { it('Renders Update market proposal', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ rationale: {
rationale: { title: 'New market id',
title: 'New market id', },
terms: {
change: {
__typename: 'UpdateMarket',
marketId: 'MarketId',
}, },
terms: { },
change: { })
__typename: 'UpdateMarket',
marketId: 'MarketId',
},
},
})
)
); );
expect(screen.getByTestId('proposal-title')).toHaveTextContent( expect(screen.getByTestId('proposal-title')).toHaveTextContent(
'New market id' 'New market id'
@ -77,53 +79,49 @@ describe('Proposal header', () => {
}); });
it('Renders New asset proposal - ERC20', () => { it('Renders New asset proposal - ERC20', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ rationale: {
rationale: { title: 'New asset: Fake currency',
title: 'New asset: Fake currency', description: '',
description: '', },
}, terms: {
terms: { change: {
change: { __typename: 'NewAsset',
__typename: 'NewAsset', name: 'Fake currency',
name: 'Fake currency', symbol: 'FAKE',
symbol: 'FAKE', source: {
source: { __typename: 'ERC20',
__typename: 'ERC20', contractAddress: '0x0',
contractAddress: '0x0',
},
}, },
}, },
}) },
) })
); );
expect(screen.getByTestId('proposal-title')).toHaveTextContent( expect(screen.getByTestId('proposal-title')).toHaveTextContent(
'New asset: Fake currency' 'New asset: Fake currency'
); );
expect(screen.getByTestId('proposal-type')).toHaveTextContent('New asset'); expect(screen.getByTestId('proposal-type')).toHaveTextContent('New asset');
expect(screen.getByTestId('proposal-details')).toHaveTextContent( expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'Symbol: FAKE. ERC20 0x0' 'Symbol: FAKE. ERC20 contract address: 0x0'
); );
}); });
it('Renders New asset proposal - BuiltInAsset', () => { it('Renders New asset proposal - BuiltInAsset', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ terms: {
terms: { change: {
change: { __typename: 'NewAsset',
__typename: 'NewAsset', name: 'Fake currency',
name: 'Fake currency', symbol: 'BIA',
symbol: 'BIA', source: {
source: { __typename: 'BuiltinAsset',
__typename: 'BuiltinAsset', maxFaucetAmountMint: '300',
maxFaucetAmountMint: '300',
},
}, },
}, },
}) },
) })
); );
expect(screen.getByTestId('proposal-title')).toHaveTextContent( expect(screen.getByTestId('proposal-title')).toHaveTextContent(
'Unknown proposal' 'Unknown proposal'
@ -135,24 +133,22 @@ describe('Proposal header', () => {
}); });
it('Renders Update network', () => { it('Renders Update network', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ rationale: {
rationale: { title: 'Network parameter',
title: 'Network parameter', },
}, terms: {
terms: { change: {
change: { __typename: 'UpdateNetworkParameter',
__typename: 'UpdateNetworkParameter', networkParameter: {
networkParameter: { __typename: 'NetworkParameter',
__typename: 'NetworkParameter', key: 'Network key',
key: 'Network key', value: 'Network value',
value: 'Network value',
},
}, },
}, },
}) },
) })
); );
expect(screen.getByTestId('proposal-title')).toHaveTextContent( expect(screen.getByTestId('proposal-title')).toHaveTextContent(
'Network parameter' 'Network parameter'
@ -165,47 +161,42 @@ describe('Proposal header', () => {
); );
}); });
it('Renders Freeform network - short rationale', () => { it('Renders Freeform proposal - short rationale', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ id: 'short',
id: 'short', rationale: {
rationale: { title: '0x0',
title: '0x0', },
terms: {
change: {
__typename: 'NewFreeform',
}, },
terms: { },
change: { })
__typename: 'NewFreeform',
},
},
})
)
); );
expect(screen.getByTestId('proposal-title')).toHaveTextContent('0x0'); expect(screen.getByTestId('proposal-title')).toHaveTextContent('0x0');
expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform'); expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform');
expect( expect(
screen.queryByTestId('proposal-description') screen.queryByTestId('proposal-description')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect(screen.getByTestId('proposal-details')).toHaveTextContent('short');
}); });
it('Renders Freeform proposal - long rationale (105 chars) - listing', () => { it('Renders Freeform proposal - long rationale (105 chars) - listing', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ id: 'long',
id: 'long', rationale: {
rationale: { title: '0x0',
title: '0x0', description:
description: 'Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean dolor.',
'Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean dolor.', },
terms: {
change: {
__typename: 'NewFreeform',
}, },
terms: { },
change: { })
__typename: 'NewFreeform',
},
},
})
)
); );
expect(screen.getByTestId('proposal-title')).toHaveTextContent('0x0'); expect(screen.getByTestId('proposal-title')).toHaveTextContent('0x0');
expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform'); expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform');
@ -213,27 +204,24 @@ describe('Proposal header', () => {
expect( expect(
screen.queryByTestId('proposal-description') screen.queryByTestId('proposal-description')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect(screen.getByTestId('proposal-details')).toHaveTextContent('long');
}); });
it('Renders Freeform proposal - long rationale (105 chars) - details', () => { it('Renders Freeform proposal - long rationale (105 chars) - details', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ id: 'long',
id: 'long', rationale: {
rationale: { title: '0x0',
title: '0x0', description:
description: 'Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean dolor.',
'Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean dolor.', },
terms: {
change: {
__typename: 'NewFreeform',
}, },
terms: { },
change: { }),
__typename: 'NewFreeform', false
},
},
}),
false
)
); );
expect(screen.getByTestId('proposal-description')).toHaveTextContent( expect(screen.getByTestId('proposal-description')).toHaveTextContent(
/Class aptent/ /Class aptent/
@ -242,43 +230,36 @@ describe('Proposal header', () => {
// Remove once proposals have rationale and re-enable above tests // Remove once proposals have rationale and re-enable above tests
it('Renders Freeform proposal - id for title', () => { it('Renders Freeform proposal - id for title', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ id: 'freeform id',
id: 'freeform id', rationale: {
rationale: { title: 'freeform',
title: 'freeform', },
terms: {
change: {
__typename: 'NewFreeform',
}, },
terms: { },
change: { })
__typename: 'NewFreeform',
},
},
})
)
); );
expect(screen.getByTestId('proposal-title')).toHaveTextContent('freeform'); expect(screen.getByTestId('proposal-title')).toHaveTextContent('freeform');
expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform'); expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform');
expect( expect(
screen.queryByTestId('proposal-description') screen.queryByTestId('proposal-description')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect(screen.queryByTestId('proposal-details')).toHaveTextContent(
'freeform id'
);
}); });
it('Renders asset change proposal header', () => { it('Renders asset change proposal header', () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ terms: {
terms: { change: {
change: { __typename: 'UpdateAsset',
__typename: 'UpdateAsset', assetId: 'foo',
assetId: 'foo',
},
}, },
}) },
) })
); );
expect(screen.getByTestId('proposal-type')).toHaveTextContent( expect(screen.getByTestId('proposal-type')).toHaveTextContent(
'Update asset' 'Update asset'
@ -287,20 +268,104 @@ describe('Proposal header', () => {
}); });
it("Renders unknown proposal if it's a different proposal type", () => { it("Renders unknown proposal if it's a different proposal type", () => {
render( renderComponent(
renderComponent( generateProposal({
generateProposal({ terms: {
terms: { change: {
change: { // @ts-ignore unknown proposal
// @ts-ignore unknown proposal __typename: 'Foo',
__typename: 'Foo',
},
}, },
}) },
) })
); );
expect(screen.getByTestId('proposal-title')).toHaveTextContent( expect(screen.getByTestId('proposal-title')).toHaveTextContent(
'Unknown proposal' 'Unknown proposal'
); );
}); });
it('Renders proposal state: Enacted', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_ENACTED,
terms: {
enactmentDatetime: lastWeek.toString(),
},
})
);
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Enacted');
});
it('Renders proposal state: Passed', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
})
);
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Passed');
});
it('Renders proposal state: Waiting for node vote', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
terms: {
enactmentDatetime: nextWeek.toString(),
},
})
);
expect(screen.getByTestId('proposal-status')).toHaveTextContent(
'Waiting for node vote'
);
});
it('Renders proposal state: Open', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(3000, 1000000000000000000),
no: generateNoVotes(0),
},
terms: {
closingDatetime: nextWeek.toString(),
},
})
);
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
});
it('Renders proposal state: Declined - majority not reached', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_DECLINED,
terms: {
enactmentDatetime: lastWeek.toString(),
},
votes: {
no: generateNoVotes(1, 1000000000000000000),
yes: generateYesVotes(1, 1000000000000000000),
},
})
);
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Declined');
});
it('Renders proposal state: Rejected', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_REJECTED,
terms: {
enactmentDatetime: lastWeek.toString(),
},
rejectionReason:
ProposalRejectionReason.PROPOSAL_ERROR_INVALID_FUTURE_PRODUCT,
})
);
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Rejected');
});
}); });

View File

@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Intent, Lozenge } from '@vegaprotocol/ui-toolkit'; import { Lozenge } from '@vegaprotocol/ui-toolkit';
import { shorten } from '@vegaprotocol/utils'; import { shorten } from '@vegaprotocol/utils';
import { Heading, SubHeading } from '../../../../components/heading'; import { Heading, SubHeading } from '../../../../components/heading';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
@ -7,6 +7,8 @@ import type { ProposalFieldsFragment } from '../../proposals/__generated__/Propo
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import { truncateMiddle } from '../../../../lib/truncate-middle'; import { truncateMiddle } from '../../../../lib/truncate-middle';
import { CurrentProposalState } from '../current-proposal-state';
import { ProposalInfoLabel } from '../proposal-info-label';
export const ProposalHeader = ({ export const ProposalHeader = ({
proposal, proposal,
@ -19,7 +21,7 @@ export const ProposalHeader = ({
const change = proposal?.terms.change; const change = proposal?.terms.change;
let details: ReactNode; let details: ReactNode;
let proposalType: ReactNode; let proposalType = '';
let title = proposal?.rationale.title.trim(); let title = proposal?.rationale.title.trim();
let description = proposal?.rationale.description.trim(); let description = proposal?.rationale.description.trim();
@ -32,10 +34,12 @@ export const ProposalHeader = ({
switch (change?.__typename) { switch (change?.__typename) {
case 'NewMarket': { case 'NewMarket': {
proposalType = t('NewMarket'); proposalType = 'NewMarket';
details = ( details = (
<> <>
{t('Code')}: {change.instrument.code}.{' '} <span>
{t('Code')}: {change.instrument.code}.
</span>{' '}
{change.instrument.futureProduct?.settlementAsset.symbol ? ( {change.instrument.futureProduct?.settlementAsset.symbol ? (
<> <>
<span className="font-semibold"> <span className="font-semibold">
@ -51,53 +55,60 @@ export const ProposalHeader = ({
break; break;
} }
case 'UpdateMarket': { case 'UpdateMarket': {
proposalType = t('UpdateMarket'); proposalType = 'UpdateMarket';
details = `${t('Market change')}: ${change.marketId}`; details = (
<>
<span>{t('Market change')}:</span>{' '}
<span>{truncateMiddle(change.marketId)}</span>
</>
);
break; break;
} }
case 'NewAsset': { case 'NewAsset': {
proposalType = t('NewAsset'); proposalType = 'NewAsset';
details = ( details = (
<> <>
{t('Symbol')}: {change.symbol}.{' '} <span>{t('Symbol')}:</span> <Lozenge>{change.symbol}.</Lozenge>{' '}
<Lozenge> {change.source.__typename === 'ERC20' && (
{change.source.__typename === 'ERC20' && <>
`ERC20 ${change.source.contractAddress}`} <span>{t('ERC20ContractAddress')}:</span>{' '}
{change.source.__typename === 'BuiltinAsset' && <Lozenge>{change.source.contractAddress}</Lozenge>
`${t('Max faucet amount mint')}: ${ </>
change.source.maxFaucetAmountMint )}{' '}
}`} {change.source.__typename === 'BuiltinAsset' && (
</Lozenge> <>
<span>{t('MaxFaucetAmountMint')}:</span>{' '}
<Lozenge>{change.source.maxFaucetAmountMint}</Lozenge>
</>
)}
</> </>
); );
break; break;
} }
case 'UpdateNetworkParameter': { case 'UpdateNetworkParameter': {
proposalType = t('NetworkParameter'); proposalType = 'NetworkParameter';
const parametersClasses = 'font-mono leading-none';
details = ( details = (
<> <>
<span className={`${parametersClasses} mr-2`}> <span>{t('Change')}:</span>{' '}
{change.networkParameter.key} <Lozenge>{change.networkParameter.key}</Lozenge>{' '}
</span>{' '} <span>{t('to')}</span>{' '}
{t('to')}{' '} <span className="whitespace-nowrap">
<span className={`${parametersClasses} ml-2`}> <Lozenge>{change.networkParameter.value}</Lozenge>
{change.networkParameter.value}
</span> </span>
</> </>
); );
break; break;
} }
case 'NewFreeform': { case 'NewFreeform': {
proposalType = t('Freeform'); proposalType = 'Freeform';
details = `${t('FreeformProposal')}: ${proposal?.id}`; details = <span />;
break; break;
} }
case 'UpdateAsset': { case 'UpdateAsset': {
proposalType = t('UpdateAsset'); proposalType = 'UpdateAsset';
details = ( details = (
<> <>
<span>{t('Asset ID')}:</span> <span>{t('AssetID')}:</span>{' '}
<Lozenge>{truncateMiddle(change.assetId)}</Lozenge> <Lozenge>{truncateMiddle(change.assetId)}</Lozenge>
</> </>
); );
@ -106,7 +117,7 @@ export const ProposalHeader = ({
} }
return ( return (
<div className="text-sm mb-2"> <>
<div data-testid="proposal-title"> <div data-testid="proposal-title">
{isListItem ? ( {isListItem ? (
<header> <header>
@ -118,30 +129,39 @@ export const ProposalHeader = ({
</div> </div>
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
{proposalType && ( <div data-testid="proposal-type">
<div data-testid="proposal-type"> <ProposalInfoLabel variant="secondary">
<Lozenge variant={Intent.None}>{proposalType}</Lozenge> {t(`${proposalType}`)}
</div> </ProposalInfoLabel>
)} </div>
</div>
<div className="flex items-center gap-2"> <div data-testid="proposal-status">
{description && !isListItem && ( <CurrentProposalState proposal={proposal} />
<div data-testid="proposal-description" className="mb-4"> </div>
<ReactMarkdown
className="react-markdown-container"
/* Prevents HTML embedded in the description from rendering */
skipHtml={true}
/* Stops users embedding images which could be used for tracking */
disallowedElements={['img']}
linkTarget="_blank"
>
{description}
</ReactMarkdown>
</div>
)}
</div> </div>
{details && <div data-testid="proposal-details">{details}</div>} {details && (
</div> <div data-testid="proposal-details" className="break-words my-10">
{details}
</div>
)}
{description && !isListItem && (
<div data-testid="proposal-description">
{/*<div className="uppercase mr-2">{t('ProposalDescription')}:</div>*/}
<SubHeading title={t('ProposalDescription')} />
<ReactMarkdown
className="react-markdown-container"
/* Prevents HTML embedded in the description from rendering */
skipHtml={true}
/* Stops users embedding images which could be used for tracking */
disallowedElements={['img']}
linkTarget="_blank"
>
{description}
</ReactMarkdown>
</div>
)}
</>
); );
}; };

View File

@ -0,0 +1 @@
export * from './proposal-info-label';

View File

@ -0,0 +1,35 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
export type ProposalInfoLabelVariant =
| 'primary'
| 'secondary'
| 'tertiary'
| 'highlight';
const base = 'rounded-md px-2 py-1 font-alpha';
const primary = 'bg-vega-light-150 text-black';
const secondary = 'bg-vega-dark-200 text-white';
const tertiary = 'bg-vega-dark-150 text-white';
const highlight = 'bg-vega-yellow text-black';
const getClassname = (variant: ProposalInfoLabelVariant) => {
return classNames(base, {
[primary]: variant === 'primary',
[secondary]: variant === 'secondary',
[tertiary]: variant === 'tertiary',
[highlight]: variant === 'highlight',
});
};
interface ProposalInfoLabelProps {
children: ReactNode;
variant?: ProposalInfoLabelVariant;
}
export const ProposalInfoLabel = ({
children,
variant = 'primary',
}: ProposalInfoLabelProps) => {
return <div className={getClassname(variant)}>{children}</div>;
};

View File

@ -1,8 +1,10 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit'; import { Icon, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import type { PartialDeep } from 'type-fest'; import type { PartialDeep } from 'type-fest';
import type * as Schema from '@vegaprotocol/types'; import type * as Schema from '@vegaprotocol/types';
import { useState } from 'react';
import classnames from 'classnames';
export const ProposalTermsJson = ({ export const ProposalTermsJson = ({
terms, terms,
@ -10,10 +12,26 @@ export const ProposalTermsJson = ({
terms: PartialDeep<Schema.ProposalTerms>; terms: PartialDeep<Schema.ProposalTerms>;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
const showDetailsIconClasses = classnames('mb-4', {
'rotate-180': showDetails,
});
return ( return (
<section> <section>
<SubHeading title={t('proposalTerms')} /> <button
<SyntaxHighlighter data={terms} /> onClick={() => setShowDetails(!showDetails)}
data-testid="proposal-terms-toggle"
>
<div className="flex items-center gap-3">
<SubHeading title={t('proposalTerms')} />
<div className={showDetailsIconClasses}>
<Icon name="chevron-down" size={8} />
</div>
</div>
</button>
{showDetails && <SyntaxHighlighter data={terms} />}
</section> </section>
); );
}; };

View File

@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { ProposalVotesTable } from './proposal-votes-table'; import { ProposalVotesTable } from './proposal-votes-table';
@ -46,6 +46,7 @@ describe('Proposal Votes Table', () => {
it('should show vote breakdown fields, excluding custom update market fields', () => { it('should show vote breakdown fields, excluding custom update market fields', () => {
renderComponent(); renderComponent();
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
expect(screen.getByText('Expected to pass')).toBeInTheDocument(); expect(screen.getByText('Expected to pass')).toBeInTheDocument();
expect(screen.getByText('Token majority met')).toBeInTheDocument(); expect(screen.getByText('Token majority met')).toBeInTheDocument();
expect(screen.getByText('Token participation met')).toBeInTheDocument(); expect(screen.getByText('Token participation met')).toBeInTheDocument();
@ -55,7 +56,7 @@ describe('Proposal Votes Table', () => {
expect(screen.getByText('Participation required')).toBeInTheDocument(); expect(screen.getByText('Participation required')).toBeInTheDocument();
expect(screen.getByText('Majority Required')).toBeInTheDocument(); expect(screen.getByText('Majority Required')).toBeInTheDocument();
expect(screen.getByText('Number of voting parties')).toBeInTheDocument(); expect(screen.getByText('Number of voting parties')).toBeInTheDocument();
expect(screen.getByText('Total yes tokens')).toBeInTheDocument(); expect(screen.getByText('Total tokens voted')).toBeInTheDocument();
expect( expect(
screen.getByText('Total tokens voted percentage') screen.getByText('Total tokens voted percentage')
).toBeInTheDocument(); ).toBeInTheDocument();
@ -70,13 +71,14 @@ describe('Proposal Votes Table', () => {
it('displays different breakdown fields for update market proposal', () => { it('displays different breakdown fields for update market proposal', () => {
renderComponent(updateMarketProposal, updateMarketProposalType); renderComponent(updateMarketProposal, updateMarketProposalType);
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
expect(screen.getByText('Liquidity majority met')).toBeInTheDocument(); expect(screen.getByText('Liquidity majority met')).toBeInTheDocument();
expect(screen.getByText('Liquidity participation met')).toBeInTheDocument(); expect(screen.getByText('Liquidity participation met')).toBeInTheDocument();
expect( expect(
screen.getByText('Liquidity shares for proposal') screen.getByText('Liquidity shares for proposal')
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.queryByText('Number of voting parties')).toBeNull(); expect(screen.queryByText('Number of voting parties')).toBeNull();
expect(screen.queryByText('Total yes tokens')).toBeNull(); expect(screen.queryByText('Total tokens voted')).toBeNull();
expect(screen.queryByText('Total tokens voted percentage')).toBeNull(); expect(screen.queryByText('Total tokens voted percentage')).toBeNull();
expect(screen.queryByText('Number of votes for')).toBeNull(); expect(screen.queryByText('Number of votes for')).toBeNull();
expect(screen.queryByText('Number of votes against')).toBeNull(); expect(screen.queryByText('Number of votes against')).toBeNull();
@ -86,6 +88,7 @@ describe('Proposal Votes Table', () => {
it('displays if an update market proposal will pass by token vote', () => { it('displays if an update market proposal will pass by token vote', () => {
renderComponent(updateMarketProposal, updateMarketProposalType); renderComponent(updateMarketProposal, updateMarketProposalType);
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
expect(screen.getByText('👍 by token vote')).toBeInTheDocument(); expect(screen.getByText('👍 by token vote')).toBeInTheDocument();
}); });
@ -110,6 +113,7 @@ describe('Proposal Votes Table', () => {
}), }),
updateMarketProposalType updateMarketProposalType
); );
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
expect(screen.getByText('👍 by liquidity vote')).toBeInTheDocument(); expect(screen.getByText('👍 by liquidity vote')).toBeInTheDocument();
}); });
}); });

View File

@ -1,9 +1,12 @@
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
KeyValueTable, KeyValueTable,
KeyValueTableRow, KeyValueTableRow,
Thumbs, Thumbs,
RoundedWrapper, RoundedWrapper,
Icon,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { formatNumber, formatNumberPercentage } from '@vegaprotocol/utils'; import { formatNumber, formatNumberPercentage } from '@vegaprotocol/utils';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
@ -26,6 +29,7 @@ export const ProposalVotesTable = ({
const { const {
appState: { totalSupply }, appState: { totalSupply },
} = useAppState(); } = useAppState();
const [showDetails, setShowDetails] = useState(false);
const { const {
willPassByTokenVote, willPassByTokenVote,
willPassByLPVote, willPassByLPVote,
@ -53,113 +57,130 @@ export const ProposalVotesTable = ({
? t('byTokenVote') ? t('byTokenVote')
: t('byLiquidityVote'); : t('byLiquidityVote');
const showDetailsIconClasses = classnames('mb-4', {
'rotate-180': showDetails,
});
return ( return (
<> <>
<SubHeading title={t('voteBreakdown')} /> <button
<RoundedWrapper> onClick={() => setShowDetails(!showDetails)}
<KeyValueTable data-testid="vote-breakdown-toggle"
data-testid="proposal-votes-table" >
numerical={true} <div className="flex items-center gap-3">
headingLevel={4} <SubHeading title={t('voteBreakdown')} />
> <div className={showDetailsIconClasses}>
<KeyValueTableRow> <Icon name="chevron-down" size={8} />
{t('expectedToPass')} </div>
{isUpdateMarket ? ( </div>
updateMarketWillPass ? ( </button>
<Thumbs up={true} text={updateMarketVotePassMethod} />
) : ( {showDetails && (
<Thumbs up={false} /> <RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
) <KeyValueTable
) : willPassByTokenVote ? ( data-testid="proposal-votes-table"
<Thumbs up={true} /> numerical={true}
) : ( headingLevel={4}
<Thumbs up={false} /> >
)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('majorityMet')}
{majorityMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
</KeyValueTableRow>
{isUpdateMarket && (
<KeyValueTableRow> <KeyValueTableRow>
{t('majorityLPMet')} {t('expectedToPass')}
{majorityLPMet ? <Thumbs up={true} /> : <Thumbs up={false} />} {isUpdateMarket ? (
</KeyValueTableRow> updateMarketWillPass ? (
)} <Thumbs up={true} text={updateMarketVotePassMethod} />
<KeyValueTableRow> ) : (
{t('participationMet')} <Thumbs up={false} />
{participationMet ? <Thumbs up={true} /> : <Thumbs up={false} />} )
</KeyValueTableRow> ) : willPassByTokenVote ? (
{isUpdateMarket && (
<KeyValueTableRow>
{t('participationLPMet')}
{participationLPMet ? (
<Thumbs up={true} /> <Thumbs up={true} />
) : ( ) : (
<Thumbs up={false} /> <Thumbs up={false} />
)} )}
</KeyValueTableRow> </KeyValueTableRow>
)}
<KeyValueTableRow>
{t('tokenForProposal')}
{formatNumber(yesTokens, 2)}
</KeyValueTableRow>
{isUpdateMarket && (
<KeyValueTableRow> <KeyValueTableRow>
{t('tokenLPForProposal')} {t('majorityMet')}
{formatNumber(yesEquityLikeShareWeight, 2)} {majorityMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
</KeyValueTableRow> </KeyValueTableRow>
)} {isUpdateMarket && (
<KeyValueTableRow>
{t('totalSupply')}
{formatNumber(totalSupply, 2)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('tokensAgainstProposal')}
{formatNumber(noTokens, 2)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('participationRequired')}
{formatNumberPercentage(requiredParticipation)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('majorityRequired')}
{formatNumberPercentage(requiredMajorityPercentage)}
</KeyValueTableRow>
{!isUpdateMarket && (
<>
<KeyValueTableRow> <KeyValueTableRow>
{t('numberOfVotingParties')} {t('majorityLPMet')}
{formatNumber(totalVotes, 0)} {majorityLPMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
</KeyValueTableRow> </KeyValueTableRow>
)}
<KeyValueTableRow>
{t('participationMet')}
{participationMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
</KeyValueTableRow>
{isUpdateMarket && (
<KeyValueTableRow> <KeyValueTableRow>
{t('totalTokensVotes')} {t('participationLPMet')}
{formatNumber(totalTokensVoted, 2)} {participationLPMet ? (
<Thumbs up={true} />
) : (
<Thumbs up={false} />
)}
</KeyValueTableRow> </KeyValueTableRow>
)}
<KeyValueTableRow>
{t('tokenForProposal')}
{formatNumber(yesTokens, 2)}
</KeyValueTableRow>
{isUpdateMarket && (
<KeyValueTableRow> <KeyValueTableRow>
{t('totalTokenVotedPercentage')} {t('tokenLPForProposal')}
{formatNumberPercentage(totalTokensPercentage, 2)} {formatNumber(yesEquityLikeShareWeight, 2)}
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow> )}
{t('numberOfForVotes')} <KeyValueTableRow>
{formatNumber(yesVotes, 0)} {t('totalSupply')}
</KeyValueTableRow> {formatNumber(totalSupply, 2)}
<KeyValueTableRow> </KeyValueTableRow>
{t('numberOfAgainstVotes')} <KeyValueTableRow>
{formatNumber(noVotes, 0)} {t('tokensAgainstProposal')}
</KeyValueTableRow> {formatNumber(noTokens, 2)}
<KeyValueTableRow> </KeyValueTableRow>
{t('yesPercentage')} <KeyValueTableRow>
{formatNumberPercentage(yesPercentage, 2)} {t('participationRequired')}
</KeyValueTableRow> {formatNumberPercentage(requiredParticipation)}
<KeyValueTableRow noBorder={true}> </KeyValueTableRow>
{t('noPercentage')} <KeyValueTableRow>
{formatNumberPercentage(noPercentage, 2)} {t('majorityRequired')}
</KeyValueTableRow> {formatNumberPercentage(requiredMajorityPercentage)}
</> </KeyValueTableRow>
)} {!isUpdateMarket && (
</KeyValueTable> <>
</RoundedWrapper> <KeyValueTableRow>
{t('numberOfVotingParties')}
{formatNumber(totalVotes, 0)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('totalTokensVotes')}
{formatNumber(totalTokensVoted, 2)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('totalTokenVotedPercentage')}
{formatNumberPercentage(totalTokensPercentage, 2)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('numberOfForVotes')}
{formatNumber(yesVotes, 0)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('numberOfAgainstVotes')}
{formatNumber(noVotes, 0)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('yesPercentage')}
{formatNumberPercentage(yesPercentage, 2)}
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
{t('noPercentage')}
{formatNumberPercentage(noPercentage, 2)}
</KeyValueTableRow>
</>
)}
</KeyValueTable>
</RoundedWrapper>
)}
</> </>
); );
}; };

View File

@ -2,7 +2,7 @@ import {
NetworkParams, NetworkParams,
useNetworkParams, useNetworkParams,
} from '@vegaprotocol/network-parameters'; } from '@vegaprotocol/network-parameters';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { ProposalHeader } from '../proposal-detail-header/proposal-header'; import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
@ -78,7 +78,7 @@ export const Proposal = ({ proposal }: ProposalProps) => {
<AsyncRenderer data={params} loading={loading} error={error}> <AsyncRenderer data={params} loading={loading} error={error}>
<section data-testid="proposal"> <section data-testid="proposal">
<ProposalHeader proposal={proposal} isListItem={false} /> <ProposalHeader proposal={proposal} isListItem={false} />
<div className="mb-10"> <div className="my-10">
<ProposalChangeTable proposal={proposal} /> <ProposalChangeTable proposal={proposal} />
</div> </div>
{proposal.terms.change.__typename === 'NewAsset' && {proposal.terms.change.__typename === 'NewAsset' &&
@ -91,14 +91,18 @@ export const Proposal = ({ proposal }: ProposalProps) => {
/> />
) : null} ) : null}
<div className="mb-12"> <div className="mb-12">
<VoteDetails <RoundedWrapper paddingBottom={true}>
proposal={proposal} <VoteDetails
proposalType={proposalType} proposal={proposal}
minVoterBalance={minVoterBalance} proposalType={proposalType}
spamProtectionMinTokens={params?.spam_protection_voting_min_tokens} minVoterBalance={minVoterBalance}
/> spamProtectionMinTokens={
params?.spam_protection_voting_min_tokens
}
/>
</RoundedWrapper>
</div> </div>
<div className="mb-10"> <div className="mb-4">
<ProposalVotesTable proposal={proposal} proposalType={proposalType} /> <ProposalVotesTable proposal={proposal} proposalType={proposalType} />
</div> </div>
<ProposalTermsJson terms={proposal.terms} /> <ProposalTermsJson terms={proposal.terms} />

View File

@ -97,7 +97,6 @@ describe('Proposals list item details', () => {
}, },
}) })
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Enacted');
expect(screen.getByTestId('vote-details')).toHaveTextContent( expect(screen.getByTestId('vote-details')).toHaveTextContent(
format(lastWeek, DATE_FORMAT_DETAILED) format(lastWeek, DATE_FORMAT_DETAILED)
); );
@ -113,7 +112,6 @@ describe('Proposals list item details', () => {
}, },
}) })
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Passed');
expect(screen.getByTestId('vote-details')).toHaveTextContent( expect(screen.getByTestId('vote-details')).toHaveTextContent(
`Enacts on ${format(nextWeek, DATE_FORMAT_DETAILED)}` `Enacts on ${format(nextWeek, DATE_FORMAT_DETAILED)}`
); );
@ -128,9 +126,6 @@ describe('Proposals list item details', () => {
}, },
}) })
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent(
'Waiting for node vote'
);
expect(screen.getByTestId('vote-details')).toHaveTextContent( expect(screen.getByTestId('vote-details')).toHaveTextContent(
`Enacts on ${format(nextWeek, DATE_FORMAT_DETAILED)}` `Enacts on ${format(nextWeek, DATE_FORMAT_DETAILED)}`
); );
@ -221,7 +216,6 @@ describe('Proposals list item details', () => {
}, },
}) })
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
expect(screen.getByTestId('vote-details')).toHaveTextContent( expect(screen.getByTestId('vote-details')).toHaveTextContent(
'5 minutes left to vote' '5 minutes left to vote'
); );
@ -236,7 +230,6 @@ describe('Proposals list item details', () => {
}, },
}) })
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
expect(screen.getByTestId('vote-details')).toHaveTextContent( expect(screen.getByTestId('vote-details')).toHaveTextContent(
'5 hours left to vote' '5 hours left to vote'
); );
@ -251,7 +244,6 @@ describe('Proposals list item details', () => {
}, },
}) })
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
expect(screen.getByTestId('vote-details')).toHaveTextContent( expect(screen.getByTestId('vote-details')).toHaveTextContent(
'5 days left to vote' '5 days left to vote'
); );
@ -268,10 +260,7 @@ describe('Proposals list item details', () => {
networkParamsQueryMock, networkParamsQueryMock,
createUserVoteQueryMock(proposal?.id, VoteValue.VALUE_YES), createUserVoteQueryMock(proposal?.id, VoteValue.VALUE_YES),
]); ]);
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(await screen.findByText('You voted For')).toBeInTheDocument();
expect(await screen.findByText('You voted')).toBeInTheDocument();
expect(await screen.findByText('For')).toBeInTheDocument();
}); });
it('Renders proposal state: Open - user voted against', async () => { it('Renders proposal state: Open - user voted against', async () => {
@ -285,9 +274,7 @@ describe('Proposals list item details', () => {
networkParamsQueryMock, networkParamsQueryMock,
createUserVoteQueryMock(proposal?.id, VoteValue.VALUE_NO), createUserVoteQueryMock(proposal?.id, VoteValue.VALUE_NO),
]); ]);
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(await screen.findByText('You voted Against')).toBeInTheDocument();
expect(await screen.findByText('You voted')).toBeInTheDocument();
expect(await screen.findByText('Against')).toBeInTheDocument();
}); });
it('Renders proposal state: Open - participation not reached', () => { it('Renders proposal state: Open - participation not reached', () => {
@ -303,7 +290,6 @@ describe('Proposals list item details', () => {
}, },
}) })
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
expect(screen.getByTestId('vote-status')).toHaveTextContent( expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Participation not reached' 'Participation not reached'
); );
@ -322,7 +308,6 @@ describe('Proposals list item details', () => {
}, },
}) })
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
expect(screen.getByTestId('vote-status')).toHaveTextContent( expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Majority not reached' 'Majority not reached'
); );
@ -342,7 +327,6 @@ describe('Proposals list item details', () => {
}, },
}) })
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
expect(screen.getByTestId('vote-status')).toHaveTextContent('Set to pass'); expect(screen.getByTestId('vote-status')).toHaveTextContent('Set to pass');
}); });
@ -359,7 +343,6 @@ describe('Proposals list item details', () => {
}, },
}) })
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Declined');
expect(screen.getByTestId('vote-status')).toHaveTextContent( expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Participation not reached' 'Participation not reached'
); );
@ -378,7 +361,6 @@ describe('Proposals list item details', () => {
}, },
}) })
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Declined');
expect(screen.getByTestId('vote-status')).toHaveTextContent( expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Majority not reached' 'Majority not reached'
); );
@ -395,7 +377,6 @@ describe('Proposals list item details', () => {
ProposalRejectionReason.PROPOSAL_ERROR_INVALID_FUTURE_PRODUCT, ProposalRejectionReason.PROPOSAL_ERROR_INVALID_FUTURE_PRODUCT,
}) })
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Rejected');
expect(screen.getByTestId('vote-status')).toHaveTextContent( expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Invalid future product' 'Invalid future product'
); );

View File

@ -1,11 +1,8 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button, Icon } from '@vegaprotocol/ui-toolkit'; import { Button } from '@vegaprotocol/ui-toolkit';
import { useVoteInformation } from '../../hooks'; import { useVoteInformation } from '../../hooks';
import { useUserVote } from '../vote-details/use-user-vote'; import { useUserVote } from '../vote-details/use-user-vote';
import { import { StatusPass } from '../current-proposal-status/current-proposal-status';
StatusPass,
StatusFail,
} from '../current-proposal-status/current-proposal-status';
import { format, formatDistanceToNowStrict } from 'date-fns'; import { format, formatDistanceToNowStrict } from 'date-fns';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats'; import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
@ -22,7 +19,7 @@ const MajorityNotReached = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <>
{t('Majority')} <StatusFail>{t('not reached')}</StatusFail> {t('Majority')} {t('not reached')}
</> </>
); );
}; };
@ -30,7 +27,7 @@ const ParticipationNotReached = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <>
{t('Participation')} <StatusFail>{t('not reached')}</StatusFail> {t('Participation')} {t('not reached')}
</> </>
); );
}; };
@ -57,17 +54,11 @@ export const ProposalsListItemDetails = ({
? t('byTokenVote') ? t('byTokenVote')
: t('byLPVote'); : t('byLPVote');
let proposalStatus: ReactNode;
let voteDetails: ReactNode; let voteDetails: ReactNode;
let voteStatus: ReactNode; let voteStatus: ReactNode;
switch (state) { switch (state) {
case ProposalState.STATE_ENACTED: { case ProposalState.STATE_ENACTED: {
proposalStatus = (
<>
{t('voteState_Enacted')} <Icon name={'tick'} />
</>
);
voteDetails = proposal?.terms.enactmentDatetime && ( voteDetails = proposal?.terms.enactmentDatetime && (
<> <>
{format( {format(
@ -79,11 +70,6 @@ export const ProposalsListItemDetails = ({
break; break;
} }
case ProposalState.STATE_PASSED: { case ProposalState.STATE_PASSED: {
proposalStatus = (
<>
{t('voteState_Passed')} <Icon name={'tick'} />
</>
);
voteDetails = proposal?.terms.change.__typename !== 'NewFreeform' && ( voteDetails = proposal?.terms.change.__typename !== 'NewFreeform' && (
<> <>
{t('toEnactOn')}{' '} {t('toEnactOn')}{' '}
@ -97,11 +83,6 @@ export const ProposalsListItemDetails = ({
break; break;
} }
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: { case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
proposalStatus = (
<>
{t('voteState_WaitingForNodeVote')} <Icon name={'time'} />
</>
);
voteDetails = proposal?.terms.change.__typename !== 'NewFreeform' && ( voteDetails = proposal?.terms.change.__typename !== 'NewFreeform' && (
<> <>
{t('toEnactOn')}{' '} {t('toEnactOn')}{' '}
@ -115,19 +96,14 @@ export const ProposalsListItemDetails = ({
break; break;
} }
case ProposalState.STATE_OPEN: { case ProposalState.STATE_OPEN: {
proposalStatus = (
<>
{t('voteState_Open')} <Icon name={'hand'} />
</>
);
voteDetails = (voteState === 'Yes' && ( voteDetails = (voteState === 'Yes' && (
<> <>
{t('youVoted')} <StatusPass>{t('voteState_Yes')}</StatusPass> {t('youVoted')} {t('voteState_Yes')}
</> </>
)) || )) ||
(voteState === 'No' && ( (voteState === 'No' && (
<> <>
{t('youVoted')} <StatusFail>{t('voteState_No')}</StatusFail> {t('youVoted')} {t('voteState_No')}
</> </>
)) || ( )) || (
<> <>
@ -148,40 +124,29 @@ export const ProposalsListItemDetails = ({
</> </>
) : ( ) : (
<> <>
{t('Set to')} <StatusFail>{t('fail')}</StatusFail> {t('Set to')} {t('fail')}
</> </>
))) || ))) ||
(!participationMet && <ParticipationNotReached />) || (!participationMet && <ParticipationNotReached />) ||
(!majorityMet && <MajorityNotReached />) || (!majorityMet && <MajorityNotReached />) ||
(willPassByTokenVote ? ( (willPassByTokenVote ? (
<> <>
{t('Set to')} <StatusPass>{t('pass')}</StatusPass> {t('Set to')} {t('pass')}
</> </>
) : ( ) : (
<> <>
{t('Set to')} <StatusFail>{t('fail')}</StatusFail> {t('Set to')} {t('fail')}
</> </>
)); ));
break; break;
} }
case ProposalState.STATE_DECLINED: { case ProposalState.STATE_DECLINED: {
proposalStatus = (
<>
{t('voteState_Declined')} <Icon name={'cross'} />
</>
);
voteStatus = voteStatus =
(!participationMet && <ParticipationNotReached />) || (!participationMet && <ParticipationNotReached />) ||
(!majorityMet && <MajorityNotReached />); (!majorityMet && <MajorityNotReached />);
break; break;
} }
case ProposalState.STATE_REJECTED: { case ProposalState.STATE_REJECTED: {
proposalStatus = (
<>
<StatusFail>{t('voteState_Rejected')}</StatusFail>{' '}
<Icon name={'warning-sign'} />
</>
);
voteStatus = proposal?.rejectionReason && ( voteStatus = proposal?.rejectionReason && (
<>{t(ProposalRejectionReasonMapping[proposal.rejectionReason])}</> <>{t(ProposalRejectionReasonMapping[proposal.rejectionReason])}</>
); );
@ -190,16 +155,10 @@ export const ProposalsListItemDetails = ({
} }
return ( return (
<div className="grid grid-cols-[1fr_auto] mt-2 items-start gap-2 text-sm"> <div className="grid grid-cols-[1fr_auto] mt-4 items-start gap-2 text-sm">
<div
className="col-start-1 row-start-1 flex items-center gap-2 text-white"
data-testid="proposal-status"
>
{proposalStatus}
</div>
{voteDetails && ( {voteDetails && (
<div <div
className="col-start-1 row-start-2 text-neutral-500" className="col-start-1 row-start-2 text-vega-light-300"
data-testid="vote-details" data-testid="vote-details"
> >
{voteDetails} {voteDetails}
@ -216,9 +175,7 @@ export const ProposalsListItemDetails = ({
{proposal?.id && ( {proposal?.id && (
<div className="col-start-2 row-start-2 justify-self-end"> <div className="col-start-2 row-start-2 justify-self-end">
<Link to={`${Routes.PROPOSALS}/${proposal.id}`}> <Link to={`${Routes.PROPOSALS}/${proposal.id}`}>
<Button data-testid="view-proposal-btn" size="sm"> <Button data-testid="view-proposal-btn">{t('View')}</Button>
{t('View')}
</Button>
</Link> </Link>
</div> </div>
)} )}

View File

@ -106,7 +106,7 @@ export const ProposalsList = ({
{proposals.length > 0 && ( {proposals.length > 0 && (
<ProposalsListFilter setFilterString={setFilterString} /> <ProposalsListFilter setFilterString={setFilterString} />
)} )}
<section className="-mx-4 p-4 mb-8 bg-neutral-800"> <section className="-mx-4 p-4 mb-8 bg-vega-dark-100">
<SubHeading title={t('openProposals')} /> <SubHeading title={t('openProposals')} />
{sortedProposals.open.length > 0 || {sortedProposals.open.length > 0 ||
sortedProtocolUpgradeProposals.open.length > 0 ? ( sortedProtocolUpgradeProposals.open.length > 0 ? (

View File

@ -3,15 +3,15 @@ import { Link } from 'react-router-dom';
import { import {
Button, Button,
Icon, Icon,
Intent,
Lozenge, Lozenge,
RoundedWrapper, RoundedWrapper,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { stripFullStops } from '@vegaprotocol/utils'; import { stripFullStops } from '@vegaprotocol/utils';
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types'; import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import type { ReactNode } from 'react'; import { ProposalInfoLabel } from '../proposal-info-label';
import Routes from '../../../routes'; import Routes from '../../../routes';
import type { ReactNode } from 'react';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals'; import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
interface ProtocolProposalsListItemProps { interface ProtocolProposalsListItemProps {
@ -29,30 +29,30 @@ export const ProtocolUpgradeProposalsListItem = ({
switch (proposal.status) { switch (proposal.status) {
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED: case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED:
proposalStatusIcon = ( proposalStatusIcon = (
<div data-testid="protocol-upgrade-proposal-status-icon-rejected"> <span data-testid="protocol-upgrade-proposal-status-icon-rejected">
<Icon name={'cross'} /> <Icon name={'cross'} />
</div> </span>
); );
break; break;
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING: case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING:
proposalStatusIcon = ( proposalStatusIcon = (
<div data-testid="protocol-upgrade-proposal-status-icon-pending"> <span data-testid="protocol-upgrade-proposal-status-icon-pending">
<Icon name={'time'} /> <Icon name={'time'} />
</div> </span>
); );
break; break;
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED: case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED:
proposalStatusIcon = ( proposalStatusIcon = (
<div data-testid="protocol-upgrade-proposal-status-icon-approved"> <span data-testid="protocol-upgrade-proposal-status-icon-approved">
<Icon name={'tick'} /> <Icon name={'tick'} />
</div> </span>
); );
break; break;
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED: case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED:
proposalStatusIcon = ( proposalStatusIcon = (
<div data-testid="protocol-upgrade-proposal-status-icon-unspecified"> <span data-testid="protocol-upgrade-proposal-status-icon-unspecified">
<Icon name={'disable'} /> <Icon name={'disable'} />
</div> </span>
); );
break; break;
} }
@ -71,18 +71,28 @@ export const ProtocolUpgradeProposalsListItem = ({
</div> </div>
<div className="text-sm"> <div className="text-sm">
<div <div className="flex gap-2">
data-testid="protocol-upgrade-proposal-type" <div
className="flex items-center gap-2 mb-4" data-testid="protocol-upgrade-proposal-type"
> className="flex items-center gap-2 mb-4"
<Lozenge variant={Intent.Success}>{t('networkUpgrade')}</Lozenge> >
<ProposalInfoLabel variant="highlight">
{t('networkUpgrade')}
</ProposalInfoLabel>
</div>
<div data-testid="protocol-upgrade-proposal-status">
<ProposalInfoLabel>
{t(`${proposal.status}`)} {proposalStatusIcon}
</ProposalInfoLabel>
</div>
</div> </div>
<div <div
data-testid="protocol-upgrade-proposal-release-tag" data-testid="protocol-upgrade-proposal-release-tag"
className="mb-2" className="mb-2"
> >
<span className="pr-2">{t('vegaReleaseTag')}</span> <span>{t('vegaReleaseTag')}:</span>{' '}
<Lozenge>{proposal.vegaReleaseTag}</Lozenge> <Lozenge>{proposal.vegaReleaseTag}</Lozenge>
</div> </div>
@ -90,30 +100,18 @@ export const ProtocolUpgradeProposalsListItem = ({
data-testid="protocol-upgrade-proposal-block-height" data-testid="protocol-upgrade-proposal-block-height"
className="mb-2" className="mb-2"
> >
<span className="pr-2">{t('upgradeBlockHeight')}</span> <span>{t('upgradeBlockHeight')}:</span>{' '}
<Lozenge>{proposal.upgradeBlockHeight}</Lozenge> <Lozenge>{proposal.upgradeBlockHeight}</Lozenge>
</div> </div>
<div className="grid grid-cols-[1fr_auto] mt-3 items-start gap-2"> <div className="grid grid-cols-1 mt-3">
<div className="col-start-1 row-start-1 text-white"> <div className="justify-self-end">
<div
data-testid="protocol-upgrade-proposal-status"
className="flex items-center gap-2"
>
<span>{t(`${proposal.status}`)}</span>
<span>{proposalStatusIcon}</span>
</div>
</div>
<div className="col-start-2 row-start-2 justify-self-end">
<Link <Link
to={`${Routes.PROPOSALS}/protocol-upgrade/${stripFullStops( to={`${Routes.PROPOSALS}/protocol-upgrade/${stripFullStops(
proposal.vegaReleaseTag proposal.vegaReleaseTag
)}`} )}`}
> >
<Button data-testid="view-proposal-btn" size="sm"> <Button data-testid="view-proposal-btn">{t('View')}</Button>
{t('View')}
</Button>
</Link> </Link>
</div> </div>
</div> </div>

View File

@ -107,10 +107,6 @@ export const VoteButtons = ({
); );
} }
if (currentStakeAvailable.isLessThanOrEqualTo(0)) {
return t('noGovernanceTokens');
}
if (minVoterBalance && spamProtectionMinTokens) { if (minVoterBalance && spamProtectionMinTokens) {
const formattedMinVoterBalance = new BigNumber( const formattedMinVoterBalance = new BigNumber(
addDecimal(minVoterBalance, 18) addDecimal(minVoterBalance, 18)
@ -163,24 +159,30 @@ export const VoteButtons = ({
return ( return (
<> <>
{changeVote || (voteState === VoteState.NotCast && proposalVotable) ? ( {changeVote || (voteState === VoteState.NotCast && proposalVotable) ? (
<div className="flex gap-4" data-testid="vote-buttons"> <>
<div className="flex-1"> {currentStakeAvailable.isLessThanOrEqualTo(0) && (
<p data-testid="no-stake-available">{t('noGovernanceTokens')}</p>
)}
<div className="flex gap-4" data-testid="vote-buttons">
<Button <Button
data-testid="vote-for" data-testid="vote-for"
onClick={() => submitVote(VoteValue.VALUE_YES)} onClick={() => submitVote(VoteValue.VALUE_YES)}
variant="primary"
disabled={currentStakeAvailable.isLessThanOrEqualTo(0)}
> >
{t('voteFor')} {t('voteFor')}
</Button> </Button>
</div>
<div className="flex-1">
<Button <Button
data-testid="vote-against" data-testid="vote-against"
onClick={() => submitVote(VoteValue.VALUE_NO)} onClick={() => submitVote(VoteValue.VALUE_NO)}
variant="primary"
disabled={currentStakeAvailable.isLessThanOrEqualTo(0)}
> >
{t('voteAgainst')} {t('voteAgainst')}
</Button> </Button>
</div> </div>
</div> </>
) : ( ) : (
(voteState === VoteState.Yes || voteState === VoteState.No) && ( (voteState === VoteState.Yes || voteState === VoteState.No) && (
<p data-testid="you-voted"> <p data-testid="you-voted">

View File

@ -1,5 +1,6 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { RoundedWrapper, Icon } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { ProposalState } from '@vegaprotocol/types'; import { ProposalState } from '@vegaprotocol/types';
import { useVoteSubmit, VoteProgress } from '@vegaprotocol/proposals'; import { useVoteSubmit, VoteProgress } from '@vegaprotocol/proposals';
@ -199,10 +200,11 @@ export const VoteDetails = ({
{proposalType === ProposalType.PROPOSAL_UPDATE_MARKET && ( {proposalType === ProposalType.PROPOSAL_UPDATE_MARKET && (
<p>{t('votingThresholdInfo')}</p> <p>{t('votingThresholdInfo')}</p>
)} )}
{pubKey ? (
<section className="mt-10"> <section className="mt-10">
<SubHeading title={t('yourVote')} /> <SubHeading title={t('castYourVote')} />
{proposal && ( {pubKey ? (
proposal && (
<VoteButtonsContainer <VoteButtonsContainer
voteState={voteState} voteState={voteState}
voteDatetime={voteDatetime} voteDatetime={voteDatetime}
@ -214,11 +216,19 @@ export const VoteDetails = ({
submit={submit} submit={submit}
dialog={Dialog} dialog={Dialog}
/> />
)} )
</section> ) : (
) : ( <RoundedWrapper paddingBottom={true}>
<ConnectToVega /> <div className="mb-4">
)} <div className="flex items-center gap-2 mb-2">
<Icon name={'info-sign'} />
<div>{t('connectAVegaWalletToVote')}</div>
</div>
</div>
<ConnectToVega />
</RoundedWrapper>
)}
</section>
</section> </section>
</> </>
); );

View File

@ -13,7 +13,7 @@ export const getIntentBorder = (intent = Intent.None) => {
'border-warning': intent === Intent.Warning, 'border-warning': intent === Intent.Warning,
'border-neutral-500': intent === Intent.None, 'border-neutral-500': intent === Intent.None,
'border-vega-blue-300': intent === Intent.Primary, 'border-vega-blue-300': intent === Intent.Primary,
'border-success': intent === Intent.Success, 'border-vega-green': intent === Intent.Success,
}; };
}; };

View File

@ -1,6 +1,6 @@
import once from 'lodash/once'; import once from 'lodash/once';
import { format } from 'date-fns';
import { getUserLocale } from '../get-user-locale'; import { getUserLocale } from '../get-user-locale';
import { utcToZonedTime, format as tzFormat } from 'date-fns-tz';
export const isValidDate = (date: Date) => export const isValidDate = (date: Date) =>
date instanceof Date && !isNaN(date.getTime()); date instanceof Date && !isNaN(date.getTime());
@ -53,15 +53,24 @@ export const formatForInput = (date: Date) => {
return `${year}-${month}-${day}T${hours}:${minutes}:${secs}`; return `${year}-${month}-${day}T${hours}:${minutes}:${secs}`;
}; };
/** Returns a user's local time zone abbreviation */
export const getTimeZoneAbbreviation = (date: Date) => {
const formatter = new Intl.DateTimeFormat(undefined, {
timeZoneName: 'short',
});
const parts = formatter.formatToParts(date);
const timeZoneAbbreviation = parts.find(
(part) => part.type === 'timeZoneName'
)?.value;
return timeZoneAbbreviation || '';
};
/** Format a user's local date and time with the time zone abbreviation */ /** Format a user's local date and time with the time zone abbreviation */
export const formatDateWithLocalTimezone = ( export const formatDateWithLocalTimezone = (
date: Date, date: Date,
formatStr = 'dd MMMM yyyy HH:mm (z)' formatStr = 'dd MMMM yyyy HH:mm'
) => { ) => {
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; const formattedDate = format(date, formatStr);
const localDatetime = utcToZonedTime(date, userTimeZone); const timeZoneAbbreviation = getTimeZoneAbbreviation(date);
return `${formattedDate} (${timeZoneAbbreviation})`;
return tzFormat(localDatetime, formatStr, {
timeZone: userTimeZone,
});
}; };