feat(governance): proposal details page improvements (#3611)
This commit is contained in:
parent
7f949a276c
commit
313eff1c95
@ -101,11 +101,11 @@ context(
|
||||
);
|
||||
cy.getByTestId('protocol-upgrade-proposal-release-tag').should(
|
||||
'have.text',
|
||||
'Vega release tagv1'
|
||||
'Vega release tag: v1'
|
||||
);
|
||||
cy.getByTestId('protocol-upgrade-proposal-block-height').should(
|
||||
'have.text',
|
||||
'Upgrade block height2015942'
|
||||
'Upgrade block height: 2015942'
|
||||
);
|
||||
cy.getByTestId('protocol-upgrade-proposal-status').should(
|
||||
'have.text',
|
||||
|
@ -23,6 +23,7 @@ export const ConnectToVega = () => {
|
||||
openVegaWalletDialog();
|
||||
}}
|
||||
data-testid="connect-to-vega-wallet-btn"
|
||||
variant="primary"
|
||||
>
|
||||
{t('connectVegaWallet')}
|
||||
</Button>
|
||||
|
@ -23,7 +23,7 @@ export const Heading = ({
|
||||
})}
|
||||
>
|
||||
<h1
|
||||
className={classNames('font-alpha calt text-5xl', {
|
||||
className={classNames('font-alpha calt text-5xl break-words', {
|
||||
'mt-0': !marginTop,
|
||||
'mb-0': !marginBottom,
|
||||
})}
|
||||
|
@ -201,7 +201,7 @@
|
||||
"NewFreeform": "Freeform",
|
||||
"tokenVotes": "Token votes",
|
||||
"liquidityVotes": "Liquidity votes",
|
||||
"yourVote": "Your vote",
|
||||
"castYourVote": "Cast your vote",
|
||||
"for": "For",
|
||||
"against": "Against",
|
||||
"majorityRequired": "Majority Required",
|
||||
@ -587,7 +587,7 @@
|
||||
"tokensAgainstProposal": "Tokens against proposal",
|
||||
"participationRequired": "Participation required",
|
||||
"numberOfVotingParties": "Number of voting parties",
|
||||
"totalTokensVotes": "Total yes tokens",
|
||||
"totalTokensVotes": "Total tokens voted",
|
||||
"totalTokenVotedPercentage": "Total tokens voted percentage",
|
||||
"numberOfForVotes": "Number of votes for",
|
||||
"numberOfAgainstVotes": "Number of votes against",
|
||||
@ -631,7 +631,10 @@
|
||||
"New market": "New market",
|
||||
"Market change": "Market change",
|
||||
"Network parameter": "Network parameter",
|
||||
"Change": "Change",
|
||||
"Unknown proposal": "Unknown proposal",
|
||||
"ERC20ContractAddress": "ERC20 contract address",
|
||||
"MaxFaucetAmountMint": "Max faucet amount mint",
|
||||
"Code": "Code",
|
||||
"settled future": "settled future",
|
||||
"Symbol": "Symbol",
|
||||
@ -677,6 +680,7 @@
|
||||
"NewProposal": "New proposal",
|
||||
"ProposalTypeQuestion": "What type of proposal would you like to make?",
|
||||
"NetworkParameterProposal": "Update network parameter proposal",
|
||||
"parameter": "parameter",
|
||||
"NewMarketProposal": "New market proposal",
|
||||
"UpdateMarketProposal": "Update market proposal",
|
||||
"NewAssetProposal": "New asset proposal",
|
||||
@ -694,6 +698,7 @@
|
||||
"UpdateMarket": "Update market",
|
||||
"NewAsset": "New asset",
|
||||
"UpdateAsset": "Update asset",
|
||||
"AssetID": "Asset ID",
|
||||
"Freeform": "Freeform",
|
||||
"RawProposal": "Let me choose (raw proposal)",
|
||||
"UseMin": "Use minimum",
|
||||
@ -737,6 +742,7 @@
|
||||
"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.",
|
||||
"FreeformProposal": "Freeform proposal",
|
||||
"Id": "ID",
|
||||
"unknownReason": "unknown reason",
|
||||
"votingEnded": "Voting has ended.",
|
||||
"STATUS": "STATUS",
|
||||
@ -791,5 +797,7 @@
|
||||
"67% voting power required": "67% voting power required",
|
||||
"Token": "Token",
|
||||
"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"
|
||||
}
|
||||
|
@ -1,7 +1,11 @@
|
||||
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 { 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 = ({
|
||||
proposal,
|
||||
@ -9,19 +13,63 @@ export const CurrentProposalState = ({
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
let className = 'text-white';
|
||||
let proposalStatus: ReactNode;
|
||||
let variant = 'tertiary' as ProposalInfoLabelVariant;
|
||||
|
||||
if (
|
||||
proposal?.state === Schema.ProposalState.STATE_DECLINED ||
|
||||
proposal?.state === Schema.ProposalState.STATE_FAILED ||
|
||||
proposal?.state === Schema.ProposalState.STATE_REJECTED
|
||||
) {
|
||||
className = 'text-danger';
|
||||
} else if (
|
||||
proposal?.state === Schema.ProposalState.STATE_ENACTED ||
|
||||
proposal?.state === Schema.ProposalState.STATE_PASSED
|
||||
) {
|
||||
className = 'text-white';
|
||||
switch (proposal?.state) {
|
||||
case ProposalState.STATE_ENACTED: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
<span className="mr-2">{t('voteState_Enacted')}</span>
|
||||
<Icon name={'tick'} />
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
return <span className={className}>{t(`${proposal?.state}`)}</span>;
|
||||
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 (
|
||||
<ProposalInfoLabel variant={variant}>{proposalStatus}</ProposalInfoLabel>
|
||||
);
|
||||
};
|
||||
|
@ -16,17 +16,12 @@ it('Renders all data for table', () => {
|
||||
render(<ProposalChangeTable proposal={proposal} />);
|
||||
expect(screen.getByText('ID')).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(
|
||||
formatDateWithLocalTimezone(new Date(proposal?.terms.closingDatetime))
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Proposed enactment')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
@ -35,17 +30,12 @@ it('Renders all data for table', () => {
|
||||
)
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Proposed by')).toBeInTheDocument();
|
||||
expect(screen.getByText(proposal?.party.id ?? '')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Proposed on')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(formatDateWithLocalTimezone(new Date(proposal?.datetime)))
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Type')).toBeInTheDocument();
|
||||
expect(screen.getByText('Network parameter')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
render(<ProposalChangeTable proposal={proposal} />);
|
||||
|
||||
expect(screen.getByText('State')).toBeInTheDocument();
|
||||
expect(screen.getByText('Enacted')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Closed on')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
formatDateWithLocalTimezone(new Date(proposal?.terms.closingDatetime))
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Enacted on')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
@ -104,7 +89,6 @@ it('Renders error details and rejection reason if present', () => {
|
||||
render(<ProposalChangeTable proposal={proposal} />);
|
||||
expect(screen.getByText('Error details')).toBeInTheDocument();
|
||||
expect(screen.getByText(errorDetails)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Rejection reason')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(ProposalRejectionReason.PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE)
|
||||
|
@ -6,7 +6,6 @@ import {
|
||||
KeyValueTableRow,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { CurrentProposalState } from '../current-proposal-state';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
|
||||
@ -20,16 +19,12 @@ export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
|
||||
const terms = proposal?.terms;
|
||||
|
||||
return (
|
||||
<RoundedWrapper>
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
<KeyValueTable data-testid="proposal-change-table">
|
||||
<KeyValueTableRow>
|
||||
{t('id')}
|
||||
{proposal?.id}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('state')}
|
||||
<CurrentProposalState proposal={proposal} />
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{isFuture(new Date(terms?.closingDatetime))
|
||||
? t('closesOn')
|
||||
@ -50,26 +45,24 @@ export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
|
||||
{t('proposedBy')}
|
||||
<span style={{ wordBreak: 'break-word' }}>{proposal?.party.id}</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<KeyValueTableRow
|
||||
noBorder={!proposal?.rejectionReason && !proposal?.errorDetails}
|
||||
>
|
||||
{t('proposedOn')}
|
||||
{formatDateWithLocalTimezone(new Date(proposal?.datetime))}
|
||||
</KeyValueTableRow>
|
||||
{proposal?.rejectionReason ? (
|
||||
<KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={!proposal?.errorDetails}>
|
||||
{t('rejectionReason')}
|
||||
{proposal.rejectionReason}
|
||||
</KeyValueTableRow>
|
||||
) : null}
|
||||
{proposal?.errorDetails ? (
|
||||
<KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
{t('errorDetails')}
|
||||
{proposal.errorDetails}
|
||||
</KeyValueTableRow>
|
||||
) : null}
|
||||
<KeyValueTableRow>
|
||||
{t('type')}
|
||||
{t(`${proposal?.terms.change.__typename}`)}
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
);
|
||||
|
@ -1,16 +1,21 @@
|
||||
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 type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
|
||||
import { lastWeek, nextWeek } from '../../test-helpers/mocks';
|
||||
|
||||
const renderComponent = (
|
||||
proposal: ProposalQuery['proposal'],
|
||||
isListItem = true
|
||||
) => <ProposalHeader proposal={proposal} isListItem={isListItem} />;
|
||||
) => render(<ProposalHeader proposal={proposal} isListItem={isListItem} />);
|
||||
|
||||
describe('Proposal header', () => {
|
||||
it('Renders New market proposal', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
rationale: {
|
||||
@ -35,7 +40,6 @@ describe('Proposal header', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||
'New some market'
|
||||
@ -47,7 +51,6 @@ describe('Proposal header', () => {
|
||||
});
|
||||
|
||||
it('Renders Update market proposal', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
rationale: {
|
||||
@ -60,7 +63,6 @@ describe('Proposal header', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||
'New market id'
|
||||
@ -77,7 +79,6 @@ describe('Proposal header', () => {
|
||||
});
|
||||
|
||||
it('Renders New asset proposal - ERC20', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
rationale: {
|
||||
@ -96,19 +97,17 @@ describe('Proposal header', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||
'New asset: Fake currency'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent('New asset');
|
||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||
'Symbol: FAKE. ERC20 0x0'
|
||||
'Symbol: FAKE. ERC20 contract address: 0x0'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders New asset proposal - BuiltInAsset', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
terms: {
|
||||
@ -123,7 +122,6 @@ describe('Proposal header', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||
'Unknown proposal'
|
||||
@ -135,7 +133,6 @@ describe('Proposal header', () => {
|
||||
});
|
||||
|
||||
it('Renders Update network', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
rationale: {
|
||||
@ -152,7 +149,6 @@ describe('Proposal header', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||
'Network parameter'
|
||||
@ -165,8 +161,7 @@ describe('Proposal header', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders Freeform network - short rationale', () => {
|
||||
render(
|
||||
it('Renders Freeform proposal - short rationale', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
id: 'short',
|
||||
@ -179,18 +174,15 @@ describe('Proposal header', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent('0x0');
|
||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform');
|
||||
expect(
|
||||
screen.queryByTestId('proposal-description')
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent('short');
|
||||
});
|
||||
|
||||
it('Renders Freeform proposal - long rationale (105 chars) - listing', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
id: 'long',
|
||||
@ -205,7 +197,6 @@ describe('Proposal header', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent('0x0');
|
||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform');
|
||||
@ -213,11 +204,9 @@ describe('Proposal header', () => {
|
||||
expect(
|
||||
screen.queryByTestId('proposal-description')
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent('long');
|
||||
});
|
||||
|
||||
it('Renders Freeform proposal - long rationale (105 chars) - details', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
id: 'long',
|
||||
@ -233,7 +222,6 @@ describe('Proposal header', () => {
|
||||
},
|
||||
}),
|
||||
false
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-description')).toHaveTextContent(
|
||||
/Class aptent/
|
||||
@ -242,7 +230,6 @@ describe('Proposal header', () => {
|
||||
|
||||
// Remove once proposals have rationale and re-enable above tests
|
||||
it('Renders Freeform proposal - id for title', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
id: 'freeform id',
|
||||
@ -255,20 +242,15 @@ describe('Proposal header', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent('freeform');
|
||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform');
|
||||
expect(
|
||||
screen.queryByTestId('proposal-description')
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('proposal-details')).toHaveTextContent(
|
||||
'freeform id'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders asset change proposal header', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
terms: {
|
||||
@ -278,7 +260,6 @@ describe('Proposal header', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent(
|
||||
'Update asset'
|
||||
@ -287,7 +268,6 @@ describe('Proposal header', () => {
|
||||
});
|
||||
|
||||
it("Renders unknown proposal if it's a different proposal type", () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
terms: {
|
||||
@ -297,10 +277,95 @@ describe('Proposal header', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||
'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');
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Intent, Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||
import { Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||
import { shorten } from '@vegaprotocol/utils';
|
||||
import { Heading, SubHeading } from '../../../../components/heading';
|
||||
import type { ReactNode } from 'react';
|
||||
@ -7,6 +7,8 @@ import type { ProposalFieldsFragment } from '../../proposals/__generated__/Propo
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { truncateMiddle } from '../../../../lib/truncate-middle';
|
||||
import { CurrentProposalState } from '../current-proposal-state';
|
||||
import { ProposalInfoLabel } from '../proposal-info-label';
|
||||
|
||||
export const ProposalHeader = ({
|
||||
proposal,
|
||||
@ -19,7 +21,7 @@ export const ProposalHeader = ({
|
||||
const change = proposal?.terms.change;
|
||||
|
||||
let details: ReactNode;
|
||||
let proposalType: ReactNode;
|
||||
let proposalType = '';
|
||||
|
||||
let title = proposal?.rationale.title.trim();
|
||||
let description = proposal?.rationale.description.trim();
|
||||
@ -32,10 +34,12 @@ export const ProposalHeader = ({
|
||||
|
||||
switch (change?.__typename) {
|
||||
case 'NewMarket': {
|
||||
proposalType = t('NewMarket');
|
||||
proposalType = 'NewMarket';
|
||||
details = (
|
||||
<>
|
||||
{t('Code')}: {change.instrument.code}.{' '}
|
||||
<span>
|
||||
{t('Code')}: {change.instrument.code}.
|
||||
</span>{' '}
|
||||
{change.instrument.futureProduct?.settlementAsset.symbol ? (
|
||||
<>
|
||||
<span className="font-semibold">
|
||||
@ -51,53 +55,60 @@ export const ProposalHeader = ({
|
||||
break;
|
||||
}
|
||||
case 'UpdateMarket': {
|
||||
proposalType = t('UpdateMarket');
|
||||
details = `${t('Market change')}: ${change.marketId}`;
|
||||
proposalType = 'UpdateMarket';
|
||||
details = (
|
||||
<>
|
||||
<span>{t('Market change')}:</span>{' '}
|
||||
<span>{truncateMiddle(change.marketId)}</span>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'NewAsset': {
|
||||
proposalType = t('NewAsset');
|
||||
proposalType = 'NewAsset';
|
||||
details = (
|
||||
<>
|
||||
{t('Symbol')}: {change.symbol}.{' '}
|
||||
<Lozenge>
|
||||
{change.source.__typename === 'ERC20' &&
|
||||
`ERC20 ${change.source.contractAddress}`}
|
||||
{change.source.__typename === 'BuiltinAsset' &&
|
||||
`${t('Max faucet amount mint')}: ${
|
||||
change.source.maxFaucetAmountMint
|
||||
}`}
|
||||
</Lozenge>
|
||||
<span>{t('Symbol')}:</span> <Lozenge>{change.symbol}.</Lozenge>{' '}
|
||||
{change.source.__typename === 'ERC20' && (
|
||||
<>
|
||||
<span>{t('ERC20ContractAddress')}:</span>{' '}
|
||||
<Lozenge>{change.source.contractAddress}</Lozenge>
|
||||
</>
|
||||
)}{' '}
|
||||
{change.source.__typename === 'BuiltinAsset' && (
|
||||
<>
|
||||
<span>{t('MaxFaucetAmountMint')}:</span>{' '}
|
||||
<Lozenge>{change.source.maxFaucetAmountMint}</Lozenge>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'UpdateNetworkParameter': {
|
||||
proposalType = t('NetworkParameter');
|
||||
const parametersClasses = 'font-mono leading-none';
|
||||
proposalType = 'NetworkParameter';
|
||||
details = (
|
||||
<>
|
||||
<span className={`${parametersClasses} mr-2`}>
|
||||
{change.networkParameter.key}
|
||||
</span>{' '}
|
||||
{t('to')}{' '}
|
||||
<span className={`${parametersClasses} ml-2`}>
|
||||
{change.networkParameter.value}
|
||||
<span>{t('Change')}:</span>{' '}
|
||||
<Lozenge>{change.networkParameter.key}</Lozenge>{' '}
|
||||
<span>{t('to')}</span>{' '}
|
||||
<span className="whitespace-nowrap">
|
||||
<Lozenge>{change.networkParameter.value}</Lozenge>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'NewFreeform': {
|
||||
proposalType = t('Freeform');
|
||||
details = `${t('FreeformProposal')}: ${proposal?.id}`;
|
||||
proposalType = 'Freeform';
|
||||
details = <span />;
|
||||
break;
|
||||
}
|
||||
case 'UpdateAsset': {
|
||||
proposalType = t('UpdateAsset');
|
||||
proposalType = 'UpdateAsset';
|
||||
details = (
|
||||
<>
|
||||
<span>{t('Asset ID')}:</span>
|
||||
<span>{t('AssetID')}:</span>{' '}
|
||||
<Lozenge>{truncateMiddle(change.assetId)}</Lozenge>
|
||||
</>
|
||||
);
|
||||
@ -106,7 +117,7 @@ export const ProposalHeader = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-sm mb-2">
|
||||
<>
|
||||
<div data-testid="proposal-title">
|
||||
{isListItem ? (
|
||||
<header>
|
||||
@ -118,15 +129,27 @@ export const ProposalHeader = ({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
{proposalType && (
|
||||
<div data-testid="proposal-type">
|
||||
<Lozenge variant={Intent.None}>{proposalType}</Lozenge>
|
||||
<ProposalInfoLabel variant="secondary">
|
||||
{t(`${proposalType}`)}
|
||||
</ProposalInfoLabel>
|
||||
</div>
|
||||
|
||||
<div data-testid="proposal-status">
|
||||
<CurrentProposalState proposal={proposal} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{details && (
|
||||
<div data-testid="proposal-details" className="break-words my-10">
|
||||
{details}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
{description && !isListItem && (
|
||||
<div data-testid="proposal-description" className="mb-4">
|
||||
<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 */
|
||||
@ -139,9 +162,6 @@ export const ProposalHeader = ({
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{details && <div data-testid="proposal-details">{details}</div>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1 @@
|
||||
export * from './proposal-info-label';
|
@ -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>;
|
||||
};
|
@ -1,8 +1,10 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { Icon, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
import { useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export const ProposalTermsJson = ({
|
||||
terms,
|
||||
@ -10,10 +12,26 @@ export const ProposalTermsJson = ({
|
||||
terms: PartialDeep<Schema.ProposalTerms>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const showDetailsIconClasses = classnames('mb-4', {
|
||||
'rotate-180': showDetails,
|
||||
});
|
||||
|
||||
return (
|
||||
<section>
|
||||
<button
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
data-testid="proposal-terms-toggle"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<SubHeading title={t('proposalTerms')} />
|
||||
<SyntaxHighlighter data={terms} />
|
||||
<div className={showDetailsIconClasses}>
|
||||
<Icon name="chevron-down" size={8} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{showDetails && <SyntaxHighlighter data={terms} />}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
@ -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 { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
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', () => {
|
||||
renderComponent();
|
||||
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
|
||||
expect(screen.getByText('Expected to pass')).toBeInTheDocument();
|
||||
expect(screen.getByText('Token majority 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('Majority Required')).toBeInTheDocument();
|
||||
expect(screen.getByText('Number of voting parties')).toBeInTheDocument();
|
||||
expect(screen.getByText('Total yes tokens')).toBeInTheDocument();
|
||||
expect(screen.getByText('Total tokens voted')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Total tokens voted percentage')
|
||||
).toBeInTheDocument();
|
||||
@ -70,13 +71,14 @@ describe('Proposal Votes Table', () => {
|
||||
|
||||
it('displays different breakdown fields for update market proposal', () => {
|
||||
renderComponent(updateMarketProposal, updateMarketProposalType);
|
||||
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
|
||||
expect(screen.getByText('Liquidity majority met')).toBeInTheDocument();
|
||||
expect(screen.getByText('Liquidity participation met')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Liquidity shares for proposal')
|
||||
).toBeInTheDocument();
|
||||
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('Number of votes for')).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', () => {
|
||||
renderComponent(updateMarketProposal, updateMarketProposalType);
|
||||
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
|
||||
expect(screen.getByText('👍 by token vote')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -110,6 +113,7 @@ describe('Proposal Votes Table', () => {
|
||||
}),
|
||||
updateMarketProposalType
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
|
||||
expect(screen.getByText('👍 by liquidity vote')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -1,9 +1,12 @@
|
||||
import classnames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
Thumbs,
|
||||
RoundedWrapper,
|
||||
Icon,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { formatNumber, formatNumberPercentage } from '@vegaprotocol/utils';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
@ -26,6 +29,7 @@ export const ProposalVotesTable = ({
|
||||
const {
|
||||
appState: { totalSupply },
|
||||
} = useAppState();
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const {
|
||||
willPassByTokenVote,
|
||||
willPassByLPVote,
|
||||
@ -53,10 +57,26 @@ export const ProposalVotesTable = ({
|
||||
? t('byTokenVote')
|
||||
: t('byLiquidityVote');
|
||||
|
||||
const showDetailsIconClasses = classnames('mb-4', {
|
||||
'rotate-180': showDetails,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
data-testid="vote-breakdown-toggle"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<SubHeading title={t('voteBreakdown')} />
|
||||
<RoundedWrapper>
|
||||
<div className={showDetailsIconClasses}>
|
||||
<Icon name="chevron-down" size={8} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{showDetails && (
|
||||
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
|
||||
<KeyValueTable
|
||||
data-testid="proposal-votes-table"
|
||||
numerical={true}
|
||||
@ -160,6 +180,7 @@ export const ProposalVotesTable = ({
|
||||
)}
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} 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 type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
@ -78,7 +78,7 @@ export const Proposal = ({ proposal }: ProposalProps) => {
|
||||
<AsyncRenderer data={params} loading={loading} error={error}>
|
||||
<section data-testid="proposal">
|
||||
<ProposalHeader proposal={proposal} isListItem={false} />
|
||||
<div className="mb-10">
|
||||
<div className="my-10">
|
||||
<ProposalChangeTable proposal={proposal} />
|
||||
</div>
|
||||
{proposal.terms.change.__typename === 'NewAsset' &&
|
||||
@ -91,14 +91,18 @@ export const Proposal = ({ proposal }: ProposalProps) => {
|
||||
/>
|
||||
) : null}
|
||||
<div className="mb-12">
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
<VoteDetails
|
||||
proposal={proposal}
|
||||
proposalType={proposalType}
|
||||
minVoterBalance={minVoterBalance}
|
||||
spamProtectionMinTokens={params?.spam_protection_voting_min_tokens}
|
||||
spamProtectionMinTokens={
|
||||
params?.spam_protection_voting_min_tokens
|
||||
}
|
||||
/>
|
||||
</RoundedWrapper>
|
||||
</div>
|
||||
<div className="mb-10">
|
||||
<div className="mb-4">
|
||||
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
|
||||
</div>
|
||||
<ProposalTermsJson terms={proposal.terms} />
|
||||
|
@ -97,7 +97,6 @@ describe('Proposals list item details', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Enacted');
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
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(
|
||||
`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(
|
||||
`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(
|
||||
'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(
|
||||
'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(
|
||||
'5 days left to vote'
|
||||
);
|
||||
@ -268,10 +260,7 @@ describe('Proposals list item details', () => {
|
||||
networkParamsQueryMock,
|
||||
createUserVoteQueryMock(proposal?.id, VoteValue.VALUE_YES),
|
||||
]);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
|
||||
expect(await screen.findByText('You voted')).toBeInTheDocument();
|
||||
expect(await screen.findByText('For')).toBeInTheDocument();
|
||||
expect(await screen.findByText('You voted For')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - user voted against', async () => {
|
||||
@ -285,9 +274,7 @@ describe('Proposals list item details', () => {
|
||||
networkParamsQueryMock,
|
||||
createUserVoteQueryMock(proposal?.id, VoteValue.VALUE_NO),
|
||||
]);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(await screen.findByText('You voted')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Against')).toBeInTheDocument();
|
||||
expect(await screen.findByText('You voted Against')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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(
|
||||
'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(
|
||||
'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');
|
||||
});
|
||||
|
||||
@ -359,7 +343,6 @@ describe('Proposals list item details', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Declined');
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
'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(
|
||||
'Majority not reached'
|
||||
);
|
||||
@ -395,7 +377,6 @@ describe('Proposals list item details', () => {
|
||||
ProposalRejectionReason.PROPOSAL_ERROR_INVALID_FUTURE_PRODUCT,
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Rejected');
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
'Invalid future product'
|
||||
);
|
||||
|
@ -1,11 +1,8 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVoteInformation } from '../../hooks';
|
||||
import { useUserVote } from '../vote-details/use-user-vote';
|
||||
import {
|
||||
StatusPass,
|
||||
StatusFail,
|
||||
} from '../current-proposal-status/current-proposal-status';
|
||||
import { StatusPass } from '../current-proposal-status/current-proposal-status';
|
||||
import { format, formatDistanceToNowStrict } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
||||
@ -22,7 +19,7 @@ const MajorityNotReached = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
{t('Majority')} <StatusFail>{t('not reached')}</StatusFail>
|
||||
{t('Majority')} {t('not reached')}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -30,7 +27,7 @@ const ParticipationNotReached = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
{t('Participation')} <StatusFail>{t('not reached')}</StatusFail>
|
||||
{t('Participation')} {t('not reached')}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -57,17 +54,11 @@ export const ProposalsListItemDetails = ({
|
||||
? t('byTokenVote')
|
||||
: t('byLPVote');
|
||||
|
||||
let proposalStatus: ReactNode;
|
||||
let voteDetails: ReactNode;
|
||||
let voteStatus: ReactNode;
|
||||
|
||||
switch (state) {
|
||||
case ProposalState.STATE_ENACTED: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
{t('voteState_Enacted')} <Icon name={'tick'} />
|
||||
</>
|
||||
);
|
||||
voteDetails = proposal?.terms.enactmentDatetime && (
|
||||
<>
|
||||
{format(
|
||||
@ -79,11 +70,6 @@ export const ProposalsListItemDetails = ({
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_PASSED: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
{t('voteState_Passed')} <Icon name={'tick'} />
|
||||
</>
|
||||
);
|
||||
voteDetails = proposal?.terms.change.__typename !== 'NewFreeform' && (
|
||||
<>
|
||||
{t('toEnactOn')}{' '}
|
||||
@ -97,11 +83,6 @@ export const ProposalsListItemDetails = ({
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
{t('voteState_WaitingForNodeVote')} <Icon name={'time'} />
|
||||
</>
|
||||
);
|
||||
voteDetails = proposal?.terms.change.__typename !== 'NewFreeform' && (
|
||||
<>
|
||||
{t('toEnactOn')}{' '}
|
||||
@ -115,19 +96,14 @@ export const ProposalsListItemDetails = ({
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_OPEN: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
{t('voteState_Open')} <Icon name={'hand'} />
|
||||
</>
|
||||
);
|
||||
voteDetails = (voteState === 'Yes' && (
|
||||
<>
|
||||
{t('youVoted')} <StatusPass>{t('voteState_Yes')}</StatusPass>
|
||||
{t('youVoted')} {t('voteState_Yes')}
|
||||
</>
|
||||
)) ||
|
||||
(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 />) ||
|
||||
(!majorityMet && <MajorityNotReached />) ||
|
||||
(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;
|
||||
}
|
||||
case ProposalState.STATE_DECLINED: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
{t('voteState_Declined')} <Icon name={'cross'} />
|
||||
</>
|
||||
);
|
||||
voteStatus =
|
||||
(!participationMet && <ParticipationNotReached />) ||
|
||||
(!majorityMet && <MajorityNotReached />);
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_REJECTED: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
<StatusFail>{t('voteState_Rejected')}</StatusFail>{' '}
|
||||
<Icon name={'warning-sign'} />
|
||||
</>
|
||||
);
|
||||
voteStatus = proposal?.rejectionReason && (
|
||||
<>{t(ProposalRejectionReasonMapping[proposal.rejectionReason])}</>
|
||||
);
|
||||
@ -190,16 +155,10 @@ export const ProposalsListItemDetails = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-[1fr_auto] mt-2 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>
|
||||
<div className="grid grid-cols-[1fr_auto] mt-4 items-start gap-2 text-sm">
|
||||
{voteDetails && (
|
||||
<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"
|
||||
>
|
||||
{voteDetails}
|
||||
@ -216,9 +175,7 @@ export const ProposalsListItemDetails = ({
|
||||
{proposal?.id && (
|
||||
<div className="col-start-2 row-start-2 justify-self-end">
|
||||
<Link to={`${Routes.PROPOSALS}/${proposal.id}`}>
|
||||
<Button data-testid="view-proposal-btn" size="sm">
|
||||
{t('View')}
|
||||
</Button>
|
||||
<Button data-testid="view-proposal-btn">{t('View')}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
@ -106,7 +106,7 @@ export const ProposalsList = ({
|
||||
{proposals.length > 0 && (
|
||||
<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')} />
|
||||
{sortedProposals.open.length > 0 ||
|
||||
sortedProtocolUpgradeProposals.open.length > 0 ? (
|
||||
|
@ -3,15 +3,15 @@ import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
Intent,
|
||||
Lozenge,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { stripFullStops } from '@vegaprotocol/utils';
|
||||
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import type { ReactNode } from 'react';
|
||||
import { ProposalInfoLabel } from '../proposal-info-label';
|
||||
import Routes from '../../../routes';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
|
||||
interface ProtocolProposalsListItemProps {
|
||||
@ -29,30 +29,30 @@ export const ProtocolUpgradeProposalsListItem = ({
|
||||
switch (proposal.status) {
|
||||
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED:
|
||||
proposalStatusIcon = (
|
||||
<div data-testid="protocol-upgrade-proposal-status-icon-rejected">
|
||||
<span data-testid="protocol-upgrade-proposal-status-icon-rejected">
|
||||
<Icon name={'cross'} />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING:
|
||||
proposalStatusIcon = (
|
||||
<div data-testid="protocol-upgrade-proposal-status-icon-pending">
|
||||
<span data-testid="protocol-upgrade-proposal-status-icon-pending">
|
||||
<Icon name={'time'} />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED:
|
||||
proposalStatusIcon = (
|
||||
<div data-testid="protocol-upgrade-proposal-status-icon-approved">
|
||||
<span data-testid="protocol-upgrade-proposal-status-icon-approved">
|
||||
<Icon name={'tick'} />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED:
|
||||
proposalStatusIcon = (
|
||||
<div data-testid="protocol-upgrade-proposal-status-icon-unspecified">
|
||||
<span data-testid="protocol-upgrade-proposal-status-icon-unspecified">
|
||||
<Icon name={'disable'} />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
}
|
||||
@ -71,18 +71,28 @@ export const ProtocolUpgradeProposalsListItem = ({
|
||||
</div>
|
||||
|
||||
<div className="text-sm">
|
||||
<div className="flex gap-2">
|
||||
<div
|
||||
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
|
||||
data-testid="protocol-upgrade-proposal-release-tag"
|
||||
className="mb-2"
|
||||
>
|
||||
<span className="pr-2">{t('vegaReleaseTag')}</span>
|
||||
<span>{t('vegaReleaseTag')}:</span>{' '}
|
||||
<Lozenge>{proposal.vegaReleaseTag}</Lozenge>
|
||||
</div>
|
||||
|
||||
@ -90,30 +100,18 @@ export const ProtocolUpgradeProposalsListItem = ({
|
||||
data-testid="protocol-upgrade-proposal-block-height"
|
||||
className="mb-2"
|
||||
>
|
||||
<span className="pr-2">{t('upgradeBlockHeight')}</span>
|
||||
<span>{t('upgradeBlockHeight')}:</span>{' '}
|
||||
<Lozenge>{proposal.upgradeBlockHeight}</Lozenge>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[1fr_auto] mt-3 items-start gap-2">
|
||||
<div className="col-start-1 row-start-1 text-white">
|
||||
<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">
|
||||
<div className="grid grid-cols-1 mt-3">
|
||||
<div className="justify-self-end">
|
||||
<Link
|
||||
to={`${Routes.PROPOSALS}/protocol-upgrade/${stripFullStops(
|
||||
proposal.vegaReleaseTag
|
||||
)}`}
|
||||
>
|
||||
<Button data-testid="view-proposal-btn" size="sm">
|
||||
{t('View')}
|
||||
</Button>
|
||||
<Button data-testid="view-proposal-btn">{t('View')}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -107,10 +107,6 @@ export const VoteButtons = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (currentStakeAvailable.isLessThanOrEqualTo(0)) {
|
||||
return t('noGovernanceTokens');
|
||||
}
|
||||
|
||||
if (minVoterBalance && spamProtectionMinTokens) {
|
||||
const formattedMinVoterBalance = new BigNumber(
|
||||
addDecimal(minVoterBalance, 18)
|
||||
@ -163,24 +159,30 @@ export const VoteButtons = ({
|
||||
return (
|
||||
<>
|
||||
{changeVote || (voteState === VoteState.NotCast && proposalVotable) ? (
|
||||
<>
|
||||
{currentStakeAvailable.isLessThanOrEqualTo(0) && (
|
||||
<p data-testid="no-stake-available">{t('noGovernanceTokens')}</p>
|
||||
)}
|
||||
|
||||
<div className="flex gap-4" data-testid="vote-buttons">
|
||||
<div className="flex-1">
|
||||
<Button
|
||||
data-testid="vote-for"
|
||||
onClick={() => submitVote(VoteValue.VALUE_YES)}
|
||||
variant="primary"
|
||||
disabled={currentStakeAvailable.isLessThanOrEqualTo(0)}
|
||||
>
|
||||
{t('voteFor')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Button
|
||||
data-testid="vote-against"
|
||||
onClick={() => submitVote(VoteValue.VALUE_NO)}
|
||||
variant="primary"
|
||||
disabled={currentStakeAvailable.isLessThanOrEqualTo(0)}
|
||||
>
|
||||
{t('voteAgainst')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
(voteState === VoteState.Yes || voteState === VoteState.No) && (
|
||||
<p data-testid="you-voted">
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { RoundedWrapper, Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { useVoteSubmit, VoteProgress } from '@vegaprotocol/proposals';
|
||||
@ -199,10 +200,11 @@ export const VoteDetails = ({
|
||||
{proposalType === ProposalType.PROPOSAL_UPDATE_MARKET && (
|
||||
<p>{t('votingThresholdInfo')}</p>
|
||||
)}
|
||||
{pubKey ? (
|
||||
|
||||
<section className="mt-10">
|
||||
<SubHeading title={t('yourVote')} />
|
||||
{proposal && (
|
||||
<SubHeading title={t('castYourVote')} />
|
||||
{pubKey ? (
|
||||
proposal && (
|
||||
<VoteButtonsContainer
|
||||
voteState={voteState}
|
||||
voteDatetime={voteDatetime}
|
||||
@ -214,11 +216,19 @@ export const VoteDetails = ({
|
||||
submit={submit}
|
||||
dialog={Dialog}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
<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>
|
||||
) : (
|
||||
<ConnectToVega />
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
@ -13,7 +13,7 @@ export const getIntentBorder = (intent = Intent.None) => {
|
||||
'border-warning': intent === Intent.Warning,
|
||||
'border-neutral-500': intent === Intent.None,
|
||||
'border-vega-blue-300': intent === Intent.Primary,
|
||||
'border-success': intent === Intent.Success,
|
||||
'border-vega-green': intent === Intent.Success,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import once from 'lodash/once';
|
||||
import { format } from 'date-fns';
|
||||
import { getUserLocale } from '../get-user-locale';
|
||||
import { utcToZonedTime, format as tzFormat } from 'date-fns-tz';
|
||||
|
||||
export const isValidDate = (date: Date) =>
|
||||
date instanceof Date && !isNaN(date.getTime());
|
||||
@ -53,15 +53,24 @@ export const formatForInput = (date: Date) => {
|
||||
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 */
|
||||
export const formatDateWithLocalTimezone = (
|
||||
date: Date,
|
||||
formatStr = 'dd MMMM yyyy HH:mm (z)'
|
||||
formatStr = 'dd MMMM yyyy HH:mm'
|
||||
) => {
|
||||
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const localDatetime = utcToZonedTime(date, userTimeZone);
|
||||
|
||||
return tzFormat(localDatetime, formatStr, {
|
||||
timeZone: userTimeZone,
|
||||
});
|
||||
const formattedDate = format(date, formatStr);
|
||||
const timeZoneAbbreviation = getTimeZoneAbbreviation(date);
|
||||
return `${formattedDate} (${timeZoneAbbreviation})`;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user