From e01fb7f9ae7e01cf73c0a9dea285681d650cd310 Mon Sep 17 00:00:00 2001 From: Sam Keen Date: Tue, 25 Jul 2023 11:23:25 +0100 Subject: [PATCH] feat(governance): proposal list tile and summary enhancements (#4326) --- .../src/integration/view/home.cy.ts | 2 +- .../src/integration/view/proposal.cy.ts | 2 +- .../governance/src/i18n/translations/dev.json | 18 +- apps/governance/src/routes/home/index.tsx | 84 +++-- .../current-proposal-state.tsx | 42 +-- .../proposal-header.spec.tsx | 80 +++-- .../proposal-header.tsx | 56 ++- .../proposal-info-label.tsx | 8 +- .../proposal-voting-status/index.tsx | 1 + .../proposal-voting-status.spec.tsx | 153 ++++++++ .../proposal-voting-status.tsx | 174 ++++++++++ .../components/proposal/proposal.spec.tsx | 2 + .../components/proposal/proposal.tsx | 164 +++++---- .../proposals-list-item-details.spec.tsx | 138 +------- .../proposals-list-item-details.tsx | 165 ++++----- .../proposals-list-item.tsx | 11 +- .../proposals-list/proposals-list.spec.tsx | 66 ++-- .../proposals-list/proposals-list.tsx | 326 ++++++++++-------- .../rejected-proposals-list.spec.tsx | 34 +- .../rejected-proposals-list.tsx | 32 +- ...tocol-upgrade-proposals-list-item.spec.tsx | 37 -- .../protocol-upgrade-proposals-list-item.tsx | 101 ++---- .../components/vote-details/vote-buttons.tsx | 4 +- .../components/vote-details/vote-details.tsx | 4 +- .../proposals/proposal/proposal-container.tsx | 32 +- .../protocol-upgrade-proposal.spec.tsx | 5 + .../routes/proposals/test-helpers/mocks.ts | 45 +++ .../src/use-network-params.ts | 64 ++-- .../icon/vega-icons/svg-icons/icon-vote.tsx | 12 + .../icon/vega-icons/vega-icon-record.ts | 5 +- 30 files changed, 1112 insertions(+), 755 deletions(-) create mode 100644 apps/governance/src/routes/proposals/components/proposal-voting-status/index.tsx create mode 100644 apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.spec.tsx create mode 100644 apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.tsx create mode 100644 libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-vote.tsx diff --git a/apps/governance-e2e/src/integration/view/home.cy.ts b/apps/governance-e2e/src/integration/view/home.cy.ts index 6e2df34fd..a84b15846 100644 --- a/apps/governance-e2e/src/integration/view/home.cy.ts +++ b/apps/governance-e2e/src/integration/view/home.cy.ts @@ -11,7 +11,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () { cy.getByTestId('home-proposals').within(() => { cy.get('[href="/proposals"]') .should('exist') - .and('have.text', 'Browse, vote, and propose'); + .and('have.text', 'See all proposals'); }); }); diff --git a/apps/governance-e2e/src/integration/view/proposal.cy.ts b/apps/governance-e2e/src/integration/view/proposal.cy.ts index 0c886c4c6..048245a2b 100644 --- a/apps/governance-e2e/src/integration/view/proposal.cy.ts +++ b/apps/governance-e2e/src/integration/view/proposal.cy.ts @@ -161,7 +161,7 @@ context( ); cy.getByTestId('protocol-upgrade-proposal-status').should( 'have.text', - 'Approved by validators ' + 'Approved by validators' ); }); }); diff --git a/apps/governance/src/i18n/translations/dev.json b/apps/governance/src/i18n/translations/dev.json index 4494b39e9..6616f0c6e 100644 --- a/apps/governance/src/i18n/translations/dev.json +++ b/apps/governance/src/i18n/translations/dev.json @@ -166,6 +166,7 @@ "proposedEnactment": "Proposed enactment", "Enacted": "Enacted", "enactedOn": "Enacted on", + "enactedOn{{date}}": "Enacted on {{enactmentDate}}", "status": "Status", "state": "State", "shouldPass": "Should pass", @@ -185,6 +186,7 @@ "proposedOn": "Proposed on", "proposedBy": "Proposed by", "toEnactOn": "Enacts on", + "enactsOn{{date}}": "Enacts on {{enactmentDate}}", "closesOn": "Closes on", "closedOn": "Closed on", "errorDetails": "Error details", @@ -217,6 +219,7 @@ "votingThresholdInfo": "If the token vote passes the participation threshold it will be the deciding vote. If not, the outcome will be determined by liquidity providers on this market.", "noGovernanceTokens": "You need some VEGA tokens to participate in governance", "youVoted": "You voted", + "voted": "Voted", "changeVote": "Change vote", "txRequested": "Confirm transaction in wallet", "votePending": "Casting vote", @@ -628,7 +631,6 @@ "pendingDescriptionLinkText": "set up and run a node on Vega", "pendingDescription2": ". A node can move from being a candidate into standby based on how much nomination it attracts, assuming it has proven reliability by sending heartbeats to the network.", "n/a": "N/A", - "Set to": "Set to", "pass": "pass", "fail": "fail", "New asset": "New asset", @@ -788,7 +790,7 @@ "overstakedPenalty": "Overstaked penalty", "multisigPenalty": "Multisig penalty", "homeProposalsIntro": "Decisions on the Vega network are on-chain, with tokenholders creating proposals that other tokenholders vote to approve or reject. Network upgrades are proposed and approved by validators.", - "homeProposalsButtonText": "Browse, vote, and propose", + "homeProposalsButtonText": "See all proposals", "homeValidatorsIntro": "Vega runs on a delegated proof of stake blockchain, where validators earn fees for validating block transactions. Tokenholders can nominate validators by staking tokens to them.", "homeValidatorsButtonText": "Browse, and stake", "homeRewardsIntro": "Track rewards you've earned for trading, liquidity provision, market creation, and staking.", @@ -838,6 +840,18 @@ "networkGovernance": "Network governance", "networkUpgrades": "Network upgrades", "assetSpecification": "Asset specification", + "viewDetails": "View details", + "vegaGovernance": "Vega Governance", + "latestProposals": "Latest proposals", + "vegaToken": "VEGA Token", + "majorityVotedForProposal": "majority voted for this proposal", + "majorityNotVotedForProposal": "majority not voted for this proposal", + "requiredMajorityVotedForProposal": "Required majority voted for this proposal", + "requiredMajorityNotVotedForProposal": "Required majority not voted for this proposal", + "minParticipationReached": "Min. participation reached", + "minParticipationNotReached": "Min. participation not reached", + "consensusNodes": "consensus nodes", + "activeNodes": "active nodes", "Estimated time to upgrade": "Estimated time to upgrade", "Upgraded at": "Upgraded at" } diff --git a/apps/governance/src/routes/home/index.tsx b/apps/governance/src/routes/home/index.tsx index dfc6dfa56..064bdabb2 100644 --- a/apps/governance/src/routes/home/index.tsx +++ b/apps/governance/src/routes/home/index.tsx @@ -20,7 +20,7 @@ import { getNotRejectedProposals, getNotRejectedProtocolUpgradeProposals, } from '../proposals/proposals/proposals-container'; -import { Heading } from '../../components/heading'; +import { Heading, SubHeading } from '../../components/heading'; import * as Schema from '@vegaprotocol/types'; import type { RouteChildProps } from '..'; import type { ProposalFieldsFragment } from '../proposals/proposals/__generated__/Proposals'; @@ -31,6 +31,10 @@ import { orderByDate, orderByUpgradeBlockHeight, } from '../proposals/components/proposals-list/proposals-list'; +import { + NetworkParams, + useNetworkParams, +} from '@vegaprotocol/network-parameters'; import { BigNumber } from '../../lib/bignumber'; const nodesToShow = 6; @@ -43,33 +47,57 @@ const HomeProposals = ({ protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[]; }) => { const { t } = useTranslation(); + const { + params: networkParams, + loading: networkParamsLoading, + error: networkParamsError, + } = useNetworkParams([ + NetworkParams.governance_proposal_market_requiredMajority, + NetworkParams.governance_proposal_updateMarket_requiredMajority, + NetworkParams.governance_proposal_updateMarket_requiredMajorityLP, + NetworkParams.governance_proposal_asset_requiredMajority, + NetworkParams.governance_proposal_updateAsset_requiredMajority, + NetworkParams.governance_proposal_updateNetParam_requiredMajority, + NetworkParams.governance_proposal_freeform_requiredMajority, + ]); return ( -
- -

{t('homeProposalsIntro')}

-
- - - + +
+ +

{t('homeProposalsIntro')}

+
+ + {t(`readMoreGovernance`)} + +
- - {t(`readMoreGovernance`)} - -
-
    - {protocolUpgradeProposals.map((proposal, index) => ( - - ))} + +
      + {protocolUpgradeProposals.map((proposal, index) => ( + + ))} - {proposals.map((proposal) => ( - - ))} -
    -
+ {proposals.map((proposal) => ( + + ))} + + +
+ + + +
+ + ); }; @@ -87,8 +115,8 @@ const HomeNodes = ({ const { t } = useTranslation(); const highlightedNodeData = [ - { title: t('active nodes'), length: activeNodes.length }, - { title: t('consensus nodes'), length: consensusNodes.length }, + { title: t('activeNodes'), length: activeNodes.length }, + { title: t('consensusNodes'), length: consensusNodes.length }, ]; return ( @@ -234,7 +262,7 @@ const GovernanceHome = ({ name }: RouteChildProps) => { [protocolUpgradeProposals] ); - const totalProposalsDesired = 4; + const totalProposalsDesired = 3; const protocolUpgradeProposalsToShow = sortedProtocolUpgradeProposals.slice( 0, totalProposalsDesired @@ -290,7 +318,7 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
- +

{t('homeVegaTokenIntro')}

diff --git a/apps/governance/src/routes/proposals/components/current-proposal-state/current-proposal-state.tsx b/apps/governance/src/routes/proposals/components/current-proposal-state/current-proposal-state.tsx index 47895762f..8e2587076 100644 --- a/apps/governance/src/routes/proposals/components/current-proposal-state/current-proposal-state.tsx +++ b/apps/governance/src/routes/proposals/components/current-proposal-state/current-proposal-state.tsx @@ -1,5 +1,4 @@ import { useTranslation } from 'react-i18next'; -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'; @@ -18,53 +17,32 @@ export const CurrentProposalState = ({ switch (proposal?.state) { case ProposalState.STATE_ENACTED: { - proposalStatus = ( - <> - {t('voteState_Enacted')} - - - ); + proposalStatus = t('voteState_Enacted'); break; } case ProposalState.STATE_PASSED: { - proposalStatus = ( - <> - {t('voteState_Passed')} - - - ); + proposalStatus = t('voteState_Passed'); break; } case ProposalState.STATE_WAITING_FOR_NODE_VOTE: { - proposalStatus = ( - <> - {t('voteState_WaitingForNodeVote')} - - - ); + proposalStatus = t('voteState_WaitingForNodeVote'); break; } case ProposalState.STATE_OPEN: { variant = 'primary' as ProposalInfoLabelVariant; - proposalStatus = <>{t('voteState_Open')}; + proposalStatus = t('voteState_Open'); break; } case ProposalState.STATE_DECLINED: { - proposalStatus = ( - <> - {t('voteState_Declined')} - - - ); + proposalStatus = t('voteState_Declined'); break; } case ProposalState.STATE_REJECTED: { - proposalStatus = ( - <> - {t('voteState_Rejected')} - - - ); + proposalStatus = t('voteState_Rejected'); + break; + } + case ProposalState.STATE_FAILED: { + proposalStatus = t('voteState_Failed'); break; } } diff --git a/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.spec.tsx b/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.spec.tsx index fa65d4748..855bccad8 100644 --- a/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.spec.tsx +++ b/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.spec.tsx @@ -1,18 +1,46 @@ import { render, screen } from '@testing-library/react'; +import { MockedProvider } from '@apollo/client/testing'; +import { + ProposalRejectionReason, + ProposalState, + VoteValue, +} from '@vegaprotocol/types'; +import { VegaWalletContext } from '@vegaprotocol/wallet'; +import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { generateNoVotes, generateProposal, generateYesVotes, } from '../../test-helpers/generate-proposals'; import { ProposalHeader } from './proposal-header'; +import { + lastWeek, + nextWeek, + mockNetworkParams, + mockWalletContext, + createUserVoteQueryMock, +} from '../../test-helpers/mocks'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; -import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types'; -import { lastWeek, nextWeek } from '../../test-helpers/mocks'; +import type { MockedResponse } from '@apollo/client/testing'; const renderComponent = ( proposal: ProposalQuery['proposal'], - isListItem = true -) => render(); + isListItem = true, + mocks: MockedResponse[] = [] +) => + render( + + + + + + + + ); describe('Proposal header', () => { it('Renders New market proposal', () => { @@ -317,22 +345,6 @@ describe('Proposal header', () => { 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({ @@ -346,4 +358,32 @@ describe('Proposal header', () => { ); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Rejected'); }); + + it('Renders proposal state: Open - user voted against', async () => { + const proposal = generateProposal({ + state: ProposalState.STATE_OPEN, + terms: { + closingDatetime: nextWeek.toString(), + }, + }); + renderComponent(proposal, true, [ + // @ts-ignore generateProposal always creates an id + createUserVoteQueryMock(proposal.id, VoteValue.VALUE_NO), + ]); + expect(await screen.findByTestId('user-voted-no')).toBeInTheDocument(); + }); + + it('Renders proposal state: Open - user voted for', async () => { + const proposal = generateProposal({ + state: ProposalState.STATE_OPEN, + terms: { + closingDatetime: nextWeek.toString(), + }, + }); + renderComponent(proposal, true, [ + // @ts-ignore generateProposal always creates an id + createUserVoteQueryMock(proposal.id, VoteValue.VALUE_YES), + ]); + expect(await screen.findByTestId('user-voted-yes')).toBeInTheDocument(); + }); }); diff --git a/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.tsx b/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.tsx index 854444e8c..d4cd74214 100644 --- a/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.tsx +++ b/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { Lozenge } from '@vegaprotocol/ui-toolkit'; +import { Lozenge, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; import { shorten } from '@vegaprotocol/utils'; import { Heading, SubHeading } from '../../../../components/heading'; import type { ReactNode } from 'react'; @@ -8,15 +8,21 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import { truncateMiddle } from '../../../../lib/truncate-middle'; import { CurrentProposalState } from '../current-proposal-state'; import { ProposalInfoLabel } from '../proposal-info-label'; +import { useUserVote } from '../vote-details/use-user-vote'; +import { ProposalVotingStatus } from '../proposal-voting-status'; +import type { NetworkParamsResult } from '@vegaprotocol/network-parameters'; export const ProposalHeader = ({ proposal, + networkParams, isListItem = true, }: { proposal: ProposalFieldsFragment | ProposalQuery['proposal']; + networkParams: Partial; isListItem?: boolean; }) => { const { t } = useTranslation(); + const { voteState } = useUserVote(proposal?.id); const change = proposal?.terms.change; let details: ReactNode; @@ -119,6 +125,35 @@ export const ProposalHeader = ({ return ( <> +
+
+ + {t(`${proposalType}`)} + +
+ +
+ {(voteState === 'Yes' || voteState === 'No') && ( +
+
+ +
+
+ {t('voted')}{' '} + {t(`voteState_${voteState}`)} +
+
+ )} + +
+ +
+
+
+
{isListItem ? (
@@ -133,23 +168,16 @@ export const ProposalHeader = ({ )}
-
-
- - {t(`${proposalType}`)} - -
- -
- -
-
- {details && ( -
+
{details}
)} + + ); }; diff --git a/apps/governance/src/routes/proposals/components/proposal-info-label/proposal-info-label.tsx b/apps/governance/src/routes/proposals/components/proposal-info-label/proposal-info-label.tsx index ec5f774ee..5ee6c5531 100644 --- a/apps/governance/src/routes/proposals/components/proposal-info-label/proposal-info-label.tsx +++ b/apps/governance/src/routes/proposals/components/proposal-info-label/proposal-info-label.tsx @@ -7,10 +7,10 @@ export type ProposalInfoLabelVariant = | '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 base = 'rounded-full px-3 py-1 font-alpha'; +const primary = 'bg-vega-green text-black'; +const secondary = 'bg-vega-dark-200 text-vega-light-200'; +const tertiary = 'bg-vega-dark-150 text-vega-light-200'; const highlight = 'bg-vega-yellow text-black'; const getClassname = (variant: ProposalInfoLabelVariant) => { diff --git a/apps/governance/src/routes/proposals/components/proposal-voting-status/index.tsx b/apps/governance/src/routes/proposals/components/proposal-voting-status/index.tsx new file mode 100644 index 000000000..bb9ad609b --- /dev/null +++ b/apps/governance/src/routes/proposals/components/proposal-voting-status/index.tsx @@ -0,0 +1 @@ +export * from './proposal-voting-status'; diff --git a/apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.spec.tsx b/apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.spec.tsx new file mode 100644 index 000000000..0fee11edc --- /dev/null +++ b/apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.spec.tsx @@ -0,0 +1,153 @@ +import { render, screen } from '@testing-library/react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { MockedProvider } from '@apollo/client/testing'; +import { VegaWalletContext } from '@vegaprotocol/wallet'; +import { + lastWeek, + mockWalletContext, + networkParamsQueryMock, + nextWeek, + mockNetworkParams, +} from '../../test-helpers/mocks'; +import { ProposalVotingStatus } from './proposal-voting-status'; +import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; +import type { MockedResponse } from '@apollo/client/testing'; +import { + generateNoVotes, + generateProposal, + generateYesVotes, +} from '../../test-helpers/generate-proposals'; +import { ProposalState } from '@vegaprotocol/types'; +import { BigNumber } from '../../../../lib/bignumber'; +import type { AppState } from '../../../../contexts/app-state/app-state-context'; + +const mockTotalSupply = new BigNumber(100); +// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :) +const fixedTokenValue = 1000000000000000000; + +const mockAppState: AppState = { + totalAssociated: new BigNumber('50063005'), + decimals: 18, + totalSupply: mockTotalSupply, + vegaWalletManageOverlay: false, + transactionOverlay: false, + bannerMessage: '', + disconnectNotice: false, +}; + +jest.mock('../../../../contexts/app-state/app-state-context', () => ({ + useAppState: () => ({ + appState: mockAppState, + }), +})); + +const renderComponent = ( + proposal: ProposalQuery['proposal'], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mocks: MockedResponse[] = [networkParamsQueryMock] +) => + render( + + + + + + + + ); + +describe('ProposalVotingStatus', () => { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(0); + }); + afterAll(() => { + jest.useRealTimers(); + }); + + it('Renders majority reached', () => { + const yesVotes = 100; + const noVotes = 0; + + renderComponent( + generateProposal({ + state: ProposalState.STATE_PASSED, + terms: { + closingDatetime: lastWeek.toString(), + enactmentDatetime: nextWeek.toString(), + }, + votes: { + __typename: 'ProposalVotes', + yes: generateYesVotes(yesVotes, fixedTokenValue), + no: generateNoVotes(noVotes, fixedTokenValue), + }, + }) + ); + expect(screen.getByTestId('majority-reached')).toBeInTheDocument(); + }); + + it('Renders majority not reached', () => { + const yesVotes = 20; + const noVotes = 80; + + renderComponent( + generateProposal({ + state: ProposalState.STATE_PASSED, + terms: { + closingDatetime: lastWeek.toString(), + enactmentDatetime: nextWeek.toString(), + }, + votes: { + __typename: 'ProposalVotes', + yes: generateYesVotes(yesVotes, fixedTokenValue), + no: generateNoVotes(noVotes, fixedTokenValue), + }, + }) + ); + expect(screen.getByTestId('majority-not-reached')).toBeInTheDocument(); + }); + + it('Renders participation reached', () => { + const yesVotes = 1000; + const noVotes = 0; + + renderComponent( + generateProposal({ + state: ProposalState.STATE_PASSED, + terms: { + closingDatetime: lastWeek.toString(), + enactmentDatetime: nextWeek.toString(), + }, + votes: { + __typename: 'ProposalVotes', + yes: generateYesVotes(yesVotes, fixedTokenValue), + no: generateNoVotes(noVotes, fixedTokenValue), + }, + }) + ); + expect(screen.getByTestId('participation-reached')).toBeInTheDocument(); + }); + + it('Renders participation not reached', () => { + const yesVotes = 0; + const noVotes = 0; + + renderComponent( + generateProposal({ + terms: { + closingDatetime: lastWeek.toString(), + enactmentDatetime: nextWeek.toString(), + }, + votes: { + __typename: 'ProposalVotes', + yes: generateYesVotes(yesVotes, fixedTokenValue), + no: generateNoVotes(noVotes, fixedTokenValue), + }, + }) + ); + expect(screen.getByTestId('participation-not-reached')).toBeInTheDocument(); + }); +}); diff --git a/apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.tsx b/apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.tsx new file mode 100644 index 000000000..a00600a9d --- /dev/null +++ b/apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.tsx @@ -0,0 +1,174 @@ +import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; +import { Icon } from '@vegaprotocol/ui-toolkit'; +import { useVoteInformation } from '../../hooks'; +import { BigNumber } from '../../../../lib/bignumber'; +import type { NetworkParamsResult } from '@vegaprotocol/network-parameters'; +import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; +import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; + +const statusClasses = (reached: boolean) => + classNames('flex items-center gap-2 px-4 py-2 rounded-md', { + 'bg-vega-green-700': reached, + 'bg-vega-red-700': !reached, + }); + +const MajorityStatus = ({ + reached, + requiredMajority, +}: { + reached: boolean; + requiredMajority: string | null | undefined; +}) => { + const { t } = useTranslation(); + + return ( +
+ {reached ? : } + {reached ? ( +
+ {requiredMajority ? ( + <> + {new BigNumber(requiredMajority).times(100).toString()}%{' '} + {t('majorityVotedForProposal')} + + ) : ( + t('requiredMajorityVotedForProposal') + )} +
+ ) : ( +
+ {requiredMajority ? ( + <> + {new BigNumber(requiredMajority).times(100).toString()}%{' '} + {t('majorityNotVotedForProposal')} + + ) : ( + t('requiredMajorityNotVotedForProposal') + )} +
+ )} +
+ ); +}; + +const ParticipationStatus = ({ reached }: { reached: boolean }) => { + const { t } = useTranslation(); + + return ( +
+ {reached ? ( + <> + +
+ {t('minParticipationReached')} +
+ + ) : ( + <> + +
+ {t('minParticipationNotReached')} +
+ + )} +
+ ); +}; + +export const ProposalVotingStatus = ({ + proposal, + networkParams, +}: { + proposal: ProposalFieldsFragment | ProposalQuery['proposal']; + networkParams: Partial; +}) => { + const { t } = useTranslation(); + const { majorityMet, majorityLPMet, participationMet, participationLPMet } = + useVoteInformation({ + proposal, + }); + + if (!proposal) { + return null; + } + + const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket'; + + let requiredVotingMajority = null; + let requiredVotingMajorityLP = null; + + if (networkParams) { + switch (proposal.terms.change.__typename) { + case 'NewMarket': + requiredVotingMajority = + networkParams.governance_proposal_market_requiredMajority; + break; + case 'UpdateMarket': + requiredVotingMajority = + networkParams.governance_proposal_updateMarket_requiredMajority; + requiredVotingMajorityLP = + networkParams.governance_proposal_updateMarket_requiredMajorityLP; + break; + case 'NewAsset': + requiredVotingMajority = + networkParams.governance_proposal_asset_requiredMajority; + break; + case 'UpdateAsset': + requiredVotingMajority = + networkParams.governance_proposal_updateAsset_requiredMajority; + break; + case 'UpdateNetworkParameter': + requiredVotingMajority = + networkParams.governance_proposal_updateNetParam_requiredMajority; + break; + case 'NewFreeform': + requiredVotingMajority = + networkParams.governance_proposal_freeform_requiredMajority; + break; + } + } + + if (isUpdateMarket) { + return ( +
+

{t('Token vote')}

+
+ {' '} + +
+ +

{t('Liquidity provider vote')}

+
+ {' '} + +
+
+ ); + } + + return ( +
+ {' '} + +
+ ); +}; diff --git a/apps/governance/src/routes/proposals/components/proposal/proposal.spec.tsx b/apps/governance/src/routes/proposals/components/proposal/proposal.spec.tsx index 30414b2c0..c334ee137 100644 --- a/apps/governance/src/routes/proposals/components/proposal/proposal.spec.tsx +++ b/apps/governance/src/routes/proposals/components/proposal/proposal.spec.tsx @@ -4,6 +4,7 @@ import { generateProposal } from '../../test-helpers/generate-proposals'; import { Proposal } from './proposal'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import { ProposalState } from '@vegaprotocol/types'; +import { mockNetworkParams } from '../../test-helpers/mocks'; jest.mock('@vegaprotocol/network-parameters', () => ({ ...jest.requireActual('@vegaprotocol/network-parameters'), @@ -46,6 +47,7 @@ const renderComponent = (proposal: ProposalQuery['proposal']) => { ); diff --git a/apps/governance/src/routes/proposals/components/proposal/proposal.tsx b/apps/governance/src/routes/proposals/components/proposal/proposal.tsx index 7e12da4f0..c87c9a89d 100644 --- a/apps/governance/src/routes/proposals/components/proposal/proposal.tsx +++ b/apps/governance/src/routes/proposals/components/proposal/proposal.tsx @@ -1,10 +1,6 @@ import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import { - NetworkParams, - useNetworkParams, -} from '@vegaprotocol/network-parameters'; -import { AsyncRenderer, Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit'; +import { Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit'; import { ProposalHeader } from '../proposal-detail-header/proposal-header'; import { ProposalDescription } from '../proposal-description'; import { ProposalChangeTable } from '../proposal-change-table'; @@ -21,6 +17,7 @@ import type { MarketInfoWithData } from '@vegaprotocol/markets'; import type { AssetQuery } from '@vegaprotocol/assets'; import { removePaginationWrapper } from '@vegaprotocol/utils'; import { ProposalState } from '@vegaprotocol/types'; +import type { NetworkParamsResult } from '@vegaprotocol/network-parameters'; export enum ProposalType { PROPOSAL_NEW_MARKET = 'PROPOSAL_NEW_MARKET', @@ -32,6 +29,7 @@ export enum ProposalType { } export interface ProposalProps { proposal: ProposalFieldsFragment | ProposalQuery['proposal']; + networkParams: Partial; newMarketData?: MarketInfoWithData | null; assetData?: AssetQuery | null; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -40,20 +38,12 @@ export interface ProposalProps { export const Proposal = ({ proposal, + networkParams, restData, newMarketData, assetData, }: ProposalProps) => { const { t } = useTranslation(); - const { params, loading, error } = useNetworkParams([ - NetworkParams.governance_proposal_market_minVoterBalance, - NetworkParams.governance_proposal_updateMarket_minVoterBalance, - NetworkParams.governance_proposal_asset_minVoterBalance, - NetworkParams.governance_proposal_updateAsset_minVoterBalance, - NetworkParams.governance_proposal_updateNetParam_minVoterBalance, - NetworkParams.governance_proposal_freeform_minVoterBalance, - NetworkParams.spam_protection_voting_min_tokens, - ]); if (!proposal) { return null; @@ -79,122 +69,122 @@ export const Proposal = ({ let minVoterBalance = null; let proposalType = null; - if (params) { + if (networkParams) { switch (proposal.terms.change.__typename) { case 'NewMarket': - minVoterBalance = params.governance_proposal_market_minVoterBalance; + minVoterBalance = + networkParams.governance_proposal_market_minVoterBalance; proposalType = ProposalType.PROPOSAL_NEW_MARKET; break; case 'UpdateMarket': minVoterBalance = - params.governance_proposal_updateMarket_minVoterBalance; + networkParams.governance_proposal_updateMarket_minVoterBalance; proposalType = ProposalType.PROPOSAL_UPDATE_MARKET; break; case 'NewAsset': - minVoterBalance = params.governance_proposal_asset_minVoterBalance; + minVoterBalance = + networkParams.governance_proposal_asset_minVoterBalance; proposalType = ProposalType.PROPOSAL_NEW_ASSET; break; case 'UpdateAsset': minVoterBalance = - params.governance_proposal_updateAsset_minVoterBalance; + networkParams.governance_proposal_updateAsset_minVoterBalance; proposalType = ProposalType.PROPOSAL_UPDATE_ASSET; break; case 'UpdateNetworkParameter': minVoterBalance = - params.governance_proposal_updateNetParam_minVoterBalance; + networkParams.governance_proposal_updateNetParam_minVoterBalance; proposalType = ProposalType.PROPOSAL_NETWORK_PARAMETER; break; case 'NewFreeform': - minVoterBalance = params.governance_proposal_freeform_minVoterBalance; + minVoterBalance = + networkParams.governance_proposal_freeform_minVoterBalance; proposalType = ProposalType.PROPOSAL_FREEFORM; break; } } return ( - -
-
- +
+
+ - {proposal.state === ProposalState.STATE_REJECTED ? ( -
- - {t('RejectedProposals')} - -
- ) : ( -
- - {t('AllProposals')} - -
- )} + {proposal.state === ProposalState.STATE_REJECTED ? ( +
+ + {t('RejectedProposals')} + +
+ ) : ( +
+ + {t('AllProposals')} + +
+ )} +
+ + +
+
+
- -
-
- -
+ {proposal.terms.change.__typename === 'NewAsset' && + proposal.terms.change.source.__typename === 'ERC20' && + proposal.id ? ( + + ) : null} - {proposal.terms.change.__typename === 'NewAsset' && - proposal.terms.change.source.__typename === 'ERC20' && - proposal.id ? ( - - ) : null} +
+ +
+ {newMarketData && (
- +
+ )} - {newMarketData && ( + {(proposal.terms.change.__typename === 'NewAsset' || + proposal.terms.change.__typename === 'UpdateAsset') && + asset && (
- +
)} - {(proposal.terms.change.__typename === 'NewAsset' || - proposal.terms.change.__typename === 'UpdateAsset') && - asset && ( -
- -
- )} - -
- -
+
+
+
-
-
- - - -
- -
- +
+ + -
+
-
- + +
+ +
+
+
); }; diff --git a/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item-details.spec.tsx b/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item-details.spec.tsx index f45b7d784..6092f66e5 100644 --- a/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item-details.spec.tsx +++ b/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item-details.spec.tsx @@ -5,11 +5,7 @@ import type { MockedResponse } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing'; import { render, screen } from '@testing-library/react'; import { format } from 'date-fns'; -import { - ProposalRejectionReason, - ProposalState, - VoteValue, -} from '@vegaprotocol/types'; +import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types'; import { generateNoVotes, generateProposal, @@ -18,7 +14,6 @@ import { import { ProposalsListItemDetails } from './proposals-list-item-details'; import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats'; import { - mockPubkey, mockWalletContext, networkParamsQueryMock, fiveMinutes, @@ -28,39 +23,6 @@ import { nextWeek, } from '../../test-helpers/mocks'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; -import { UserVoteDocument } from '../vote-details/__generated__/Vote'; -import faker from 'faker'; - -const createUserVoteQueryMock = ( - proposalId: string | undefined | null, - value: VoteValue -) => ({ - request: { - query: UserVoteDocument, - variables: { - partyId: mockPubkey.publicKey, - }, - }, - result: { - data: { - party: { - votesConnection: { - edges: [ - { - node: { - proposalId, - vote: { - value, - datetime: faker.date.past().toISOString(), - }, - }, - }, - ], - }, - }, - }, - }, -}); const renderComponent = ( proposal: ProposalQuery['proposal'], @@ -131,7 +93,7 @@ describe('Proposals list item details', () => { ); }); - it('Renders proposal state: Update market proposal - set to pass by LP vote', () => { + it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', () => { renderComponent( generateProposal({ state: ProposalState.STATE_OPEN, @@ -153,11 +115,11 @@ describe('Proposals list item details', () => { }) ); expect(screen.getByTestId('vote-status')).toHaveTextContent( - 'Set to pass by LP vote' + 'Currently expected to pass by LP vote' ); }); - it('Renders proposal state: Update market proposal - set to pass by token vote', () => { + it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', () => { renderComponent( generateProposal({ state: ProposalState.STATE_OPEN, @@ -179,11 +141,11 @@ describe('Proposals list item details', () => { }) ); expect(screen.getByTestId('vote-status')).toHaveTextContent( - 'Set to pass by token vote' + 'Currently expected to pass by token vote' ); }); - it('Renders proposal state: Update market proposal - set to fail', () => { + it('Renders proposal state: Update market proposal - Currently expected to fail', () => { renderComponent( generateProposal({ state: ProposalState.STATE_OPEN, @@ -204,7 +166,9 @@ describe('Proposals list item details', () => { }, }) ); - expect(screen.getByTestId('vote-status')).toHaveTextContent('Set to fail'); + expect(screen.getByTestId('vote-status')).toHaveTextContent( + 'Currently expected to fail' + ); }); it('Renders proposal state: Open - 5 minutes left to vote', () => { @@ -249,52 +213,6 @@ describe('Proposals list item details', () => { ); }); - it('Renders proposal state: Open - user voted for', async () => { - const proposal = generateProposal({ - state: ProposalState.STATE_OPEN, - terms: { - closingDatetime: nextWeek.toString(), - }, - }); - renderComponent(proposal, [ - networkParamsQueryMock, - createUserVoteQueryMock(proposal?.id, VoteValue.VALUE_YES), - ]); - expect(await screen.findByText('You voted For')).toBeInTheDocument(); - }); - - it('Renders proposal state: Open - user voted against', async () => { - const proposal = generateProposal({ - state: ProposalState.STATE_OPEN, - terms: { - closingDatetime: nextWeek.toString(), - }, - }); - renderComponent(proposal, [ - networkParamsQueryMock, - createUserVoteQueryMock(proposal?.id, VoteValue.VALUE_NO), - ]); - expect(await screen.findByText('You voted Against')).toBeInTheDocument(); - }); - - it('Renders proposal state: Open - participation not reached', () => { - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - terms: { - enactmentDatetime: nextWeek.toString(), - }, - votes: { - no: generateNoVotes(0), - yes: generateYesVotes(0), - }, - }) - ); - expect(screen.getByTestId('vote-status')).toHaveTextContent( - 'Participation not reached' - ); - }); - it('Renders proposal state: Open - majority not reached', () => { renderComponent( generateProposal({ @@ -309,7 +227,7 @@ describe('Proposals list item details', () => { }) ); expect(screen.getByTestId('vote-status')).toHaveTextContent( - 'Majority not reached' + 'Currently expected to fail' ); }); @@ -327,42 +245,8 @@ describe('Proposals list item details', () => { }, }) ); - expect(screen.getByTestId('vote-status')).toHaveTextContent('Set to pass'); - }); - - it('Renders proposal state: Declined - participation not reached', () => { - renderComponent( - generateProposal({ - state: ProposalState.STATE_DECLINED, - terms: { - enactmentDatetime: lastWeek.toString(), - }, - votes: { - no: generateNoVotes(0), - yes: generateYesVotes(0), - }, - }) - ); expect(screen.getByTestId('vote-status')).toHaveTextContent( - 'Participation not reached' - ); - }); - - 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('vote-status')).toHaveTextContent( - 'Majority not reached' + 'Currently expected to pass' ); }); diff --git a/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item-details.tsx b/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item-details.tsx index d807981c5..9c61e0692 100644 --- a/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item-details.tsx +++ b/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item-details.tsx @@ -1,9 +1,6 @@ import { Link } from 'react-router-dom'; -import { Button } from '@vegaprotocol/ui-toolkit'; -import { useVoteInformation } from '../../hooks'; -import { useUserVote } from '../vote-details/use-user-vote'; -import { StatusPass } from '../current-proposal-status/current-proposal-status'; -import { format, formatDistanceToNowStrict } from 'date-fns'; +import { Button, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; +import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns'; import { useTranslation } from 'react-i18next'; import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats'; import type { ReactNode } from 'react'; @@ -14,138 +11,100 @@ import { import Routes from '../../../routes'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; - -const MajorityNotReached = () => { - const { t } = useTranslation(); - return ( - <> - {t('Majority')} {t('not reached')} - - ); -}; -const ParticipationNotReached = () => { - const { t } = useTranslation(); - return ( - <> - {t('Participation')} {t('not reached')} - - ); -}; +import { useVoteInformation } from '../../hooks'; export const ProposalsListItemDetails = ({ proposal, }: { proposal: ProposalFieldsFragment | ProposalQuery['proposal']; }) => { + const { t } = useTranslation(); const state = proposal?.state; - const { - willPassByTokenVote, - willPassByLPVote, - majorityMet, - participationMet, - } = useVoteInformation({ + const { willPassByTokenVote, willPassByLPVote } = useVoteInformation({ proposal, }); - const { t } = useTranslation(); - const { voteState } = useUserVote(proposal?.id); - const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket'; const updateMarketWillPass = willPassByTokenVote || willPassByLPVote; const updateMarketVotePassMethod = willPassByTokenVote ? t('byTokenVote') : t('byLPVote'); + const nowToEnactmentInHours = differenceInHours( + new Date(proposal?.terms.closingDatetime), + new Date() + ); + const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket'; let voteDetails: ReactNode; let voteStatus: ReactNode; switch (state) { case ProposalState.STATE_ENACTED: { - voteDetails = proposal?.terms.enactmentDatetime && ( - <> - {format( - new Date(proposal?.terms.enactmentDatetime), - DATE_FORMAT_DETAILED - )} - - ); + voteDetails = + proposal?.terms.enactmentDatetime && + t('enactedOn{{date}}', { + enactmentDate: + proposal?.terms.enactmentDatetime && + format( + new Date(proposal?.terms.enactmentDatetime), + DATE_FORMAT_DETAILED + ), + }); break; } case ProposalState.STATE_PASSED: { - voteDetails = proposal?.terms.change.__typename !== 'NewFreeform' && ( - <> - {t('toEnactOn')}{' '} - {proposal?.terms.enactmentDatetime && + voteDetails = + proposal?.terms.change.__typename !== 'NewFreeform' && + t('enactsOn{{date}}', { + enactmentDate: + proposal?.terms.enactmentDatetime && format( new Date(proposal.terms.enactmentDatetime), DATE_FORMAT_DETAILED - )} - - ); + ), + }); break; } case ProposalState.STATE_WAITING_FOR_NODE_VOTE: { - voteDetails = proposal?.terms.change.__typename !== 'NewFreeform' && ( - <> - {t('toEnactOn')}{' '} - {proposal?.terms.enactmentDatetime && + voteDetails = + proposal?.terms.change.__typename !== 'NewFreeform' && + t('enactsOn{{date}}', { + enactmentDate: + proposal?.terms.enactmentDatetime && format( new Date(proposal.terms.enactmentDatetime), DATE_FORMAT_DETAILED - )} - - ); + ), + }); break; } case ProposalState.STATE_OPEN: { - voteDetails = (voteState === 'Yes' && ( - <> - {t('youVoted')} {t('voteState_Yes')} - - )) || - (voteState === 'No' && ( - <> - {t('youVoted')} {t('voteState_No')} - - )) || ( - <> - {formatDistanceToNowStrict( - new Date(proposal?.terms.closingDatetime) - )}{' '} - {t('left to vote')} - - ); + voteDetails = ( + + {formatDistanceToNowStrict(new Date(proposal?.terms.closingDatetime))}{' '} + {t('left to vote')} + + ); voteStatus = (isUpdateMarket && (updateMarketWillPass ? ( <> - {t('Set to')}{' '} - - {t('pass')} {updateMarketVotePassMethod} - + {t('currentlySetTo')} {t('pass')} {updateMarketVotePassMethod} ) : ( <> - {t('Set to')} {t('fail')} + {t('currentlySetTo')} {t('fail')} ))) || - (!participationMet && ) || - (!majorityMet && ) || (willPassByTokenVote ? ( <> - {t('Set to')} {t('pass')} + {t('currentlySetTo')} {t('pass')} ) : ( <> - {t('Set to')} {t('fail')} + {t('currentlySetTo')} {t('fail')} )); break; } - case ProposalState.STATE_DECLINED: { - voteStatus = - (!participationMet && ) || - (!majorityMet && ); - break; - } case ProposalState.STATE_REJECTED: { voteStatus = proposal?.rejectionReason && ( <>{t(ProposalRejectionReasonMapping[proposal.rejectionReason])} @@ -155,29 +114,21 @@ export const ProposalsListItemDetails = ({ } return ( -
- {voteDetails && ( -
- {voteDetails} -
- )} - {voteStatus && ( -
- {voteStatus} -
- )} +
+
+ + +
+
+ {voteDetails && {voteDetails}} + {voteDetails && voteStatus && ·} + {voteStatus && {voteStatus}} +
+ {proposal?.id && ( -
- - - -
+ + + )}
); diff --git a/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item.tsx b/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item.tsx index cf3a89da5..49de00fea 100644 --- a/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item.tsx +++ b/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item.tsx @@ -3,18 +3,23 @@ import { ProposalHeader } from '../proposal-detail-header/proposal-header'; import { ProposalsListItemDetails } from './proposals-list-item-details'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; +import type { NetworkParamsResult } from '@vegaprotocol/network-parameters'; interface ProposalsListItemProps { proposal?: ProposalFieldsFragment | ProposalQuery['proposal'] | null; + networkParams: Partial | null; } -export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => { - if (!proposal || !proposal.id) return null; +export const ProposalsListItem = ({ + proposal, + networkParams, +}: ProposalsListItemProps) => { + if (!proposal || !proposal.id || !networkParams) return null; return (
  • - +
  • diff --git a/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.spec.tsx b/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.spec.tsx index e752ea273..bb8c93f70 100644 --- a/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.spec.tsx +++ b/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.spec.tsx @@ -88,31 +88,39 @@ afterAll(() => { jest.useRealTimers(); }); +jest.mock('../vote-details/use-user-vote', () => ({ + useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })), +})); + describe('Proposals list', () => { - it('Render a page title and link to the make proposal form', () => { + it('Render a page title and link to the make proposal form', async () => { render(renderComponent([])); + await screen.findByTestId('proposals-list'); expect(screen.getByText('Proposals')).toBeInTheDocument(); expect(screen.getByTestId('new-proposal-link')).toBeInTheDocument(); }); - it('Will hide filter if no proposals', () => { + it('Will hide filter if no proposals', async () => { render(renderComponent([])); + await screen.findByTestId('proposals-list'); expect( screen.queryByTestId('proposals-list-filter') ).not.toBeInTheDocument(); }); - it('Will show filter if there are proposals', () => { + it('Will show filter if there are proposals', async () => { render(renderComponent([enactedProposalClosedLastWeek])); + await screen.findByTestId('proposals-list'); expect(screen.queryByTestId('proposals-list-filter')).toBeInTheDocument(); }); - it('Will render a link to rejected proposals', () => { + it('Will render a link to rejected proposals', async () => { render(renderComponent([])); + await screen.findByTestId('proposals-list'); expect(screen.getByText('See rejected proposals')).toBeInTheDocument(); }); - it('Places proposals correctly in open or closed lists', () => { + it('Places proposals correctly in open or closed lists', async () => { render( renderComponent([ openProposalClosesNextWeek, @@ -121,6 +129,7 @@ describe('Proposals list', () => { failedProposalClosedLastMonth, ]) ); + await screen.findByTestId('proposals-list'); const openProposals = within(screen.getByTestId('open-proposals')); const closedProposals = within(screen.getByTestId('closed-proposals')); expect(openProposals.getAllByTestId('proposals-list-item').length).toBe(2); @@ -129,42 +138,47 @@ describe('Proposals list', () => { ); }); - it('Displays info on no proposals', () => { + it('Displays info on no proposals', async () => { render(renderComponent([])); + await screen.findByTestId('proposals-list'); expect(screen.queryByTestId('open-proposals')).not.toBeInTheDocument(); expect(screen.getByTestId('no-open-proposals')).toBeInTheDocument(); expect(screen.queryByTestId('closed-proposals')).not.toBeInTheDocument(); expect(screen.getByTestId('no-closed-proposals')).toBeInTheDocument(); }); - it('Displays info on no open proposals if only closed are present', () => { + it('Displays info on no open proposals if only closed are present', async () => { render(renderComponent([enactedProposalClosedLastWeek])); + await screen.findByTestId('proposals-list'); expect(screen.queryByTestId('open-proposals')).not.toBeInTheDocument(); expect(screen.getByTestId('no-open-proposals')).toBeInTheDocument(); expect(screen.getByTestId('closed-proposals')).toBeInTheDocument(); expect(screen.queryByTestId('no-closed-proposals')).not.toBeInTheDocument(); }); - it('Displays info on no closed proposals if only open are present', () => { + it('Displays info on no closed proposals if only open are present', async () => { render(renderComponent([openProposalClosesNextWeek])); + await screen.findByTestId('proposals-list'); expect(screen.getByTestId('open-proposals')).toBeInTheDocument(); expect(screen.queryByTestId('no-open-proposals')).not.toBeInTheDocument(); expect(screen.queryByTestId('closed-proposals')).not.toBeInTheDocument(); expect(screen.getByTestId('no-closed-proposals')).toBeInTheDocument(); }); - it('Opens filter form when button is clicked', () => { + it('Opens filter form when button is clicked', async () => { render( renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek]) ); + await screen.findByTestId('proposals-list'); fireEvent.click(screen.getByTestId('proposal-filter-toggle')); expect(screen.getByTestId('proposals-list-filter')).toBeInTheDocument(); }); - it('Filters list by text - party id', () => { + it('Filters list by text - party id', async () => { render( renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek]) ); + await screen.findByTestId('proposals-list'); fireEvent.click(screen.getByTestId('proposal-filter-toggle')); fireEvent.change(screen.getByTestId('filter-input'), { target: { value: 'bvcx' }, @@ -174,10 +188,11 @@ describe('Proposals list', () => { expect(container.querySelector('#proposal1')).not.toBeInTheDocument(); }); - it('Filters list by text - proposal id', () => { + it('Filters list by text - proposal id', async () => { render( renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek]) ); + await screen.findByTestId('proposals-list'); fireEvent.click(screen.getByTestId('proposal-filter-toggle')); fireEvent.change(screen.getByTestId('filter-input'), { target: { value: 'proposal1' }, @@ -187,10 +202,11 @@ describe('Proposals list', () => { expect(container.querySelector('#proposal2')).not.toBeInTheDocument(); }); - it('Filters list by text - check for substring matching', () => { + it('Filters list by text - check for substring matching', async () => { render( renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek]) ); + await screen.findByTestId('proposals-list'); fireEvent.click(screen.getByTestId('proposal-filter-toggle')); fireEvent.change(screen.getByTestId('filter-input'), { target: { value: 'osal1' }, @@ -200,10 +216,11 @@ describe('Proposals list', () => { expect(container.querySelector('#proposal2')).not.toBeInTheDocument(); }); - it('When filter is used, clear button is visible', () => { + it('When filter is used, clear button is visible', async () => { render( renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek]) ); + await screen.findByTestId('proposals-list'); fireEvent.click(screen.getByTestId('proposal-filter-toggle')); fireEvent.change(screen.getByTestId('filter-input'), { target: { value: 'test' }, @@ -211,10 +228,11 @@ describe('Proposals list', () => { expect(screen.getByTestId('clear-filter')).toBeInTheDocument(); }); - it('When clear filter button is used, input is cleared', () => { + it('When clear filter button is used, input is cleared', async () => { render( renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek]) ); + await screen.findByTestId('proposals-list'); fireEvent.click(screen.getByTestId('proposal-filter-toggle')); fireEvent.change(screen.getByTestId('filter-input'), { target: { value: 'test' }, @@ -225,37 +243,41 @@ describe('Proposals list', () => { ); }); - it('Displays a toggle for closed proposals if there are both closed governance proposals and closed upgrade proposals', () => { + it('Displays a toggle for closed proposals if there are both closed governance proposals and closed upgrade proposals', async () => { render( renderComponent( [enactedProposalClosedLastWeek], [closedProtocolUpgradeProposal] ) ); + await screen.findByTestId('proposals-list'); expect(screen.getByTestId('toggle-closed-proposals')).toBeInTheDocument(); }); - it('Does not display a toggle for closed proposals if there are only closed upgrade proposals', () => { + it('Does not display a toggle for closed proposals if there are only closed upgrade proposals', async () => { render(renderComponent([], [closedProtocolUpgradeProposal])); + await screen.findByTestId('proposals-list'); expect( screen.queryByTestId('toggle-closed-proposals') ).not.toBeInTheDocument(); }); - it('Does not display a toggle for closed proposals if there are only closed governance proposals', () => { + it('Does not display a toggle for closed proposals if there are only closed governance proposals', async () => { render(renderComponent([enactedProposalClosedLastWeek])); + await screen.findByTestId('proposals-list'); expect( screen.queryByTestId('toggle-closed-proposals') ).not.toBeInTheDocument(); }); - it('Does not display a toggle for closed proposals if the proposal filter is engaged', () => { + it('Does not display a toggle for closed proposals if the proposal filter is engaged', async () => { render( renderComponent( [enactedProposalClosedLastWeek], [closedProtocolUpgradeProposal] ) ); + await screen.findByTestId('proposal-filter-toggle'); fireEvent.click(screen.getByTestId('proposal-filter-toggle')); fireEvent.change(screen.getByTestId('filter-input'), { target: { value: 'test' }, @@ -265,7 +287,7 @@ describe('Proposals list', () => { ).not.toBeInTheDocument(); }); - it('Displays closed governance proposals by default due to default for the toggle', () => { + it('Displays closed governance proposals by default due to default for the toggle', async () => { render( renderComponent( [enactedProposalClosedLastWeek], @@ -273,17 +295,19 @@ describe('Proposals list', () => { ) ); expect( - screen.getByTestId('closed-governance-proposals') + await screen.findByTestId('closed-governance-proposals') ).toBeInTheDocument(); }); - it('Displays closed upgrade proposals when the toggle is clicked', () => { + it('Displays closed upgrade proposals when the toggle is clicked', async () => { render( renderComponent( [enactedProposalClosedLastWeek], [closedProtocolUpgradeProposal] ) ); + + await screen.findByTestId('toggle-closed-proposals'); fireEvent.click(screen.getByText('Network upgrades')); expect(screen.getByTestId('closed-upgrade-proposals')).toBeInTheDocument(); }); diff --git a/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.tsx b/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.tsx index 996c2a7dd..68484bcbc 100644 --- a/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.tsx +++ b/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.tsx @@ -8,6 +8,7 @@ import { ProtocolUpgradeProposalsListItem } from '../protocol-upgrade-proposals- import { ProposalsListFilter } from '../proposals-list-filter'; import Routes from '../../../routes'; import { + AsyncRenderer, Button, Toggle, VegaIcon, @@ -19,6 +20,10 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals'; import { DocsLinks, ExternalLinks } from '@vegaprotocol/environment'; +import { + NetworkParams, + useNetworkParams, +} from '@vegaprotocol/network-parameters'; interface ProposalsListProps { proposals: Array; @@ -70,6 +75,20 @@ export const ProposalsList = ({ lastBlockHeight, }: ProposalsListProps) => { const { t } = useTranslation(); + const { + params: networkParams, + loading: networkParamsLoading, + error: networkParamsError, + } = useNetworkParams([ + NetworkParams.governance_proposal_market_requiredMajority, + NetworkParams.governance_proposal_updateMarket_requiredMajority, + NetworkParams.governance_proposal_updateMarket_requiredMajorityLP, + NetworkParams.governance_proposal_asset_requiredMajority, + NetworkParams.governance_proposal_updateAsset_requiredMajority, + NetworkParams.governance_proposal_updateNetParam_requiredMajority, + NetworkParams.governance_proposal_freeform_requiredMajority, + ]); + const [filterString, setFilterString] = useState(''); const [closedProposalsView, setClosedProposalsView] = useState( @@ -134,166 +153,181 @@ export const ProposalsList = ({ p?.party?.id?.toString().includes(filterString); return ( - <> -
    - + +
    +
    + - {DocsLinks && ( -
    - - - -
    + {DocsLinks && ( +
    + + + +
    + )} +
    + +

    + {t( + `The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network. Network upgrades are proposed and approved by validators.` + )}{' '} + + {t(`Find out more about Vega governance`)} + +

    + + {proposals.length > 0 && ( + { + setFilterString(value); + if (value.length > 0) { + // If the filter is engaged, ensure the user is viewing governance proposals, + // as network upgrades do not have IDs to filter by and will be excluded. + setClosedProposalsView( + ClosedProposalsViewOptions.NetworkGovernance + ); + } + }} + /> )} -
    -

    - {t( - `The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network. Network upgrades are proposed and approved by validators.` - )}{' '} - - {t(`Find out more about Vega governance`)} - -

    +
    + - {proposals.length > 0 && ( - { - setFilterString(value); - if (value.length > 0) { - // If the filter is engaged, ensure the user is viewing governance proposals, - // as network upgrades do not have IDs to filter by and will be excluded. - setClosedProposalsView( - ClosedProposalsViewOptions.NetworkGovernance - ); - } - }} - /> - )} + {sortedProposals.open.length > 0 || + sortedProtocolUpgradeProposals.open.length > 0 ? ( +
      + {filterString.length < 1 && + sortedProtocolUpgradeProposals.open.map((proposal) => ( + + ))} -
      - - - {sortedProposals.open.length > 0 || - sortedProtocolUpgradeProposals.open.length > 0 ? ( -
        - {filterString.length < 1 && - sortedProtocolUpgradeProposals.open.map((proposal) => ( - ( + ))} +
      + ) : ( +

      + {t('noOpenProposals')} +

      + )} +
      - {sortedProposals.open.filter(filterPredicate).map((proposal) => ( - - ))} -
    - ) : ( -

    - {t('noOpenProposals')} -

    - )} -
    - -
    - - {sortedProposals.closed.length > 0 || - sortedProtocolUpgradeProposals.closed.length > 0 ? ( - <> - { - // We need both the closed proposals and closed protocol upgrade - // proposals to be present for there to be a toggle. It also gets - // hidden if the user has filtered the list, as the upgrade proposals - // do not have the necessary fields for filtering. - sortedProposals.closed.length > 0 && - sortedProtocolUpgradeProposals.closed.length > 0 && - filterString.length < 1 && ( -
    -
    - - setClosedProposalsView( - e.target.value as ClosedProposalsViewOptions - ) - } - /> +
    + + {sortedProposals.closed.length > 0 || + sortedProtocolUpgradeProposals.closed.length > 0 ? ( + <> + { + // We need both the closed proposals and closed protocol upgrade + // proposals to be present for there to be a toggle. It also gets + // hidden if the user has filtered the list, as the upgrade proposals + // do not have the necessary fields for filtering. + sortedProposals.closed.length > 0 && + sortedProtocolUpgradeProposals.closed.length > 0 && + filterString.length < 1 && ( +
    +
    + + setClosedProposalsView( + e.target.value as ClosedProposalsViewOptions + ) + } + /> +
    -
    - ) - } + ) + } -
      - {closedProposalsView === - ClosedProposalsViewOptions.NetworkUpgrades && ( -
      - {sortedProtocolUpgradeProposals.closed.map((proposal) => ( - - ))} -
      - )} - - {closedProposalsView === - ClosedProposalsViewOptions.NetworkGovernance && ( -
      - {sortedProposals.closed - .filter(filterPredicate) - .map((proposal) => ( - + {closedProposalsView === + ClosedProposalsViewOptions.NetworkUpgrades && ( +
      + {sortedProtocolUpgradeProposals.closed.map((proposal) => ( + ))} -
      - )} -
    - - ) : ( -

    - {t('noClosedProposals')} -

    - )} -
    +
    + )} - - {t('seeRejectedProposals')} - - + {closedProposalsView === + ClosedProposalsViewOptions.NetworkGovernance && ( +
    + {sortedProposals.closed + .filter(filterPredicate) + .map((proposal) => ( + + ))} +
    + )} + + + ) : ( +

    + {t('noClosedProposals')} +

    + )} + + + + {t('seeRejectedProposals')} + +
    +
    ); }; diff --git a/apps/governance/src/routes/proposals/components/proposals-list/rejected-proposals-list.spec.tsx b/apps/governance/src/routes/proposals/components/proposals-list/rejected-proposals-list.spec.tsx index 987d18d9f..ad288470b 100644 --- a/apps/governance/src/routes/proposals/components/proposals-list/rejected-proposals-list.spec.tsx +++ b/apps/governance/src/routes/proposals/components/proposals-list/rejected-proposals-list.spec.tsx @@ -5,7 +5,7 @@ import { BrowserRouter as Router } from 'react-router-dom'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { RejectedProposalsList } from './rejected-proposals-list'; import { ProposalState } from '@vegaprotocol/types'; -import { render, screen, within } from '@testing-library/react'; +import { render, screen, waitFor, within } from '@testing-library/react'; import { mockWalletContext, networkParamsQueryMock, @@ -55,24 +55,38 @@ afterAll(() => { jest.useRealTimers(); }); +jest.mock('../vote-details/use-user-vote', () => ({ + useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })), +})); + describe('Rejected proposals list', () => { - it('Renders a list of proposals', () => { + it('Renders a list of proposals', async () => { render( renderComponent([ rejectedProposalClosedLastMonth, rejectedProposalClosesNextWeek, ]) ); - const rejectedProposals = within(screen.getByTestId('rejected-proposals')); - const rejectedProposalsItems = rejectedProposals.getAllByTestId( - 'proposals-list-item' - ); - expect(rejectedProposalsItems).toHaveLength(2); + + await waitFor(() => { + const rejectedProposals = within( + screen.getByTestId('rejected-proposals') + ); + const rejectedProposalsItems = rejectedProposals.getAllByTestId( + 'proposals-list-item' + ); + expect(rejectedProposalsItems).toHaveLength(2); + }); }); - it('Displays text when there are no proposals', () => { + it('Displays text when there are no proposals', async () => { render(renderComponent([])); - expect(screen.queryByTestId('rejected-proposals')).not.toBeInTheDocument(); - expect(screen.getByTestId('no-rejected-proposals')).toBeInTheDocument(); + + await waitFor(() => { + expect(screen.getByTestId('no-rejected-proposals')).toBeInTheDocument(); + expect( + screen.queryByTestId('rejected-proposals') + ).not.toBeInTheDocument(); + }); }); }); diff --git a/apps/governance/src/routes/proposals/components/proposals-list/rejected-proposals-list.tsx b/apps/governance/src/routes/proposals/components/proposals-list/rejected-proposals-list.tsx index 0b128df9d..31ae42f45 100644 --- a/apps/governance/src/routes/proposals/components/proposals-list/rejected-proposals-list.tsx +++ b/apps/governance/src/routes/proposals/components/proposals-list/rejected-proposals-list.tsx @@ -1,10 +1,15 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { Heading } from '../../../../components/heading'; import { ProposalsListItem } from '../proposals-list-item'; import { ProposalsListFilter } from '../proposals-list-filter'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; +import { + NetworkParams, + useNetworkParams, +} from '@vegaprotocol/network-parameters'; interface ProposalsListProps { proposals: Array; @@ -12,6 +17,19 @@ interface ProposalsListProps { export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => { const { t } = useTranslation(); + const { + params: networkParams, + loading: networkParamsLoading, + error: networkParamsError, + } = useNetworkParams([ + NetworkParams.governance_proposal_market_requiredMajority, + NetworkParams.governance_proposal_updateMarket_requiredMajority, + NetworkParams.governance_proposal_updateMarket_requiredMajorityLP, + NetworkParams.governance_proposal_asset_requiredMajority, + NetworkParams.governance_proposal_updateAsset_requiredMajority, + NetworkParams.governance_proposal_updateNetParam_requiredMajority, + NetworkParams.governance_proposal_freeform_requiredMajority, + ]); const [filterString, setFilterString] = useState(''); const filterPredicate = ( @@ -21,7 +39,11 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => { p?.party?.id?.toString().includes(filterString); return ( - <> + { {proposals.length > 0 ? (
      {proposals.filter(filterPredicate).map((proposal) => ( - + ))}
    ) : ( @@ -40,6 +66,6 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {

    )} - +
    ); }; diff --git a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.spec.tsx b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.spec.tsx index 47a035b29..60285d79b 100644 --- a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.spec.tsx +++ b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.spec.tsx @@ -20,43 +20,6 @@ const renderComponent = (proposal: ProtocolUpgradeProposalFieldsFragment) => ); describe('ProtocolUpgradeProposalsListItem', () => { - it('renders the correct status icon for each proposal status', () => { - const statuses = [ - { - status: - ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED, - icon: 'protocol-upgrade-proposal-status-icon-rejected', - text: 'Declined by validators', - }, - { - status: - ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING, - icon: 'protocol-upgrade-proposal-status-icon-pending', - text: 'Waiting for validator votes', - }, - { - status: - ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED, - icon: 'protocol-upgrade-proposal-status-icon-approved', - text: 'Approved by validators', - }, - { - status: - ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED, - icon: 'protocol-upgrade-proposal-status-icon-unspecified', - text: 'Unspecified', - }, - ]; - - statuses.forEach(({ status, icon, text }) => { - renderComponent({ ...proposal, status }); - const statusIcon = screen.getByTestId(icon); - const textContent = screen.getByText(text); - expect(statusIcon).toBeInTheDocument(); - expect(textContent).toBeInTheDocument(); - }); - }); - it('renders the correct Vega release tag', () => { renderComponent(proposal); const releaseTag = screen.getByTestId( diff --git a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.tsx b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.tsx index 24175cb11..af65c9086 100644 --- a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.tsx +++ b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.tsx @@ -1,17 +1,10 @@ import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import { - Button, - Icon, - Lozenge, - RoundedWrapper, -} from '@vegaprotocol/ui-toolkit'; +import { Button, RoundedWrapper } from '@vegaprotocol/ui-toolkit'; import { stripFullStops } from '@vegaprotocol/utils'; -import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types'; import { SubHeading } from '../../../../components/heading'; import { ProposalInfoLabel } from '../proposal-info-label'; import Routes from '../../../routes'; -import type { ReactNode } from 'react'; import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals'; interface ProtocolProposalsListItemProps { @@ -24,45 +17,21 @@ export const ProtocolUpgradeProposalsListItem = ({ const { t } = useTranslation(); if (!proposal || !proposal.upgradeBlockHeight) return null; - let proposalStatusIcon: ReactNode; - - switch (proposal.status) { - case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED: - proposalStatusIcon = ( - - - - ); - break; - case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING: - proposalStatusIcon = ( - - - - ); - break; - case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED: - proposalStatusIcon = ( - - - - ); - break; - case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED: - proposalStatusIcon = ( - - - - ); - break; - } - return (
  • +
    + + {t('networkUpgrade')} + +
    +
    -
    -
    -
    - - {t('networkUpgrade')} - -
    - -
    - - {t(`${proposal.status}`)} {proposalStatusIcon} - -
    -
    - +
    {t('vegaReleaseTag')}:{' '} - {proposal.vegaReleaseTag} + {proposal.vegaReleaseTag}
    {t('upgradeBlockHeight')}:{' '} - {proposal.upgradeBlockHeight} -
    - -
    -
    - - - -
    + {proposal.upgradeBlockHeight}
    + +
    + {t(`${proposal.status}`)} +
    + + + +
  • ); diff --git a/apps/governance/src/routes/proposals/components/vote-details/vote-buttons.tsx b/apps/governance/src/routes/proposals/components/vote-details/vote-buttons.tsx index 09a58541b..6b5ad3016 100644 --- a/apps/governance/src/routes/proposals/components/vote-details/vote-buttons.tsx +++ b/apps/governance/src/routes/proposals/components/vote-details/vote-buttons.tsx @@ -24,8 +24,8 @@ interface VoteButtonsContainerProps { voteDatetime: Date | null; proposalId: string | null; proposalState: ProposalState; - minVoterBalance: string | null; - spamProtectionMinTokens: string | null; + minVoterBalance: string | null | undefined; + spamProtectionMinTokens: string | null | undefined; submit: (voteValue: VoteValue, proposalId: string | null) => Promise; dialog: (props: DialogProps) => JSX.Element; className?: string; diff --git a/apps/governance/src/routes/proposals/components/vote-details/vote-details.tsx b/apps/governance/src/routes/proposals/components/vote-details/vote-details.tsx index a819118a4..2e3b3b533 100644 --- a/apps/governance/src/routes/proposals/components/vote-details/vote-details.tsx +++ b/apps/governance/src/routes/proposals/components/vote-details/vote-details.tsx @@ -17,8 +17,8 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; interface VoteDetailsProps { proposal: ProposalFieldsFragment | ProposalQuery['proposal']; - minVoterBalance: string | null; - spamProtectionMinTokens: string | null; + minVoterBalance: string | null | undefined; + spamProtectionMinTokens: string | null | undefined; proposalType: ProposalType | null; } diff --git a/apps/governance/src/routes/proposals/proposal/proposal-container.tsx b/apps/governance/src/routes/proposals/proposal/proposal-container.tsx index 451c3042f..bfbd0ee30 100644 --- a/apps/governance/src/routes/proposals/proposal/proposal-container.tsx +++ b/apps/governance/src/routes/proposals/proposal/proposal-container.tsx @@ -10,9 +10,33 @@ import { ENV } from '../../../config'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { marketInfoWithDataProvider } from '@vegaprotocol/markets'; import { useAssetQuery } from '@vegaprotocol/assets'; +import { + NetworkParams, + useNetworkParams, +} from '@vegaprotocol/network-parameters'; export const ProposalContainer = () => { const params = useParams<{ proposalId: string }>(); + const { + params: networkParams, + loading: networkParamsLoading, + error: networkParamsError, + } = useNetworkParams([ + NetworkParams.governance_proposal_market_minVoterBalance, + NetworkParams.governance_proposal_updateMarket_minVoterBalance, + NetworkParams.governance_proposal_asset_minVoterBalance, + NetworkParams.governance_proposal_updateAsset_minVoterBalance, + NetworkParams.governance_proposal_updateNetParam_minVoterBalance, + NetworkParams.governance_proposal_freeform_minVoterBalance, + NetworkParams.spam_protection_voting_min_tokens, + NetworkParams.governance_proposal_market_requiredMajority, + NetworkParams.governance_proposal_updateMarket_requiredMajority, + NetworkParams.governance_proposal_updateMarket_requiredMajorityLP, + NetworkParams.governance_proposal_asset_requiredMajority, + NetworkParams.governance_proposal_updateAsset_requiredMajority, + NetworkParams.governance_proposal_updateNetParam_requiredMajority, + NetworkParams.governance_proposal_freeform_requiredMajority, + ]); const { state: { data: restData }, } = useFetch(`${ENV.rest}governance?proposalId=${params.proposalId}`); @@ -62,10 +86,13 @@ export const ProposalContainer = () => { return ( { {data?.proposal ? ( ({ + useVegaRelease: jest.fn(), + useVegaReleases: jest.fn(), +})); + const renderComponent = () => render( diff --git a/apps/governance/src/routes/proposals/test-helpers/mocks.ts b/apps/governance/src/routes/proposals/test-helpers/mocks.ts index bead8e64d..25e4e6230 100644 --- a/apps/governance/src/routes/proposals/test-helpers/mocks.ts +++ b/apps/governance/src/routes/proposals/test-helpers/mocks.ts @@ -2,6 +2,10 @@ import { NetworkParamsDocument } from '@vegaprotocol/network-parameters'; import type { MockedResponse } from '@apollo/client/testing'; import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; import type { PubKey } from '@vegaprotocol/wallet'; +import type { VoteValue } from '@vegaprotocol/types'; +import type { UserVoteQuery } from '../components/vote-details/__generated__/Vote'; +import { UserVoteDocument } from '../components/vote-details/__generated__/Vote'; +import faker from 'faker'; export const mockPubkey: PubKey = { publicKey: '0x123', @@ -49,6 +53,16 @@ export const networkParamsQueryMock: MockedResponse = { }, }; +export const mockNetworkParams = { + governance_proposal_asset_requiredMajority: '0.66', + governance_proposal_freeform_requiredMajority: '0.66', + governance_proposal_market_requiredMajority: '0.66', + governance_proposal_updateAsset_requiredMajority: '0.66', + governance_proposal_updateMarket_requiredMajority: '0.66', + governance_proposal_updateMarket_requiredMajorityLP: '0.66', + governance_proposal_updateNetParam_requiredMajority: '0.5', +}; + const oneMinute = 1000 * 60; const oneHour = oneMinute * 60; const oneDay = oneHour * 24; @@ -62,3 +76,34 @@ export const lastWeek = new Date(-oneWeek); export const nextWeek = new Date(oneWeek); export const lastMonth = new Date(-oneMonth); export const nextMonth = new Date(oneMonth); + +export const createUserVoteQueryMock = ( + proposalId: string, + value: VoteValue +): MockedResponse => ({ + request: { + query: UserVoteDocument, + variables: { + partyId: mockPubkey.publicKey, + }, + }, + result: { + data: { + party: { + votesConnection: { + edges: [ + { + node: { + proposalId, + vote: { + value, + datetime: faker.date.past().toISOString(), + }, + }, + }, + ], + }, + }, + }, + }, +}); diff --git a/libs/network-parameters/src/use-network-params.ts b/libs/network-parameters/src/use-network-params.ts index 0f028ea31..bdb68e663 100644 --- a/libs/network-parameters/src/use-network-params.ts +++ b/libs/network-parameters/src/use-network-params.ts @@ -16,6 +16,12 @@ export const NetworkParams = { governance_proposal_market_maxClose: 'governance_proposal_market_maxClose', governance_proposal_market_minEnact: 'governance_proposal_market_minEnact', governance_proposal_market_maxEnact: 'governance_proposal_market_maxEnact', + governance_proposal_market_requiredMajority: + 'governance_proposal_market_requiredMajority', + governance_proposal_market_requiredParticipation: + 'governance_proposal_market_requiredParticipation', + governance_proposal_market_minProposerBalance: + 'governance_proposal_market_minProposerBalance', governance_proposal_updateMarket_minVoterBalance: 'governance_proposal_updateMarket_minVoterBalance', governance_proposal_updateMarket_requiredMajority: @@ -30,12 +36,24 @@ export const NetworkParams = { 'governance_proposal_updateMarket_minEnact', governance_proposal_updateMarket_maxEnact: 'governance_proposal_updateMarket_maxEnact', + governance_proposal_updateMarket_requiredParticipation: + 'governance_proposal_updateMarket_requiredParticipation', + governance_proposal_updateMarket_requiredParticipationLP: + 'governance_proposal_updateMarket_requiredParticipationLP', + governance_proposal_updateMarket_minProposerBalance: + 'governance_proposal_updateMarket_minProposerBalance', governance_proposal_asset_minVoterBalance: 'governance_proposal_asset_minVoterBalance', governance_proposal_asset_minClose: 'governance_proposal_asset_minClose', governance_proposal_asset_maxClose: 'governance_proposal_asset_maxClose', governance_proposal_asset_minEnact: 'governance_proposal_asset_minEnact', governance_proposal_asset_maxEnact: 'governance_proposal_asset_maxEnact', + governance_proposal_asset_requiredMajority: + 'governance_proposal_asset_requiredMajority', + governance_proposal_asset_requiredParticipation: + 'governance_proposal_asset_requiredParticipation', + governance_proposal_asset_minProposerBalance: + 'governance_proposal_asset_minProposerBalance', governance_proposal_updateAsset_minVoterBalance: 'governance_proposal_updateAsset_minVoterBalance', governance_proposal_updateAsset_minClose: @@ -46,6 +64,12 @@ export const NetworkParams = { 'governance_proposal_updateAsset_minEnact', governance_proposal_updateAsset_maxEnact: 'governance_proposal_updateAsset_maxEnact', + governance_proposal_updateAsset_requiredMajority: + 'governance_proposal_updateAsset_requiredMajority', + governance_proposal_updateAsset_requiredParticipation: + 'governance_proposal_updateAsset_requiredParticipation', + governance_proposal_updateAsset_minProposerBalance: + 'governance_proposal_updateAsset_minProposerBalance', governance_proposal_updateNetParam_minClose: 'governance_proposal_updateNetParam_minClose', governance_proposal_updateNetParam_minVoterBalance: @@ -56,42 +80,18 @@ export const NetworkParams = { 'governance_proposal_updateNetParam_minEnact', governance_proposal_updateNetParam_maxEnact: 'governance_proposal_updateNetParam_maxEnact', - governance_proposal_freeform_minVoterBalance: - 'governance_proposal_freeform_minVoterBalance', - governance_proposal_freeform_minClose: - 'governance_proposal_freeform_minClose', - governance_proposal_freeform_maxClose: - 'governance_proposal_freeform_maxClose', - governance_proposal_updateMarket_requiredParticipation: - 'governance_proposal_updateMarket_requiredParticipation', - governance_proposal_updateMarket_requiredParticipationLP: - 'governance_proposal_updateMarket_requiredParticipationLP', - governance_proposal_updateMarket_minProposerBalance: - 'governance_proposal_updateMarket_minProposerBalance', - governance_proposal_market_requiredMajority: - 'governance_proposal_market_requiredMajority', - governance_proposal_market_requiredParticipation: - 'governance_proposal_market_requiredParticipation', - governance_proposal_market_minProposerBalance: - 'governance_proposal_market_minProposerBalance', - governance_proposal_asset_requiredMajority: - 'governance_proposal_asset_requiredMajority', - governance_proposal_asset_requiredParticipation: - 'governance_proposal_asset_requiredParticipation', - governance_proposal_updateAsset_requiredMajority: - 'governance_proposal_updateAsset_requiredMajority', - governance_proposal_updateAsset_requiredParticipation: - 'governance_proposal_updateAsset_requiredParticipation', - governance_proposal_asset_minProposerBalance: - 'governance_proposal_asset_minProposerBalance', - governance_proposal_updateAsset_minProposerBalance: - 'governance_proposal_updateAsset_minProposerBalance', governance_proposal_updateNetParam_requiredMajority: 'governance_proposal_updateNetParam_requiredMajority', governance_proposal_updateNetParam_requiredParticipation: 'governance_proposal_updateNetParam_requiredParticipation', governance_proposal_updateNetParam_minProposerBalance: 'governance_proposal_updateNetParam_minProposerBalance', + governance_proposal_freeform_minVoterBalance: + 'governance_proposal_freeform_minVoterBalance', + governance_proposal_freeform_minClose: + 'governance_proposal_freeform_minClose', + governance_proposal_freeform_maxClose: + 'governance_proposal_freeform_maxClose', governance_proposal_freeform_requiredParticipation: 'governance_proposal_freeform_requiredParticipation', governance_proposal_freeform_requiredMajority: @@ -111,7 +111,7 @@ export const NetworkParams = { type Params = typeof NetworkParams; export type NetworkParamsKey = keyof Params; -type Result = { +export type NetworkParamsResult = { [key in keyof Params]: string; }; @@ -137,7 +137,7 @@ export const useNetworkParams = (params?: T) => { }, [data, params]); return { - params: paramsObj as Pick, + params: paramsObj as Pick, loading, error, }; diff --git a/libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-vote.tsx b/libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-vote.tsx new file mode 100644 index 000000000..e839e9aee --- /dev/null +++ b/libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-vote.tsx @@ -0,0 +1,12 @@ +export const IconVote = ({ size = 16 }: { size: number }) => { + return ( + + + + + ); +}; diff --git a/libs/ui-toolkit/src/components/icon/vega-icons/vega-icon-record.ts b/libs/ui-toolkit/src/components/icon/vega-icons/vega-icon-record.ts index 5688b3c57..e84a59600 100644 --- a/libs/ui-toolkit/src/components/icon/vega-icons/vega-icon-record.ts +++ b/libs/ui-toolkit/src/components/icon/vega-icons/vega-icon-record.ts @@ -28,6 +28,7 @@ import { IconTransfer } from './svg-icons/icon-transfer'; import { IconTrendUp } from './svg-icons/icon-trend-up'; import { IconTrendDown } from './svg-icons/icon-trend-down'; import { IconTwitter } from './svg-icons/icon-twitter'; +import { IconVote } from './svg-icons/icon-vote'; import { IconWithdraw } from './svg-icons/icon-withdraw'; import { IconSearch } from './svg-icons/icon-search'; @@ -63,6 +64,7 @@ export enum VegaIconNames { TREND_UP = 'trend-up', TREND_DOWN = 'trend-down', TWITTER = 'twitter', + VOTE = 'vote', WITHDRAW = 'withdraw', } @@ -96,10 +98,11 @@ export const VegaIconNameMap: Record< search: IconSearch, star: IconStar, tick: IconTick, + ticket: IconTicket, transfer: IconTransfer, 'trend-up': IconTrendUp, 'trend-down': IconTrendDown, twitter: IconTwitter, - ticket: IconTicket, + vote: IconVote, withdraw: IconWithdraw, };