From 4f610bbd1b7646ac2f8188d9d35c1e7aba7d4e3f Mon Sep 17 00:00:00 2001 From: Sam Keen Date: Tue, 5 Sep 2023 08:43:37 +0100 Subject: [PATCH] feat(governance): vote status improvements (#4654) --- .../governance/src/i18n/translations/dev.json | 16 +- apps/governance/src/routes/home/index.tsx | 73 ++-- .../proposal-header.spec.tsx | 2 - .../proposal-header.tsx | 9 +- .../components/proposal-votes-table/index.tsx | 1 - .../proposal-votes-table.spec.tsx | 119 ------ .../proposal-votes-table.tsx | 177 -------- .../proposal-voting-status/index.tsx | 1 - .../proposal-voting-status.spec.tsx | 153 ------- .../proposal-voting-status.tsx | 174 -------- .../components/proposal/proposal.spec.tsx | 8 - .../components/proposal/proposal.tsx | 162 ++++---- .../proposals-list-item-details.spec.tsx | 121 +----- .../proposals-list-item-details.tsx | 31 +- .../proposals-list-item.tsx | 15 +- .../proposals-list/proposals-list.tsx | 326 +++++++-------- .../rejected-proposals-list.tsx | 32 +- .../components/vote-breakdown/index.ts | 1 + .../vote-breakdown/vote-breakdown.spec.tsx | 348 ++++++++++++++++ .../vote-breakdown/vote-breakdown.tsx | 378 ++++++++++++++++++ .../components/vote-details/index.tsx | 2 +- .../components/vote-details/user-vote.tsx | 78 ++++ .../components/vote-details/vote-buttons.tsx | 5 +- .../components/vote-details/vote-details.tsx | 255 ------------ .../components/icon/blueprint-icons/icon.tsx | 3 +- 25 files changed, 1073 insertions(+), 1417 deletions(-) delete mode 100644 apps/governance/src/routes/proposals/components/proposal-votes-table/index.tsx delete mode 100644 apps/governance/src/routes/proposals/components/proposal-votes-table/proposal-votes-table.spec.tsx delete mode 100644 apps/governance/src/routes/proposals/components/proposal-votes-table/proposal-votes-table.tsx delete mode 100644 apps/governance/src/routes/proposals/components/proposal-voting-status/index.tsx delete mode 100644 apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.spec.tsx delete mode 100644 apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.tsx create mode 100644 apps/governance/src/routes/proposals/components/vote-breakdown/index.ts create mode 100644 apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.spec.tsx create mode 100644 apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.tsx create mode 100644 apps/governance/src/routes/proposals/components/vote-details/user-vote.tsx delete mode 100644 apps/governance/src/routes/proposals/components/vote-details/vote-details.tsx diff --git a/apps/governance/src/i18n/translations/dev.json b/apps/governance/src/i18n/translations/dev.json index f8b65b066..95a131119 100644 --- a/apps/governance/src/i18n/translations/dev.json +++ b/apps/governance/src/i18n/translations/dev.json @@ -201,6 +201,8 @@ "STATE_WAITING_FOR_NODE_VOTE": "Waiting for node vote", "UpdateNetworkParameter": "Network parameter", "NewFreeform": "Freeform", + "setToPass": "Set to pass", + "setToFail": "Set to fail", "tokenVotes": "Token votes", "liquidityVotes": "Liquidity votes", "castYourVote": "Cast your vote", @@ -209,13 +211,23 @@ "against": "Against", "majorityRequired": "Majority Required", "participation": "Participation", - "met": "Met", - "notMet": "Not Met", + "majorityThreshold": "majority threshold", + "participationThreshold": "participation threshold", + "met": "met", + "notMet": "not met", "governanceRequired": "Required", "daysLeft": "{{daysLeft}} left to vote.", "toVote": "to vote", "voteFor": "Vote for", "voteAgainst": "Vote against", + "tokenVote": "Token vote", + "tokenVotesFor": "Token votes for", + "tokenVotesAgainst": "Token votes against", + "totalTokensVoted": "Total tokens voted", + "liquidityProviderVote": "Liquidity provider vote", + "liquidityProviderVotesFor": "LP votes for", + "liquidityProviderVotesAgainst": "LP votes against", + "totalLiquidityProviderTokensVoted": "Total LP tokens voted", "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", diff --git a/apps/governance/src/routes/home/index.tsx b/apps/governance/src/routes/home/index.tsx index 064bdabb2..f6c8b4f69 100644 --- a/apps/governance/src/routes/home/index.tsx +++ b/apps/governance/src/routes/home/index.tsx @@ -31,10 +31,6 @@ 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; @@ -47,57 +43,34 @@ 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(`readMoreGovernance`)} - -
+
+ +

{t('homeProposalsIntro')}

+
+ + {t(`readMoreGovernance`)} + +
- -
    - {protocolUpgradeProposals.map((proposal, index) => ( - - ))} + +
      + {protocolUpgradeProposals.map((proposal, index) => ( + + ))} - {proposals.map((proposal) => ( - - ))} -
    + {proposals.map((proposal) => ( + + ))} +
-
- - - -
-
- +
+ + + +
+
); }; 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 be59889e9..def3377e3 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 @@ -16,7 +16,6 @@ import { ProposalHeader } from './proposal-header'; import { lastWeek, nextWeek, - mockNetworkParams, mockWalletContext, createUserVoteQueryMock, } from '../../test-helpers/mocks'; @@ -48,7 +47,6 @@ const renderComponent = ( 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 8507212e5..c6b968566 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 @@ -8,22 +8,19 @@ 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 { ProposalVotingStatus } from '../proposal-voting-status'; -import type { NetworkParamsResult } from '@vegaprotocol/network-parameters'; import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals'; import { FLAGS } from '@vegaprotocol/environment'; import Routes from '../../../routes'; import { Link } from 'react-router-dom'; import type { VoteState } from '../vote-details/use-user-vote'; +import { VoteBreakdown } from '../vote-breakdown'; export const ProposalHeader = ({ proposal, - networkParams, isListItem = true, voteState, }: { proposal: ProposalFieldsFragment | ProposalQuery['proposal']; - networkParams: Partial; isListItem?: boolean; voteState?: VoteState | null; }) => { @@ -146,7 +143,7 @@ export const ProposalHeader = ({ className="flex items-center gap-2" data-testid={`user-voted-${voteState.toLowerCase()}`} > -
+
@@ -185,7 +182,7 @@ export const ProposalHeader = ({
)} - + ); }; diff --git a/apps/governance/src/routes/proposals/components/proposal-votes-table/index.tsx b/apps/governance/src/routes/proposals/components/proposal-votes-table/index.tsx deleted file mode 100644 index cdae0a4e4..000000000 --- a/apps/governance/src/routes/proposals/components/proposal-votes-table/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ProposalVotesTable } from './proposal-votes-table'; diff --git a/apps/governance/src/routes/proposals/components/proposal-votes-table/proposal-votes-table.spec.tsx b/apps/governance/src/routes/proposals/components/proposal-votes-table/proposal-votes-table.spec.tsx deleted file mode 100644 index 6edce2761..000000000 --- a/apps/governance/src/routes/proposals/components/proposal-votes-table/proposal-votes-table.spec.tsx +++ /dev/null @@ -1,119 +0,0 @@ -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'; -import { ProposalType } from '../proposal/proposal'; -import { - generateNoVotes, - generateProposal, - generateYesVotes, -} from '../../test-helpers/generate-proposals'; - -const defaultProposal = generateProposal(); -const defaultProposalType = ProposalType.PROPOSAL_NETWORK_PARAMETER; -const updateMarketProposal = generateProposal({ - terms: { - change: { - __typename: 'UpdateMarket', - marketId: '12345', - }, - }, - votes: { - __typename: 'ProposalVotes', - yes: generateYesVotes(10), - no: generateNoVotes(0), - }, -}); -const updateMarketProposalType = ProposalType.PROPOSAL_UPDATE_MARKET; - -const renderComponent = ( - proposal = defaultProposal, - proposalType = defaultProposalType -) => - render( - - - - - - ); - -describe('Proposal Votes Table', () => { - it('should render successfully', () => { - const { baseElement } = renderComponent(); - expect(baseElement).toBeTruthy(); - }); - - 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(); - expect(screen.getByText('Tokens for proposal')).toBeInTheDocument(); - expect(screen.getByText('Total Supply')).toBeInTheDocument(); - expect(screen.getByText('Tokens against proposal')).toBeInTheDocument(); - expect(screen.getByText('Participation required')).toBeInTheDocument(); - expect(screen.getByText('Majority Required')).toBeInTheDocument(); - expect(screen.getByText('Number of voting parties')).toBeInTheDocument(); - expect(screen.getByText('Total tokens voted')).toBeInTheDocument(); - expect( - screen.getByText('Total tokens voted percentage') - ).toBeInTheDocument(); - expect(screen.getByText('Number of votes for')).toBeInTheDocument(); - expect(screen.getByText('Number of votes against')).toBeInTheDocument(); - expect(screen.getByText('Yes percentage')).toBeInTheDocument(); - expect(screen.getByText('No percentage')).toBeInTheDocument(); - expect(screen.queryByText('Liquidity majority met')).toBeNull(); - expect(screen.queryByText('Liquidity participation met')).toBeNull(); - expect(screen.queryByText('Liquidity shares for proposal')).toBeNull(); - }); - - 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 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(); - expect(screen.queryByText('Yes percentage')).toBeNull(); - expect(screen.queryByText('No percentage')).toBeNull(); - }); - - 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(); - }); - - it('displays if an update market proposal will pass by LP vote', () => { - renderComponent( - generateProposal({ - terms: { - change: { - __typename: 'UpdateMarket', - marketId: '12345', - }, - }, - votes: { - __typename: 'ProposalVotes', - yes: { - ...generateYesVotes(0, 1, '10'), - }, - no: { - ...generateNoVotes(0, 1, '0'), - }, - }, - }), - updateMarketProposalType - ); - fireEvent.click(screen.getByTestId('vote-breakdown-toggle')); - expect(screen.getByText('👍 by liquidity vote')).toBeInTheDocument(); - }); -}); diff --git a/apps/governance/src/routes/proposals/components/proposal-votes-table/proposal-votes-table.tsx b/apps/governance/src/routes/proposals/components/proposal-votes-table/proposal-votes-table.tsx deleted file mode 100644 index e1d2af14a..000000000 --- a/apps/governance/src/routes/proposals/components/proposal-votes-table/proposal-votes-table.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - KeyValueTable, - KeyValueTableRow, - Thumbs, - RoundedWrapper, -} from '@vegaprotocol/ui-toolkit'; -import { formatNumber, formatNumberPercentage } from '@vegaprotocol/utils'; -import { SubHeading } from '../../../../components/heading'; -import { useVoteInformation } from '../../hooks'; -import { useAppState } from '../../../../contexts/app-state/app-state-context'; -import { ProposalType } from '../proposal/proposal'; -import { CollapsibleToggle } from '../../../../components/collapsible-toggle'; -import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; -import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; - -interface ProposalVotesTableProps { - proposal: ProposalFieldsFragment | ProposalQuery['proposal']; - proposalType: ProposalType | null; -} - -export const ProposalVotesTable = ({ - proposal, - proposalType, -}: ProposalVotesTableProps) => { - const { t } = useTranslation(); - const { - appState: { totalSupply }, - } = useAppState(); - const [showDetails, setShowDetails] = useState(false); - const { - willPassByTokenVote, - willPassByLPVote, - totalTokensPercentage, - participationMet, - participationLPMet, - totalTokensVoted, - noPercentage, - yesPercentage, - noTokens, - yesTokens, - yesEquityLikeShareWeight, - yesVotes, - noVotes, - totalVotes, - requiredMajorityPercentage, - requiredParticipation, - majorityMet, - majorityLPMet, - } = useVoteInformation({ proposal }); - - const isUpdateMarket = proposalType === ProposalType.PROPOSAL_UPDATE_MARKET; - const updateMarketWillPass = willPassByTokenVote || willPassByLPVote; - const updateMarketVotePassMethod = willPassByTokenVote - ? t('byTokenVote') - : t('byLiquidityVote'); - - return ( - <> - - - - - {showDetails && ( - - - - {t('expectedToPass')} - {isUpdateMarket ? ( - updateMarketWillPass ? ( - - ) : ( - - ) - ) : willPassByTokenVote ? ( - - ) : ( - - )} - - - {t('majorityMet')} - {majorityMet ? : } - - {isUpdateMarket && ( - - {t('majorityLPMet')} - {majorityLPMet ? : } - - )} - - {t('participationMet')} - {participationMet ? : } - - {isUpdateMarket && ( - - {t('participationLPMet')} - {participationLPMet ? ( - - ) : ( - - )} - - )} - - {t('tokenForProposal')} - {formatNumber(yesTokens, 2)} - - {isUpdateMarket && ( - - {t('tokenLPForProposal')} - {formatNumber(yesEquityLikeShareWeight, 2)} - - )} - - {t('totalSupply')} - {formatNumber(totalSupply, 2)} - - - {t('tokensAgainstProposal')} - {formatNumber(noTokens, 2)} - - - {t('participationRequired')} - {formatNumberPercentage(requiredParticipation)} - - - {t('majorityRequired')} - {formatNumberPercentage(requiredMajorityPercentage)} - - {!isUpdateMarket && ( - <> - - {t('numberOfVotingParties')} - {formatNumber(totalVotes, 0)} - - - {t('totalTokensVotes')} - {formatNumber(totalTokensVoted, 2)} - - - {t('totalTokenVotedPercentage')} - {formatNumberPercentage(totalTokensPercentage, 2)} - - - {t('numberOfForVotes')} - {formatNumber(yesVotes, 0)} - - - {t('numberOfAgainstVotes')} - {formatNumber(noVotes, 0)} - - - {t('yesPercentage')} - {formatNumberPercentage(yesPercentage, 2)} - - - {t('noPercentage')} - {formatNumberPercentage(noPercentage, 2)} - - - )} - - - )} - - ); -}; 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 deleted file mode 100644 index bb9ad609b..000000000 --- a/apps/governance/src/routes/proposals/components/proposal-voting-status/index.tsx +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 0fee11edc..000000000 --- a/apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.spec.tsx +++ /dev/null @@ -1,153 +0,0 @@ -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 deleted file mode 100644 index a00600a9d..000000000 --- a/apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.tsx +++ /dev/null @@ -1,174 +0,0 @@ -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 db81de7a8..dceb58bd9 100644 --- a/apps/governance/src/routes/proposals/components/proposal/proposal.spec.tsx +++ b/apps/governance/src/routes/proposals/components/proposal/proposal.spec.tsx @@ -34,12 +34,6 @@ jest.mock('../proposal-change-table', () => ({ jest.mock('../proposal-json', () => ({ ProposalJson: () =>
, })); -jest.mock('../proposal-votes-table', () => ({ - ProposalVotesTable: () =>
, -})); -jest.mock('../vote-details', () => ({ - VoteDetails: () =>
, -})); jest.mock('../list-asset', () => ({ ListAsset: () =>
, })); @@ -104,8 +98,6 @@ it('renders each section', async () => { expect(await screen.findByTestId('proposal-header')).toBeInTheDocument(); expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument(); expect(screen.getByTestId('proposal-json')).toBeInTheDocument(); - expect(screen.getByTestId('proposal-votes-table')).toBeInTheDocument(); - expect(screen.getByTestId('proposal-vote-details')).toBeInTheDocument(); expect(screen.queryByTestId('proposal-list-asset')).not.toBeInTheDocument(); }); diff --git a/apps/governance/src/routes/proposals/components/proposal/proposal.tsx b/apps/governance/src/routes/proposals/components/proposal/proposal.tsx index 64790df1f..ebd3b0da7 100644 --- a/apps/governance/src/routes/proposals/components/proposal/proposal.tsx +++ b/apps/governance/src/routes/proposals/components/proposal/proposal.tsx @@ -5,9 +5,8 @@ import { ProposalHeader } from '../proposal-detail-header/proposal-header'; import { ProposalDescription } from '../proposal-description'; import { ProposalChangeTable } from '../proposal-change-table'; import { ProposalJson } from '../proposal-json'; -import { ProposalVotesTable } from '../proposal-votes-table'; import { ProposalAssetDetails } from '../proposal-asset-details'; -import { VoteDetails } from '../vote-details'; +import { UserVote } from '../vote-details'; import { ListAsset } from '../list-asset'; import Routes from '../../../routes'; import { ProposalMarketData } from '../proposal-market-data'; @@ -22,14 +21,6 @@ import type { NetworkParamsResult } from '@vegaprotocol/network-parameters'; import { useVoteSubmit } from '@vegaprotocol/proposals'; import { useUserVote } from '../vote-details/use-user-vote'; -export enum ProposalType { - PROPOSAL_NEW_MARKET = 'PROPOSAL_NEW_MARKET', - PROPOSAL_UPDATE_MARKET = 'PROPOSAL_UPDATE_MARKET', - PROPOSAL_NEW_ASSET = 'PROPOSAL_NEW_ASSET', - PROPOSAL_UPDATE_ASSET = 'PROPOSAL_UPDATE_ASSET', - PROPOSAL_NETWORK_PARAMETER = 'PROPOSAL_NETWORK_PARAMETER', - PROPOSAL_FREEFORM = 'PROPOSAL_FREEFORM', -} export interface ProposalProps { proposal: ProposalFieldsFragment | ProposalQuery['proposal']; networkParams: Partial; @@ -80,39 +71,32 @@ export const Proposal = ({ } let minVoterBalance = null; - let proposalType = null; if (networkParams) { switch (proposal.terms.change.__typename) { case 'NewMarket': minVoterBalance = networkParams.governance_proposal_market_minVoterBalance; - proposalType = ProposalType.PROPOSAL_NEW_MARKET; break; case 'UpdateMarket': minVoterBalance = networkParams.governance_proposal_updateMarket_minVoterBalance; - proposalType = ProposalType.PROPOSAL_UPDATE_MARKET; break; case 'NewAsset': minVoterBalance = networkParams.governance_proposal_asset_minVoterBalance; - proposalType = ProposalType.PROPOSAL_NEW_ASSET; break; case 'UpdateAsset': minVoterBalance = networkParams.governance_proposal_updateAsset_minVoterBalance; - proposalType = ProposalType.PROPOSAL_UPDATE_ASSET; break; case 'UpdateNetworkParameter': minVoterBalance = networkParams.governance_proposal_updateNetParam_minVoterBalance; - proposalType = ProposalType.PROPOSAL_NETWORK_PARAMETER; break; case 'NewFreeform': minVoterBalance = networkParams.governance_proposal_freeform_minVoterBalance; - proposalType = ProposalType.PROPOSAL_FREEFORM; break; } } @@ -140,91 +124,81 @@ export const Proposal = ({ -
-
- -
- - {proposal.terms.change.__typename === 'NewAsset' && - proposal.terms.change.source.__typename === 'ERC20' && - proposal.id ? ( - - ) : null} - -
- -
- - {newMarketData && ( -
- -
- )} - - {proposal.terms.change.__typename === 'UpdateMarket' && ( -
- -
- )} - - {(proposal.terms.change.__typename === 'NewAsset' || - proposal.terms.change.__typename === 'UpdateAsset') && - asset && ( -
- -
- )} - -
- -
+
+
-
-
- - - -
+ {proposal.terms.change.__typename === 'NewAsset' && + proposal.terms.change.source.__typename === 'ERC20' && + proposal.id ? ( + + ) : null} +
+ +
+ + {newMarketData && (
- +
+ )} + + {proposal.terms.change.__typename === 'UpdateMarket' && ( +
+ +
+ )} + + {(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 6092f66e5..bbc0edb46 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 @@ -6,11 +6,7 @@ import { MockedProvider } from '@apollo/client/testing'; import { render, screen } from '@testing-library/react'; import { format } from 'date-fns'; import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types'; -import { - generateNoVotes, - generateProposal, - generateYesVotes, -} from '../../test-helpers/generate-proposals'; +import { generateProposal } from '../../test-helpers/generate-proposals'; import { ProposalsListItemDetails } from './proposals-list-item-details'; import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats'; import { @@ -93,84 +89,6 @@ describe('Proposals list item details', () => { ); }); - it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', () => { - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - terms: { - change: { - __typename: 'UpdateMarket', - }, - }, - votes: { - yes: { - ...generateYesVotes(0), - totalEquityLikeShareWeight: '1000', - }, - no: { - ...generateNoVotes(0), - totalEquityLikeShareWeight: '0', - }, - }, - }) - ); - expect(screen.getByTestId('vote-status')).toHaveTextContent( - 'Currently expected to pass by LP vote' - ); - }); - - it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', () => { - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - terms: { - change: { - __typename: 'UpdateMarket', - }, - }, - votes: { - yes: { - ...generateYesVotes(1000, 1000), - totalEquityLikeShareWeight: '0', - }, - no: { - ...generateNoVotes(0), - totalEquityLikeShareWeight: '0', - }, - }, - }) - ); - expect(screen.getByTestId('vote-status')).toHaveTextContent( - 'Currently expected to pass by token vote' - ); - }); - - it('Renders proposal state: Update market proposal - Currently expected to fail', () => { - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - terms: { - change: { - __typename: 'UpdateMarket', - }, - }, - votes: { - yes: { - ...generateYesVotes(0), - totalEquityLikeShareWeight: '0', - }, - no: { - ...generateNoVotes(0), - totalEquityLikeShareWeight: '0', - }, - }, - }) - ); - expect(screen.getByTestId('vote-status')).toHaveTextContent( - 'Currently expected to fail' - ); - }); - it('Renders proposal state: Open - 5 minutes left to vote', () => { renderComponent( generateProposal({ @@ -213,43 +131,6 @@ describe('Proposals list item details', () => { ); }); - it('Renders proposal state: Open - majority not reached', () => { - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - terms: { - enactmentDatetime: nextWeek.toString(), - }, - votes: { - no: generateNoVotes(1, 1000000000000000000), - yes: generateYesVotes(1, 1000000000000000000), - }, - }) - ); - expect(screen.getByTestId('vote-status')).toHaveTextContent( - 'Currently expected to fail' - ); - }); - - it('Renders proposal state: Open - will pass', () => { - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - votes: { - __typename: 'ProposalVotes', - yes: generateYesVotes(3000, 1000000000000000000), - no: generateNoVotes(0), - }, - terms: { - closingDatetime: nextWeek.toString(), - }, - }) - ); - expect(screen.getByTestId('vote-status')).toHaveTextContent( - 'Currently expected to pass' - ); - }); - it('Renders proposal state: Rejected', () => { renderComponent( generateProposal({ 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 97ab568c4..4cb854c62 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 @@ -11,7 +11,6 @@ import { import Routes from '../../../routes'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; -import { useVoteInformation } from '../../hooks'; export const ProposalsListItemDetails = ({ proposal, @@ -20,18 +19,10 @@ export const ProposalsListItemDetails = ({ }) => { const { t } = useTranslation(); const state = proposal?.state; - const { willPassByTokenVote, willPassByLPVote } = useVoteInformation({ - proposal, - }); - 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; @@ -78,31 +69,11 @@ export const ProposalsListItemDetails = ({ } case ProposalState.STATE_OPEN: { voteDetails = ( - + {formatDistanceToNowStrict(new Date(proposal?.terms.closingDatetime))}{' '} {t('left to vote')} ); - voteStatus = - (isUpdateMarket && - (updateMarketWillPass ? ( - <> - {t('currentlySetTo')} {t('pass')} {updateMarketVotePassMethod} - - ) : ( - <> - {t('currentlySetTo')} {t('fail')} - - ))) || - (willPassByTokenVote ? ( - <> - {t('currentlySetTo')} {t('pass')} - - ) : ( - <> - {t('currentlySetTo')} {t('fail')} - - )); break; } case ProposalState.STATE_REJECTED: { 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 ba9664323..815aa3f3f 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 @@ -4,28 +4,19 @@ import { ProposalsListItemDetails } from './proposals-list-item-details'; import { useUserVote } from '../vote-details/use-user-vote'; 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, - networkParams, -}: ProposalsListItemProps) => { +export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => { const { voteState } = useUserVote(proposal?.id); - if (!proposal || !proposal.id || !networkParams) return null; + if (!proposal || !proposal.id) return null; return (
  • - +
  • 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 68484bcbc..4cfac6cc3 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,7 +8,6 @@ import { ProtocolUpgradeProposalsListItem } from '../protocol-upgrade-proposals- import { ProposalsListFilter } from '../proposals-list-filter'; import Routes from '../../../routes'; import { - AsyncRenderer, Button, Toggle, VegaIcon, @@ -20,10 +19,6 @@ 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; @@ -75,20 +70,6 @@ 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( @@ -153,181 +134,166 @@ export const ProposalsList = ({ p?.party?.id?.toString().includes(filterString); return ( - -
    -
    - +
    +
    + - {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 - ); - } - }} - /> + {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`)} + +

    - {sortedProposals.open.length > 0 || - sortedProtocolUpgradeProposals.open.length > 0 ? ( -
      - {filterString.length < 1 && - sortedProtocolUpgradeProposals.open.map((proposal) => ( - - ))} + {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.filter(filterPredicate).map((proposal) => ( - + + + {sortedProposals.open.length > 0 || + sortedProtocolUpgradeProposals.open.length > 0 ? ( +
        + {filterString.length < 1 && + sortedProtocolUpgradeProposals.open.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.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 + ) + } + />
    - ) - } +
    + ) + } -
      - {closedProposalsView === - ClosedProposalsViewOptions.NetworkUpgrades && ( -
      - {sortedProtocolUpgradeProposals.closed.map((proposal) => ( - + {closedProposalsView === + ClosedProposalsViewOptions.NetworkUpgrades && ( +
      + {sortedProtocolUpgradeProposals.closed.map((proposal) => ( + + ))} +
      + )} + + {closedProposalsView === + ClosedProposalsViewOptions.NetworkGovernance && ( +
      + {sortedProposals.closed + .filter(filterPredicate) + .map((proposal) => ( + ))} -
      - )} +
      + )} +
    + + ) : ( +

    + {t('noClosedProposals')} +

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

    - {t('noClosedProposals')} -

    - )} - - - - {t('seeRejectedProposals')} - -
    - + + {t('seeRejectedProposals')} + +
    ); }; 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 31ae42f45..0b128df9d 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,15 +1,10 @@ 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; @@ -17,19 +12,6 @@ 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 = ( @@ -39,11 +21,7 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => { p?.party?.id?.toString().includes(filterString); return ( - + <> { {proposals.length > 0 ? (
      {proposals.filter(filterPredicate).map((proposal) => ( - + ))}
    ) : ( @@ -66,6 +40,6 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {

    )} -
    + ); }; diff --git a/apps/governance/src/routes/proposals/components/vote-breakdown/index.ts b/apps/governance/src/routes/proposals/components/vote-breakdown/index.ts new file mode 100644 index 000000000..73b0db8e5 --- /dev/null +++ b/apps/governance/src/routes/proposals/components/vote-breakdown/index.ts @@ -0,0 +1 @@ +export * from './vote-breakdown'; diff --git a/apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.spec.tsx b/apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.spec.tsx new file mode 100644 index 000000000..623c81f67 --- /dev/null +++ b/apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.spec.tsx @@ -0,0 +1,348 @@ +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, +} from '../../test-helpers/mocks'; +import { VoteBreakdown } from './vote-breakdown'; +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('VoteBreakdown', () => { + 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('token-majority-met')).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('token-majority-not-met')).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('token-participation-met')).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('token-participation-not-met') + ).toBeInTheDocument(); + }); + + it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', () => { + renderComponent( + generateProposal({ + state: ProposalState.STATE_OPEN, + terms: { + change: { + __typename: 'UpdateMarket', + }, + }, + votes: { + yes: { + ...generateYesVotes(0), + totalEquityLikeShareWeight: '1000', + }, + no: { + ...generateNoVotes(0), + totalEquityLikeShareWeight: '0', + }, + }, + }) + ); + expect(screen.getByTestId('vote-status')).toHaveTextContent( + 'Currently expected to pass by liquidity vote' + ); + }); + + it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', () => { + renderComponent( + generateProposal({ + state: ProposalState.STATE_OPEN, + terms: { + change: { + __typename: 'UpdateMarket', + }, + }, + votes: { + yes: { + ...generateYesVotes(1000, fixedTokenValue), + totalEquityLikeShareWeight: '0', + }, + no: { + ...generateNoVotes(0, fixedTokenValue), + totalEquityLikeShareWeight: '0', + }, + }, + }) + ); + expect(screen.getByTestId('vote-status')).toHaveTextContent( + 'Currently expected to pass by token vote' + ); + }); + + it('Renders proposal state: Update market proposal - Currently expected to fail', () => { + renderComponent( + generateProposal({ + state: ProposalState.STATE_OPEN, + terms: { + change: { + __typename: 'UpdateMarket', + }, + }, + votes: { + yes: { + ...generateYesVotes(0), + totalEquityLikeShareWeight: '0', + }, + no: { + ...generateNoVotes(0), + totalEquityLikeShareWeight: '0', + }, + }, + }) + ); + expect(screen.getByTestId('vote-status')).toHaveTextContent( + 'Currently expected to fail' + ); + }); + + it('Progress bar displays status - token majority', () => { + const yesVotes = 80; + const noVotes = 20; + + renderComponent( + generateProposal({ + state: ProposalState.STATE_PASSED, + terms: { + closingDatetime: lastWeek.toString(), + enactmentDatetime: nextWeek.toString(), + }, + votes: { + __typename: 'ProposalVotes', + yes: generateYesVotes(yesVotes, fixedTokenValue), + no: generateNoVotes(noVotes, fixedTokenValue), + }, + }) + ); + + const element = screen.getByTestId('token-majority-progress'); + const style = window.getComputedStyle(element); + + expect(style.width).toBe(`${yesVotes}%`); + }); + + it('Progress bar displays status - token participation', () => { + const yesVotes = 40; + const noVotes = 20; + const totalVotes = yesVotes + noVotes; + const totalSupplyValue = mockTotalSupply.toNumber(); + const expectedProgress = (totalVotes / totalSupplyValue) * 100; // Here it should be 60% + + renderComponent( + generateProposal({ + state: ProposalState.STATE_PASSED, + terms: { + closingDatetime: lastWeek.toString(), + enactmentDatetime: nextWeek.toString(), + }, + votes: { + __typename: 'ProposalVotes', + yes: generateYesVotes(yesVotes, fixedTokenValue), + no: generateNoVotes(noVotes, fixedTokenValue), + }, + }) + ); + + const element = screen.getByTestId('token-participation-progress'); + const style = window.getComputedStyle(element); + + expect(style.width).toBe(`${expectedProgress}%`); + }); + + it('Progress bar displays status - LP majority', () => { + const yesVotesLP = 800; + const noVotesLP = 200; + const expectedProgress = (yesVotesLP / (yesVotesLP + noVotesLP)) * 100; // 80% + + renderComponent( + generateProposal({ + state: ProposalState.STATE_PASSED, + terms: { + change: { + __typename: 'UpdateMarket', + }, + }, + votes: { + __typename: 'ProposalVotes', + yes: { + ...generateYesVotes(0), + totalEquityLikeShareWeight: `${yesVotesLP}`, + }, + no: { + ...generateNoVotes(0), + totalEquityLikeShareWeight: `${noVotesLP}`, + }, + }, + }) + ); + + const element = screen.getByTestId('lp-majority-progress'); + const style = window.getComputedStyle(element); + expect(style.width).toBe(`${expectedProgress}%`); + }); + + it('Progress bar displays status - LP participation', () => { + const yesVotesLP = 400; + const noVotesLP = 600; + const totalVotesLP = yesVotesLP + noVotesLP; + const totalLPSupply = 1000; + const expectedProgress = (totalVotesLP / totalLPSupply) * 100; // 100% + + renderComponent( + generateProposal({ + state: ProposalState.STATE_PASSED, + terms: { + change: { + __typename: 'UpdateMarket', + }, + }, + votes: { + __typename: 'ProposalVotes', + yes: { + ...generateYesVotes(0), + totalEquityLikeShareWeight: `${yesVotesLP}`, + }, + no: { + ...generateNoVotes(0), + totalEquityLikeShareWeight: `${noVotesLP}`, + }, + }, + }) + ); + + const element = screen.getByTestId('lp-participation-progress'); + const style = window.getComputedStyle(element); + expect(style.width).toBe(`${expectedProgress}%`); + }); +}); diff --git a/apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.tsx b/apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.tsx new file mode 100644 index 000000000..6f0e36e00 --- /dev/null +++ b/apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.tsx @@ -0,0 +1,378 @@ +import classNames from 'classnames'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { useVoteInformation } from '../../hooks'; +import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit'; +import { formatNumber, toBigNum } from '@vegaprotocol/utils'; +import { ProposalState } from '@vegaprotocol/types'; +import type { ReactNode } from 'react'; +import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; +import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; + +interface VoteBreakdownProps { + proposal: ProposalFieldsFragment | ProposalQuery['proposal']; +} + +interface VoteProgressProps { + percentageFor: BigNumber; + colourfulBg?: boolean; + testId?: string; + children?: ReactNode; +} + +const VoteProgress = ({ + percentageFor, + colourfulBg, + testId, + children, +}: VoteProgressProps) => { + const containerClasses = classNames( + 'relative h-10 rounded-md border border-vega-dark-300 overflow-hidden', + colourfulBg ? 'bg-vega-pink' : 'bg-vega-dark-400' + ); + + const progressClasses = classNames( + 'absolute h-full top-0 left-0', + colourfulBg ? 'bg-vega-green' : 'bg-white' + ); + + const textClasses = classNames( + 'absolute top-0 left-0 w-full h-full flex items-center justify-start px-3 text-black' + ); + + return ( +
    +
    +
    {children}
    +
    + ); +}; + +interface StatusProps { + reached: boolean; + threshold: BigNumber; + text: string; + testId?: string; +} + +const Status = ({ reached, threshold, text, testId }: StatusProps) => { + const { t } = useTranslation(); + + return ( +
    + {reached ? ( +
    + + + {threshold.toString()}% {text} {t('met')} + +
    + ) : ( +
    + + + {threshold.toString()}% {text} {t('not met')} + +
    + )} +
    + ); +}; + +export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => { + const { + totalTokensPercentage, + participationMet, + totalTokensVoted, + totalLPTokensPercentage, + noPercentage, + noLPPercentage, + yesPercentage, + yesLPPercentage, + yesTokens, + noTokens, + yesEquityLikeShareWeight, + noEquityLikeShareWeight, + totalEquityLikeShareWeight, + requiredMajorityPercentage, + requiredMajorityLPPercentage, + requiredParticipation, + requiredParticipationLP, + participationLPMet, + majorityMet, + majorityLPMet, + willPassByTokenVote, + willPassByLPVote, + } = useVoteInformation({ proposal }); + + const { t } = useTranslation(); + const defaultDP = 2; + const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN; + const isUpdateMarket = proposal?.terms?.change?.__typename === 'UpdateMarket'; + const participationThresholdProgress = BigNumber.min( + totalTokensPercentage.dividedBy(requiredParticipation).multipliedBy(100), + new BigNumber(100) + ); + const lpParticipationThresholdProgress = + requiredParticipationLP && + BigNumber.min( + totalLPTokensPercentage + .dividedBy(requiredParticipationLP) + .multipliedBy(100), + new BigNumber(100) + ); + const willPass = willPassByTokenVote || willPassByLPVote; + const updateMarketVotePassMethod = willPassByTokenVote + ? t('byTokenVote') + : t('byLiquidityVote'); + + const sectionWrapperClasses = classNames('grid sm:grid-cols-2 gap-6'); + const headingClasses = classNames('mb-2 text-vega-dark-400'); + const progressDetailsClasses = classNames( + 'flex justify-between flex-wrap mt-2 text-sm' + ); + + return ( +
    + {isProposalOpen && ( +
    + + {willPass ? ( + + ) : ( + + )} + + {t('currentlySetTo')} + {willPass ? ( + + {t('pass')} + {isUpdateMarket && {updateMarketVotePassMethod}} + + ) : ( + {t('fail')} + )} +
    + )} + + {isUpdateMarket && ( +
    +

    {t('liquidityProviderVote')}

    +
    +
    + + + + +
    +
    + {t('liquidityProviderVotesFor')}: + + + + + ( + {yesLPPercentage.toFixed(defaultDP)}% + } + > + + + ) + +
    + +
    + {t('liquidityProviderVotesAgainst')}: + + + + + ( + {noLPPercentage.toFixed(defaultDP)}% + } + > + + + ) + +
    +
    +
    + +
    + + + + +
    +
    + {t('totalLiquidityProviderTokensVoted')}: + + + + + ({totalEquityLikeShareWeight.toFixed(defaultDP)}%) + +
    +
    +
    +
    +
    + )} + + {isUpdateMarket &&

    {t('tokenVote')}

    } +
    +
    + + + + +
    +
    + {t('tokenVotesFor')}: + + + + + ( + {yesPercentage.toFixed(defaultDP)}%} + > + + + ) + +
    + +
    + {t('tokenVotesAgainst')}: + + + + + ( + {noPercentage.toFixed(defaultDP)}%} + > + + + ) + +
    +
    +
    + +
    + + + + +
    +
    + {t('totalTokensVoted')}: + + + + ({totalTokensPercentage.toFixed(defaultDP)}%) +
    +
    +
    +
    +
    + ); +}; diff --git a/apps/governance/src/routes/proposals/components/vote-details/index.tsx b/apps/governance/src/routes/proposals/components/vote-details/index.tsx index b8d4db866..eb79e6910 100644 --- a/apps/governance/src/routes/proposals/components/vote-details/index.tsx +++ b/apps/governance/src/routes/proposals/components/vote-details/index.tsx @@ -1 +1 @@ -export { VoteDetails } from './vote-details'; +export { UserVote } from './user-vote'; diff --git a/apps/governance/src/routes/proposals/components/vote-details/user-vote.tsx b/apps/governance/src/routes/proposals/components/vote-details/user-vote.tsx new file mode 100644 index 000000000..70e764d18 --- /dev/null +++ b/apps/governance/src/routes/proposals/components/vote-details/user-vote.tsx @@ -0,0 +1,78 @@ +import { useTranslation } from 'react-i18next'; +import { Icon, ExternalLink } from '@vegaprotocol/ui-toolkit'; +import { useVegaWallet } from '@vegaprotocol/wallet'; +import { ProposalState } from '@vegaprotocol/types'; +import { ConnectToVega } from '../../../../components/connect-to-vega'; +import { VoteButtonsContainer } from './vote-buttons'; +import { SubHeading } from '../../../../components/heading'; +import type { VoteValue } from '@vegaprotocol/types'; +import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet'; +import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; +import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; +import type { VoteState } from './use-user-vote'; + +interface UserVoteProps { + proposal: ProposalFieldsFragment | ProposalQuery['proposal']; + minVoterBalance: string | null | undefined; + spamProtectionMinTokens: string | null | undefined; + transaction: VegaTxState | null; + submit: (voteValue: VoteValue, proposalId: string | null) => Promise; + dialog: (props: DialogProps) => JSX.Element; + voteState: VoteState | null; + voteDatetime: Date | null; +} + +export const UserVote = ({ + proposal, + minVoterBalance, + spamProtectionMinTokens, + submit, + transaction, + dialog, + voteState, + voteDatetime, +}: UserVoteProps) => { + const { pubKey } = useVegaWallet(); + + const { t } = useTranslation(); + + return ( +
    + {proposal?.state === ProposalState.STATE_OPEN ? ( + + ) : ( + + )} + + {pubKey ? ( + proposal && ( + + ) + ) : ( +
    +
    +
    + +
    {t('connectAVegaWalletToVote')}
    +
    + + {t('findOutMoreAboutHowToVote')} + +
    + +
    + )} +
    + ); +}; 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 7199c9365..911c2d41a 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 @@ -190,14 +190,15 @@ export const VoteButtons = ({ (voteState === VoteState.Yes || voteState === VoteState.No) && (

    {t('youVoted')}:{' '} - + {t(`voteState_${voteState}`)} {' '} {voteDatetime ? ( - {format(voteDatetime, DATE_FORMAT_LONG)}. + on {format(voteDatetime, DATE_FORMAT_LONG)}. ) : null} {proposalVotable ? ( { setChangeVote(true); 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 deleted file mode 100644 index b8fbb7c0a..000000000 --- a/apps/governance/src/routes/proposals/components/vote-details/vote-details.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { formatDistanceToNow } from 'date-fns'; -import { RoundedWrapper, Icon, ExternalLink } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet } from '@vegaprotocol/wallet'; -import { ProposalState } from '@vegaprotocol/types'; -import { VoteProgress } from '@vegaprotocol/proposals'; -import { formatNumber } from '../../../../lib/format-number'; -import { ConnectToVega } from '../../../../components/connect-to-vega'; -import { useVoteInformation } from '../../hooks'; -import { CurrentProposalStatus } from '../current-proposal-status'; -import { VoteButtonsContainer } from './vote-buttons'; -import { SubHeading } from '../../../../components/heading'; -import { ProposalType } from '../proposal/proposal'; -import type { VoteValue } from '@vegaprotocol/types'; -import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet'; -import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; -import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; -import type { VoteState } from './use-user-vote'; - -interface VoteDetailsProps { - proposal: ProposalFieldsFragment | ProposalQuery['proposal']; - minVoterBalance: string | null | undefined; - spamProtectionMinTokens: string | null | undefined; - proposalType: ProposalType | null; - transaction: VegaTxState | null; - submit: (voteValue: VoteValue, proposalId: string | null) => Promise; - dialog: (props: DialogProps) => JSX.Element; - voteState: VoteState | null; - voteDatetime: Date | null; -} - -export const VoteDetails = ({ - proposal, - minVoterBalance, - spamProtectionMinTokens, - proposalType, - submit, - transaction, - dialog, - voteState, - voteDatetime, -}: VoteDetailsProps) => { - const { pubKey } = useVegaWallet(); - const { - totalTokensPercentage, - participationMet, - totalTokensVoted, - totalLPTokensPercentage, - noPercentage, - noLPPercentage, - yesPercentage, - yesLPPercentage, - yesTokens, - noTokens, - requiredMajorityPercentage, - requiredMajorityLPPercentage, - requiredParticipation, - requiredParticipationLP, - participationLPMet, - } = useVoteInformation({ proposal }); - - const { t } = useTranslation(); - - const defaultDecimals = 2; - const daysLeft = t('daysLeft', { - daysLeft: formatDistanceToNow(new Date(proposal?.terms.closingDatetime)), - }); - - return ( - <> - {proposalType === ProposalType.PROPOSAL_UPDATE_MARKET && ( -

    - -

    - - - - {'. '} - {proposal?.state === ProposalState.STATE_OPEN ? daysLeft : null} -

    - - - - - - - - - - - - - - - -
    - {t('for')} - - - - {t('against')} -
    - {yesLPPercentage.toFixed(defaultDecimals)}% - - {t('majorityRequired')}{' '} - {requiredMajorityLPPercentage.toFixed(defaultDecimals)}% - - {noLPPercentage.toFixed(defaultDecimals)}% -
    - -

    - {t('participation')} - {': '} - {participationLPMet ? ( - {t('met')} - ) : ( - {t('notMet')} - )}{' '} - {formatNumber(totalLPTokensPercentage, defaultDecimals)}% - - {requiredParticipationLP && ( - <> - ({formatNumber(requiredParticipationLP, defaultDecimals)}%{' '} - {t('governanceRequired')}) - - )} - -

    -
    - )} -
    - -

    - - - - {'. '} - {proposal?.state === ProposalState.STATE_OPEN ? daysLeft : null} -

    - - - - - - - - - - - - - - - - - - - - -
    {t('for')} - - {t('against')}
    - {yesPercentage.toFixed(defaultDecimals)}% - - {t('majorityRequired')}{' '} - {requiredMajorityPercentage.toFixed(defaultDecimals)}% - - {noPercentage.toFixed(defaultDecimals)}% -
    - {' '} - {formatNumber(yesTokens, defaultDecimals)}{' '} - - {formatNumber(noTokens, defaultDecimals)} -
    -

    - {t('participation')} - {': '} - {participationMet ? ( - {t('met')} - ) : ( - {t('notMet')} - )}{' '} - {formatNumber(totalTokensVoted, defaultDecimals)}{' '} - {formatNumber(totalTokensPercentage, defaultDecimals)}% - - ({formatNumber(requiredParticipation, defaultDecimals)}%{' '} - {t('governanceRequired')}) - -

    - {proposalType === ProposalType.PROPOSAL_UPDATE_MARKET && ( -

    {t('votingThresholdInfo')}

    - )} - -
    - {proposal?.state === ProposalState.STATE_OPEN ? ( - - ) : ( - - )} - - {pubKey ? ( - proposal && ( - - ) - ) : ( - -
    -
    - -
    {t('connectAVegaWalletToVote')}
    -
    - - {t('findOutMoreAboutHowToVote')} - -
    - -
    - )} -
    -
    - - ); -}; diff --git a/libs/ui-toolkit/src/components/icon/blueprint-icons/icon.tsx b/libs/ui-toolkit/src/components/icon/blueprint-icons/icon.tsx index ca238b76a..5118d720f 100644 --- a/libs/ui-toolkit/src/components/icon/blueprint-icons/icon.tsx +++ b/libs/ui-toolkit/src/components/icon/blueprint-icons/icon.tsx @@ -7,7 +7,7 @@ export type { IconName } from '@blueprintjs/icons'; export interface IconProps { name: IconName; className?: string; - size?: 2 | 3 | 4 | 6 | 8 | 10 | 12 | 14 | 16; + size?: 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 14 | 16; ariaLabel?: string; } @@ -23,6 +23,7 @@ export const Icon = ({ size = 4, name, className, ariaLabel }: IconProps) => { 'w-2 h-2': size === 2, 'w-3 h-3': size === 3, 'w-4 h-4': size === 4, + 'w-5 h-5': size === 5, 'w-6 h-6': size === 6, 'w-8 h-8': size === 8, 'w-10 h-10': size === 10,