From 5030bd1cfbdbca5a5c6089d544715018545f3fa8 Mon Sep 17 00:00:00 2001 From: Maxime Julian <44675210+therealemjy@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:30:17 +0200 Subject: [PATCH] feat: display executed payloads counts on proposal list (#3354) Co-authored-by: therealemjy --- .changeset/thin-hairs-vanish.md | 5 + apps/evm/src/__mocks__/models/proposals.ts | 5 +- apps/evm/src/clients/api/__mocks__/index.ts | 1 + .../__snapshots__/index.spec.ts.snap | 5 +- .../LabeledProgressCircle/index.tsx | 25 +++ .../src/components/ProgressCircle/index.tsx | 2 +- .../evm/src/components/ProposalCard/styles.ts | 4 +- apps/evm/src/components/index.ts | 1 + .../libs/translations/translations/en.json | 14 +- .../Status/Indicator/index.tsx | 43 +++++ .../GovernanceProposal/Status/index.tsx | 68 ++++++++ .../ProposalList/GovernanceProposal/index.tsx | 162 ++++++------------ .../ProposalList/GovernanceProposal/styles.ts | 50 ------ .../pages/Governance/ProposalList/index.tsx | 2 +- .../index.multichainGovernance.spec.tsx.snap | 3 + .../__snapshots__/index.spec.tsx.snap | 3 + ...ec.tsx => index.governanceSearch.spec.tsx} | 0 .../index.multichainGovernance.spec.tsx | 37 ++++ .../pages/Governance/__tests__/index.spec.tsx | 16 +- apps/evm/src/pages/Governance/testIds.ts | 1 + .../Commands/BscCommand/CurrentStep/index.tsx | 34 +--- .../NonBscCommand/CurrentStep/index.tsx | 10 +- .../Proposal/Commands/Progress/index.tsx | 19 +- .../evm/src/pages/Proposal/Commands/index.tsx | 4 +- .../useIsProposalCancelableByUser/index.tsx | 2 +- .../index.multichainGovernance.spec.tsx.snap | 2 +- .../index.multichainGovernance.spec.tsx | 23 ++- apps/evm/src/utilities/index.ts | 1 + 28 files changed, 316 insertions(+), 226 deletions(-) create mode 100644 .changeset/thin-hairs-vanish.md create mode 100644 apps/evm/src/components/LabeledProgressCircle/index.tsx create mode 100644 apps/evm/src/pages/Governance/ProposalList/GovernanceProposal/Status/Indicator/index.tsx create mode 100644 apps/evm/src/pages/Governance/ProposalList/GovernanceProposal/Status/index.tsx create mode 100644 apps/evm/src/pages/Governance/__tests__/__snapshots__/index.multichainGovernance.spec.tsx.snap create mode 100644 apps/evm/src/pages/Governance/__tests__/__snapshots__/index.spec.tsx.snap rename apps/evm/src/pages/Governance/__tests__/{indexSearch.spec.tsx => index.governanceSearch.spec.tsx} (100%) create mode 100644 apps/evm/src/pages/Governance/__tests__/index.multichainGovernance.spec.tsx diff --git a/.changeset/thin-hairs-vanish.md b/.changeset/thin-hairs-vanish.md new file mode 100644 index 0000000000..a621db9b46 --- /dev/null +++ b/.changeset/thin-hairs-vanish.md @@ -0,0 +1,5 @@ +--- +"@venusprotocol/evm": minor +--- + +display executed payloads counts on proposal list diff --git a/apps/evm/src/__mocks__/models/proposals.ts b/apps/evm/src/__mocks__/models/proposals.ts index efd7eff0ed..acfda0763b 100644 --- a/apps/evm/src/__mocks__/models/proposals.ts +++ b/apps/evm/src/__mocks__/models/proposals.ts @@ -15,11 +15,14 @@ export const proposals: Proposal[] = [ }, endBlock: 33499859, endDate: new Date('2023-09-20T07:54:35.000Z'), + queuedDate: new Date('2023-09-20T09:54:35.000Z'), + executionEtaDate: new Date('2023-09-21T06:54:35.000Z'), + executedDate: new Date('2023-09-21T07:54:35.000Z'), forVotesMantissa: new BigNumber('605461000000000000000000'), proposalId: 98, proposerAddress: '0x2ce1d0ffd7e869d9df33e28552b12ddded326706', startDate: new Date('2023-09-20T07:47:05.000Z'), - state: ProposalState.Defeated, + state: ProposalState.Executed, createdTxHash: '0xb8a70919dbf83e5c63af8efbad418b2a81ca9f4937b12f806482581abaf03b65', totalVotesMantissa: new BigNumber('605461000000000000000000'), proposalActions: [], diff --git a/apps/evm/src/clients/api/__mocks__/index.ts b/apps/evm/src/clients/api/__mocks__/index.ts index 98d2959db4..a2e8eaec36 100644 --- a/apps/evm/src/clients/api/__mocks__/index.ts +++ b/apps/evm/src/clients/api/__mocks__/index.ts @@ -317,6 +317,7 @@ export const useGetCurrentVotes = vi.fn(() => export const getProposals = vi.fn(async () => ({ proposals, + total: 100, })); export const useGetProposals = vi.fn(() => useQuery({ diff --git a/apps/evm/src/clients/api/queries/getProposal/useGetCachedProposal/__tests__/__snapshots__/index.spec.ts.snap b/apps/evm/src/clients/api/queries/getProposal/useGetCachedProposal/__tests__/__snapshots__/index.spec.ts.snap index 4fd8db452f..80d3ce9b22 100644 --- a/apps/evm/src/clients/api/queries/getProposal/useGetCachedProposal/__tests__/__snapshots__/index.spec.ts.snap +++ b/apps/evm/src/clients/api/queries/getProposal/useGetCachedProposal/__tests__/__snapshots__/index.spec.ts.snap @@ -15,6 +15,8 @@ exports[`useGetCachedProposal > returns proposal from cache when it exists 1`] = }, "endBlock": 33499859, "endDate": 2023-09-20T07:54:35.000Z, + "executedDate": 2023-09-21T07:54:35.000Z, + "executionEtaDate": 2023-09-21T06:54:35.000Z, "forVotes": [ { "address": "0x2ce1d0ffd7e869d9df33e28552b12ddded326706", @@ -29,6 +31,7 @@ exports[`useGetCachedProposal > returns proposal from cache when it exists 1`] = "proposalId": 98, "proposalType": 0, "proposerAddress": "0x2ce1d0ffd7e869d9df33e28552b12ddded326706", + "queuedDate": 2023-09-20T09:54:35.000Z, "remoteProposals": [ { "bridgedDate": 2023-02-01T00:00:00.000Z, @@ -183,7 +186,7 @@ exports[`useGetCachedProposal > returns proposal from cache when it exists 1`] = }, ], "startDate": 2023-09-20T07:47:05.000Z, - "state": 3, + "state": 7, "totalVotesMantissa": "6.05461e+23", } `; diff --git a/apps/evm/src/components/LabeledProgressCircle/index.tsx b/apps/evm/src/components/LabeledProgressCircle/index.tsx new file mode 100644 index 0000000000..8b49c9f4c5 --- /dev/null +++ b/apps/evm/src/components/LabeledProgressCircle/index.tsx @@ -0,0 +1,25 @@ +import { cn } from 'utilities'; +import { ProgressCircle } from '../ProgressCircle'; + +export interface LabeledProgressCircleProps extends React.HTMLAttributes { + value: number; + total: number; +} + +export const LabeledProgressCircle: React.FC = ({ + className, + value, + total, + ...otherProps +}) => ( +
+ + +

+ {value}/{total} +

+
+); diff --git a/apps/evm/src/components/ProgressCircle/index.tsx b/apps/evm/src/components/ProgressCircle/index.tsx index 13af2b6089..17b3442fb2 100644 --- a/apps/evm/src/components/ProgressCircle/index.tsx +++ b/apps/evm/src/components/ProgressCircle/index.tsx @@ -18,7 +18,7 @@ export const ProgressCircle: React.FC = ({ }) => { const theme = useTheme(); - const strokeWidthPx = size === 'sm' ? 3 : 4; + const strokeWidthPx = size === 'sm' ? 3 : 6; const sizePx = size === 'sm' ? 16 : 50; const { circumference, offset } = useMemo(() => { diff --git a/apps/evm/src/components/ProposalCard/styles.ts b/apps/evm/src/components/ProposalCard/styles.ts index b4d7f4446e..07d3958707 100644 --- a/apps/evm/src/components/ProposalCard/styles.ts +++ b/apps/evm/src/components/ProposalCard/styles.ts @@ -57,12 +57,12 @@ export const useStyles = () => { flex-direction: column; align-items: center; justify-content: center; + ${theme.breakpoints.down('sm')} { flex-direction: row; border-left: none; border-top: 1px solid ${theme.palette.secondary.light}; - padding-top: ${theme.spacing(10)}; - padding-bottom: ${theme.spacing(10)}; + padding: ${theme.spacing(6, 0)}; } `, }; diff --git a/apps/evm/src/components/index.ts b/apps/evm/src/components/index.ts index d30b7f3a49..a9aca96fcb 100644 --- a/apps/evm/src/components/index.ts +++ b/apps/evm/src/components/index.ts @@ -19,6 +19,7 @@ export * from './Notice'; export * from './Pagination'; export * from './ProgressBar'; export * from './ProgressCircle'; +export * from './LabeledProgressCircle'; export * from './ProposalCard'; export * from './Card'; export * from './ProgressBar/AccountHealth'; diff --git a/apps/evm/src/libs/translations/translations/en.json b/apps/evm/src/libs/translations/translations/en.json index d7af3636bb..0ebfc24482 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -659,6 +659,7 @@ }, "proposalState": { "active": "Active", + "bridged": "Bridged", "canceled": "Canceled", "created": "Created", "defeated": "Defeated", @@ -1109,14 +1110,7 @@ "title": "Operations" }, "status": { - "active": "Active", - "bridged": "Bridged", - "canceled": "Canceled", - "defeated": "Defeated", - "executed": "Executed", - "expired": "Expired", - "pending": "Pending", - "queued": "Queued" + "pending": "Pending" } }, "commands": { @@ -1151,6 +1145,10 @@ }, "queueButtonLabel": "Queue", "queuedUntilDate": "Queued until: {{ date, dd MMM yyyy h:mm a }}", + "status": { + "executedPayloads": "Executed payloads", + "readyForExecution": "Ready for execution" + }, "statusCard": { "ariaLabelAbstain": "votes abstain", "ariaLabelAgainst": "votes against", diff --git a/apps/evm/src/pages/Governance/ProposalList/GovernanceProposal/Status/Indicator/index.tsx b/apps/evm/src/pages/Governance/ProposalList/GovernanceProposal/Status/Indicator/index.tsx new file mode 100644 index 0000000000..b1577b9de3 --- /dev/null +++ b/apps/evm/src/pages/Governance/ProposalList/GovernanceProposal/Status/Indicator/index.tsx @@ -0,0 +1,43 @@ +import { Icon, type IconName } from 'components'; +import { useMemo } from 'react'; +import { type Proposal, ProposalState } from 'types'; +import { cn } from 'utilities'; + +export type IndicatorProps = React.HTMLAttributes & Pick; + +export const Indicator: React.FC = ({ state, className, ...otherProps }) => { + const [colorClass, iconName] = useMemo<[string, IconName]>(() => { + let tmpColorClass = 'bg-grey'; + let tmpIconName: IconName = 'dots'; + + if (state === ProposalState.Executed) { + tmpColorClass = 'bg-green'; + tmpIconName = 'mark'; + } else if (state === ProposalState.Succeeded) { + tmpColorClass = 'bg-orange'; + tmpIconName = 'exclamation'; + } else if ( + state === ProposalState.Defeated || + state === ProposalState.Expired || + state === ProposalState.Canceled + ) { + tmpColorClass = 'bg-red'; + tmpIconName = 'close'; + } + + return [tmpColorClass, tmpIconName]; + }, [state]); + + return ( +
+ +
+ ); +}; diff --git a/apps/evm/src/pages/Governance/ProposalList/GovernanceProposal/Status/index.tsx b/apps/evm/src/pages/Governance/ProposalList/GovernanceProposal/Status/index.tsx new file mode 100644 index 0000000000..58431db8da --- /dev/null +++ b/apps/evm/src/pages/Governance/ProposalList/GovernanceProposal/Status/index.tsx @@ -0,0 +1,68 @@ +import { LabeledProgressCircle } from 'components'; +import { useIsFeatureEnabled } from 'hooks/useIsFeatureEnabled'; +import { useTranslation } from 'libs/translations'; +import { useMemo } from 'react'; +import { ProposalState } from 'types'; +import { cn, getProposalStateLabel } from 'utilities'; +import { Indicator } from './Indicator'; + +export interface StatusProps extends React.HTMLAttributes { + state: ProposalState; + totalPayloadsCount: number; + executedPayloadsCount: number; +} + +export const Status: React.FC = ({ + state, + totalPayloadsCount, + executedPayloadsCount, + className, + ...otherProps +}) => { + const { t } = useTranslation(); + const isMultichainGovernanceFeatureEnabled = useIsFeatureEnabled({ + name: 'multichainGovernance', + }); + + const isFullyExecuted = executedPayloadsCount === totalPayloadsCount; + const shouldShowExecutedPayloadsStatus = + isMultichainGovernanceFeatureEnabled && state === ProposalState.Executed && !isFullyExecuted; + + const label = useMemo(() => { + if (shouldShowExecutedPayloadsStatus) { + return t('voteProposalUi.status.executedPayloads'); + } + + if (state === ProposalState.Succeeded) { + return t('voteProposalUi.status.readyForExecution'); + } + + return getProposalStateLabel({ state }); + }, [t, state, shouldShowExecutedPayloadsStatus]); + + return ( +
+
+ {shouldShowExecutedPayloadsStatus ? ( + + ) : ( + + )} +
+ +

+ {label} +

+
+ ); +}; diff --git a/apps/evm/src/pages/Governance/ProposalList/GovernanceProposal/index.tsx b/apps/evm/src/pages/Governance/ProposalList/GovernanceProposal/index.tsx index c04f40450d..d1e0d60b70 100644 --- a/apps/evm/src/pages/Governance/ProposalList/GovernanceProposal/index.tsx +++ b/apps/evm/src/pages/Governance/ProposalList/GovernanceProposal/index.tsx @@ -1,24 +1,23 @@ /** @jsxImportSource @emotion/react */ -import type { SerializedStyles } from '@emotion/react'; import Typography from '@mui/material/Typography'; import { BigNumber } from 'bignumber.js'; import { useMemo } from 'react'; -import { - ActiveVotingProgress, - Countdown, - Icon, - type IconName, - ProposalCard, - ProposalTypeChip, -} from 'components'; +import { ActiveVotingProgress, Countdown, ProposalCard, ProposalTypeChip } from 'components'; import { routes } from 'constants/routing'; import { useGetToken } from 'libs/tokens'; import { useTranslation } from 'libs/translations'; import { useAccountAddress } from 'libs/wallet'; -import { type Proposal, ProposalState, ProposalType, type Token, VoteSupport } from 'types'; -import { getProposalStateLabel } from 'utilities/getProposalStateLabel'; - +import { + type Proposal, + ProposalState, + ProposalType, + RemoteProposalState, + type Token, + VoteSupport, +} from 'types'; + +import { Status } from './Status'; import greenPulseAnimation from './greenPulseAnimation.gif'; import { useStyles } from './styles'; import TEST_IDS from './testIds'; @@ -31,93 +30,6 @@ import TEST_IDS from './testIds'; // t('voteProposalUi.defeatedDate') // t('voteProposalUi.expiredDate') -interface StateCard { - state: ProposalState | undefined; -} - -const StatusCard: React.FC = ({ state }) => { - const styles = useStyles(); - const { t } = useTranslation(); - - const statusContent: Record< - Exclude, - { - iconWrapperCss: SerializedStyles; - iconName: IconName; - iconCss?: SerializedStyles; - label: string; - } - > = useMemo(() => { - const label = - state !== undefined ? getProposalStateLabel({ state }) : t('proposalState.active'); - - return { - [ProposalState.Queued]: { - iconWrapperCss: styles.iconDotsWrapper, - iconName: 'dots', - label, - }, - [ProposalState.Pending]: { - iconWrapperCss: styles.iconDotsWrapper, - iconName: 'dots', - label, - }, - [ProposalState.Executed]: { - iconWrapperCss: styles.iconMarkWrapper, - iconName: 'mark', - iconCss: styles.iconCheck, - label, - }, - [ProposalState.Defeated]: { - iconWrapperCss: styles.iconCloseWrapper, - iconName: 'close', - label, - }, - [ProposalState.Succeeded]: { - iconWrapperCss: styles.iconInfoWrapper, - iconName: 'exclamation', - label, - }, - [ProposalState.Expired]: { - iconWrapperCss: styles.iconCloseWrapper, - iconName: 'close', - label, - }, - [ProposalState.Canceled]: { - iconWrapperCss: styles.iconCloseWrapper, - iconName: 'close', - label, - }, - }; - }, [ - t, - styles.iconCheck, - styles.iconCloseWrapper, - styles.iconDotsWrapper, - styles.iconInfoWrapper, - styles.iconMarkWrapper, - state, - ]); - - if (state !== undefined && state !== ProposalState.Active) { - return ( - <> -
- -
- - {statusContent[state].label} - - - ); - } - - return null; -}; - interface GovernanceProposalProps extends Proposal { className?: string; isUserConnected: boolean; @@ -129,6 +41,7 @@ const GovernanceProposalUi: React.FC = ({ proposalId, description, state, + remoteProposals, executedDate, executionEtaDate, cancelDate, @@ -183,6 +96,43 @@ const GovernanceProposalUi: React.FC = ({ } }, [state, cancelDate, executedDate, endDate, executionEtaDate, expiredDate]); + const contentRightItemDom = useMemo(() => { + if (state === ProposalState.Active) { + return ( + + ); + } + + const totalPayloadsCount = 1 + remoteProposals.length; // BSC proposal + remote proposals + const executedPayloadsCount = + (state === ProposalState.Executed ? 1 : 0) + + remoteProposals.filter( + remoteProposal => remoteProposal.state === RemoteProposalState.Executed, + ).length; + + return ( + + ); + }, [ + state, + remoteProposals, + forVotesMantissa, + againstVotesMantissa, + abstainedVotesMantissa, + votedTotalMantissa, + xvs, + ]); + return ( = ({ ) : undefined } title={description.title} - contentRightItem={ - state === ProposalState.Active ? ( - - ) : ( - - ) - } + contentRightItem={contentRightItemDom} footer={ statusDate && statusKey ? (
diff --git a/apps/evm/src/pages/Governance/ProposalList/GovernanceProposal/styles.ts b/apps/evm/src/pages/Governance/ProposalList/GovernanceProposal/styles.ts index eb134d4cb7..a2275c468b 100644 --- a/apps/evm/src/pages/Governance/ProposalList/GovernanceProposal/styles.ts +++ b/apps/evm/src/pages/Governance/ProposalList/GovernanceProposal/styles.ts @@ -5,56 +5,6 @@ export const useStyles = () => { const theme = useTheme(); return { - // /* StatusCard styles */ - statusText: css` - color: ${theme.palette.text.primary}; - text-transform: none; - margin-top: ${theme.spacing(2)}; - text-align: center; - ${theme.breakpoints.down('sm')} { - margin-top: 0; - margin-left: ${theme.spacing(3)}; - } - `, - - iconWrapper: css` - border-radius: 50%; - width: ${theme.shape.iconSize.xxLarge}px; - height: ${theme.shape.iconSize.xxLarge}px; - display: flex; - align-items: center; - justify-content: center; - ${theme.breakpoints.down('sm')} { - width: ${theme.shape.iconSize.xLarge}px; - height: ${theme.shape.iconSize.xLarge}px; - } - `, - iconDotsWrapper: css` - background-color: ${theme.palette.text.secondary}; - `, - iconInfoWrapper: css` - background-color: ${theme.palette.interactive.warning}; - `, - iconMarkWrapper: css` - background-color: ${theme.palette.interactive.success}; - `, - iconCloseWrapper: css` - background-color: ${theme.palette.interactive.error}; - `, - icon: css` - width: ${theme.spacing(6)}; - height: ${theme.spacing(6)}; - color: white; - ${theme.breakpoints.down('sm')} { - width: ${theme.spacing(4)}; - height: ${theme.spacing(4)}; - } - `, - iconCheck: css` - background-color: ${theme.palette.interactive.success}; - border-radius: 50%; - stroke-width: ${theme.spacing(0.5)}; - `, timestamp: css` display: flex; justify-content: space-between; diff --git a/apps/evm/src/pages/Governance/ProposalList/index.tsx b/apps/evm/src/pages/Governance/ProposalList/index.tsx index df98e669d8..8827f62a9b 100644 --- a/apps/evm/src/pages/Governance/ProposalList/index.tsx +++ b/apps/evm/src/pages/Governance/ProposalList/index.tsx @@ -178,7 +178,7 @@ const ProposalList: React.FC = ({ latestProposalStateData?.state !== 1; return ( -
+

{t('vote.proposals')}

diff --git a/apps/evm/src/pages/Governance/__tests__/__snapshots__/index.multichainGovernance.spec.tsx.snap b/apps/evm/src/pages/Governance/__tests__/__snapshots__/index.multichainGovernance.spec.tsx.snap new file mode 100644 index 0000000000..47f5e9563d --- /dev/null +++ b/apps/evm/src/pages/Governance/__tests__/__snapshots__/index.multichainGovernance.spec.tsx.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Governance > displays proposals with unexecuted payloads correctly 1`] = `"Proposals+ Create proposal#98Not votedVIP Comptroller Diamond proxyExecuted: 21 Sep 2023 7:54 AM3/7Executed payloads#97Not votedVIP Comptroller Diamond proxyActive until: 20 Sep 2023 7:54 AMFor605.46K XVSAgainst500K XVSAbstain500K XVS#96Not votedVIP Comptroller Diamond proxyCanceled: 19 Sep 2023 3:46 PMCanceled#95Not votedtestDefeated: 15 Sep 2023 10:23 AMDefeated#94Not voted123Ready for execution#93Not voted123QueuedItems 1 - 10 out of 1001234"`; diff --git a/apps/evm/src/pages/Governance/__tests__/__snapshots__/index.spec.tsx.snap b/apps/evm/src/pages/Governance/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 0000000000..8c003a968e --- /dev/null +++ b/apps/evm/src/pages/Governance/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Governance > displays proposals correctly 1`] = `"Proposals+ Create proposal#98Not votedVIP Comptroller Diamond proxyExecuted: 21 Sep 2023 7:54 AMExecuted#97Not votedVIP Comptroller Diamond proxyActive until: 20 Sep 2023 7:54 AMFor605.46K XVSAgainst500K XVSAbstain500K XVS#96Not votedVIP Comptroller Diamond proxyCanceled: 19 Sep 2023 3:46 PMCanceled#95Not votedtestDefeated: 15 Sep 2023 10:23 AMDefeated#94Not voted123Ready for execution#93Not voted123QueuedItems 1 - 10 out of 1001234"`; diff --git a/apps/evm/src/pages/Governance/__tests__/indexSearch.spec.tsx b/apps/evm/src/pages/Governance/__tests__/index.governanceSearch.spec.tsx similarity index 100% rename from apps/evm/src/pages/Governance/__tests__/indexSearch.spec.tsx rename to apps/evm/src/pages/Governance/__tests__/index.governanceSearch.spec.tsx diff --git a/apps/evm/src/pages/Governance/__tests__/index.multichainGovernance.spec.tsx b/apps/evm/src/pages/Governance/__tests__/index.multichainGovernance.spec.tsx new file mode 100644 index 0000000000..c816ac5722 --- /dev/null +++ b/apps/evm/src/pages/Governance/__tests__/index.multichainGovernance.spec.tsx @@ -0,0 +1,37 @@ +import { screen, waitFor } from '@testing-library/dom'; +import type Vi from 'vitest'; + +import fakeAccountAddress from '__mocks__/models/address'; +import { proposals } from '__mocks__/models/proposals'; +import { type UseIsFeatureEnabled, useIsFeatureEnabled } from 'hooks/useIsFeatureEnabled'; +import { renderComponent } from 'testUtils/render'; +import Governance from '..'; +import GOVERNANCE_PROPOSAL_TEST_IDS from '../ProposalList/GovernanceProposal/testIds'; +import TEST_IDS from '../testIds'; + +describe('Governance', () => { + beforeEach(() => { + (useIsFeatureEnabled as Vi.Mock).mockImplementation( + ({ name }: UseIsFeatureEnabled) => + name === 'voteProposal' || name === 'createProposal' || name === 'multichainGovernance', + ); + }); + + it('renders without crashing', async () => { + renderComponent(); + }); + + it('displays proposals with unexecuted payloads correctly', async () => { + renderComponent(, { + accountAddress: fakeAccountAddress, + }); + + // Wait for list to be displayed + const firstProposalId = proposals[0].proposalId.toString(); + await waitFor(async () => + screen.getByTestId(GOVERNANCE_PROPOSAL_TEST_IDS.governanceProposal(firstProposalId)), + ); + + expect(screen.getByTestId(TEST_IDS.proposalList).textContent).toMatchSnapshot(); + }); +}); diff --git a/apps/evm/src/pages/Governance/__tests__/index.spec.tsx b/apps/evm/src/pages/Governance/__tests__/index.spec.tsx index dc81889828..6a88c1b4f4 100644 --- a/apps/evm/src/pages/Governance/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/Governance/__tests__/index.spec.tsx @@ -1,4 +1,4 @@ -import { fireEvent, waitFor } from '@testing-library/react'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; import BigNumber from 'bignumber.js'; import _cloneDeep from 'lodash/cloneDeep'; import type Vi from 'vitest'; @@ -47,6 +47,20 @@ describe('Governance', () => { renderComponent(); }); + it('displays proposals correctly', async () => { + renderComponent(, { + accountAddress: fakeAccountAddress, + }); + + // Wait for list to be displayed + const firstProposalId = proposals[0].proposalId.toString(); + await waitFor(async () => + screen.getByTestId(GOVERNANCE_PROPOSAL_TEST_IDS.governanceProposal(firstProposalId)), + ); + + expect(screen.getByTestId(TEST_IDS.proposalList).textContent).toMatchSnapshot(); + }); + it('opens create proposal modal when clicking text if user has enough voting weight', async () => { (getProposalState as Vi.Mock).mockImplementation(async () => ({ state: 2 })); const { getByText } = renderComponent(, { diff --git a/apps/evm/src/pages/Governance/testIds.ts b/apps/evm/src/pages/Governance/testIds.ts index 3bd35c6d97..97de28d8f3 100644 --- a/apps/evm/src/pages/Governance/testIds.ts +++ b/apps/evm/src/pages/Governance/testIds.ts @@ -1,4 +1,5 @@ export default { createProposal: 'create-proposal', proposalStateSelect: 'proposal-state-select', + proposalList: 'proposal-list', }; diff --git a/apps/evm/src/pages/Proposal/Commands/BscCommand/CurrentStep/index.tsx b/apps/evm/src/pages/Proposal/Commands/BscCommand/CurrentStep/index.tsx index 6f23d8da23..7a0d8fb625 100644 --- a/apps/evm/src/pages/Proposal/Commands/BscCommand/CurrentStep/index.tsx +++ b/apps/evm/src/pages/Proposal/Commands/BscCommand/CurrentStep/index.tsx @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'libs/translations'; import { type Proposal, ProposalState } from 'types'; +import { getProposalStateLabel } from 'utilities'; import { Status, type StatusProps } from '../../Status'; export type CurrentStepProps = React.HTMLAttributes & @@ -34,38 +35,21 @@ export const CurrentStep: React.FC = ({ const [type, status] = useMemo<[StatusProps['type'], string]>(() => { let tmpType: StatusProps['type'] = 'info'; - let tmpStatus = t('voteProposalUi.command.status.pending'); - if (state === ProposalState.Canceled) { - tmpStatus = t('voteProposalUi.command.status.canceled'); - tmpType = 'error'; - } + const tmpStatus = getProposalStateLabel({ state }); - if (state === ProposalState.Defeated) { - tmpStatus = t('voteProposalUi.command.status.defeated'); + if ( + state === ProposalState.Canceled || + state === ProposalState.Defeated || + state === ProposalState.Expired + ) { tmpType = 'error'; - } - - if (state === ProposalState.Expired) { - tmpStatus = t('voteProposalUi.command.status.expired'); - tmpType = 'error'; - } - - if (state === ProposalState.Active) { - tmpStatus = t('voteProposalUi.command.status.active'); - } - - if (state === ProposalState.Queued) { - tmpStatus = t('voteProposalUi.command.status.queued'); - } - - if (state === ProposalState.Executed) { - tmpStatus = t('voteProposalUi.command.status.executed'); + } else if (state === ProposalState.Executed) { tmpType = 'success'; } return [tmpType, tmpStatus]; - }, [state, t]); + }, [state]); const previousStepDate = useMemo(() => { if (state === ProposalState.Pending) { diff --git a/apps/evm/src/pages/Proposal/Commands/NonBscCommand/CurrentStep/index.tsx b/apps/evm/src/pages/Proposal/Commands/NonBscCommand/CurrentStep/index.tsx index ee04b8b85f..eba0f7dd7c 100644 --- a/apps/evm/src/pages/Proposal/Commands/NonBscCommand/CurrentStep/index.tsx +++ b/apps/evm/src/pages/Proposal/Commands/NonBscCommand/CurrentStep/index.tsx @@ -35,25 +35,25 @@ export const CurrentStep: React.FC = ({ let tmpStatus = t('voteProposalUi.command.status.pending'); if (state === RemoteProposalState.Bridged) { - tmpStatus = t('voteProposalUi.command.status.bridged'); + tmpStatus = t('proposalState.bridged'); } if (state === RemoteProposalState.Canceled) { - tmpStatus = t('voteProposalUi.command.status.canceled'); + tmpStatus = t('proposalState.canceled'); tmpType = 'error'; } if (state === RemoteProposalState.Queued) { - tmpStatus = t('voteProposalUi.command.status.queued'); + tmpStatus = t('proposalState.queued'); } if (state === RemoteProposalState.Executed) { - tmpStatus = t('voteProposalUi.command.status.executed'); + tmpStatus = t('proposalState.executed'); tmpType = 'success'; } if (state === RemoteProposalState.Expired) { - tmpStatus = t('voteProposalUi.command.status.expired'); + tmpStatus = t('proposalState.expired'); tmpType = 'error'; } diff --git a/apps/evm/src/pages/Proposal/Commands/Progress/index.tsx b/apps/evm/src/pages/Proposal/Commands/Progress/index.tsx index 3eb8f07a53..911f1e6688 100644 --- a/apps/evm/src/pages/Proposal/Commands/Progress/index.tsx +++ b/apps/evm/src/pages/Proposal/Commands/Progress/index.tsx @@ -1,20 +1,20 @@ -import { Icon, ProgressCircle } from 'components'; +import { Icon, LabeledProgressCircle } from 'components'; import { useTranslation } from 'libs/translations'; import { cn } from 'utilities'; export interface ProgressProps extends React.HTMLAttributes { - successfulPayloadsCount: number; + executedPayloadsCount: number; totalPayloadsCount: number; } export const Progress: React.FC = ({ className, - successfulPayloadsCount, + executedPayloadsCount, totalPayloadsCount, ...otherProps }) => { const { t } = useTranslation(); - const isComplete = successfulPayloadsCount === totalPayloadsCount; + const isComplete = executedPayloadsCount === totalPayloadsCount; return (
@@ -27,16 +27,7 @@ export const Progress: React.FC = ({ {isComplete ? ( ) : ( -
- - -

- {successfulPayloadsCount}/{totalPayloadsCount} -

-
+ )}
); diff --git a/apps/evm/src/pages/Proposal/Commands/index.tsx b/apps/evm/src/pages/Proposal/Commands/index.tsx index 36ea7499ec..b153c0ebd1 100644 --- a/apps/evm/src/pages/Proposal/Commands/index.tsx +++ b/apps/evm/src/pages/Proposal/Commands/index.tsx @@ -17,7 +17,7 @@ export interface CommandsProps extends CardProps { export const Commands: React.FC = ({ proposal, ...otherProps }) => { const { t } = useTranslation(); - const [totalPayloadsCount, successfulPayloadsCount] = useMemo(() => { + const [totalPayloadsCount, executedPayloadsCount] = useMemo(() => { const totalCount = 1 + proposal.remoteProposals.length; // BSC proposal + Remote proposals let count = proposal.remoteProposals.reduce( @@ -38,7 +38,7 @@ export const Commands: React.FC = ({ proposal, ...otherProps }) =

{t('voteProposalUi.commands.title')}

diff --git a/apps/evm/src/pages/Proposal/Commands/useIsProposalCancelableByUser/index.tsx b/apps/evm/src/pages/Proposal/Commands/useIsProposalCancelableByUser/index.tsx index 318536319f..30fff083bf 100644 --- a/apps/evm/src/pages/Proposal/Commands/useIsProposalCancelableByUser/index.tsx +++ b/apps/evm/src/pages/Proposal/Commands/useIsProposalCancelableByUser/index.tsx @@ -32,7 +32,7 @@ export const useIsProposalCancelableByUser = ({ !proposalVotesMantissa || proposalVotesMantissa.isGreaterThanOrEqualTo(proposalThresholdMantissa); - const isCancelable = userIsProposer || !proposerHasEnoughVotingPower; + const isCancelable = hasCorrectState && (userIsProposer || !proposerHasEnoughVotingPower); return { isCancelable, diff --git a/apps/evm/src/pages/Proposal/__tests__/__snapshots__/index.multichainGovernance.spec.tsx.snap b/apps/evm/src/pages/Proposal/__tests__/__snapshots__/index.multichainGovernance.spec.tsx.snap index 6750ca14ab..3891fad9e0 100644 --- a/apps/evm/src/pages/Proposal/__tests__/__snapshots__/index.multichainGovernance.spec.tsx.snap +++ b/apps/evm/src/pages/Proposal/__tests__/__snapshots__/index.multichainGovernance.spec.tsx.snap @@ -10,7 +10,7 @@ exports[`ProposalUi page - Feature enabled: multichainGovernance > renders BSC c exports[`ProposalUi page - Feature enabled: multichainGovernance > renders BSC command correctly. Proposal state: 4 1`] = `"CommandsExecuted payloads0/1BNB testnetQueueQueue"`; -exports[`ProposalUi page - Feature enabled: multichainGovernance > renders BSC command correctly. Proposal state: 5 1`] = `"CommandsExecuted payloads0/1BNB testnetWaiting for queueing period to endQueuedExecutable at 15 Mar 2024 12:00 PM"`; +exports[`ProposalUi page - Feature enabled: multichainGovernance > renders BSC command correctly. Proposal state: 5 1`] = `"CommandsExecuted payloads0/1BNB testnetWaiting for queueing period to endQueued20 Sep 2023 09:54 AMExecutable at 15 Mar 2024 12:00 PM"`; exports[`ProposalUi page - Feature enabled: multichainGovernance > renders BSC command correctly. Proposal state: 5 2`] = `"CommandsExecuted payloads0/1BNB testnetExecuteExecute"`; diff --git a/apps/evm/src/pages/Proposal/__tests__/index.multichainGovernance.spec.tsx b/apps/evm/src/pages/Proposal/__tests__/index.multichainGovernance.spec.tsx index 4a00045d7e..1c0b28488a 100644 --- a/apps/evm/src/pages/Proposal/__tests__/index.multichainGovernance.spec.tsx +++ b/apps/evm/src/pages/Proposal/__tests__/index.multichainGovernance.spec.tsx @@ -261,7 +261,7 @@ describe('ProposalUi page - Feature enabled: multichainGovernance', () => { }); }); - it('does not allow user to cancel if voting power of the proposer is greater than or equals threshold', async () => { + it('does not let user cancel if the BSC proposal if voting power of the proposer is greater than or equals threshold', async () => { const cancelMock = vi.fn(); (useCancelProposal as Vi.Mock).mockImplementation(() => ({ mutateAsync: cancelMock, @@ -276,6 +276,27 @@ describe('ProposalUi page - Feature enabled: multichainGovernance', () => { ).not.toBeInTheDocument(); }); + it('does not let user cancel the BSC proposal if it has passed the succeeded state', async () => { + (useGetProposal as Vi.Mock).mockImplementation(() => ({ + data: { + proposal: succeededProposal, + }, + })); + + const cancelMock = vi.fn(); + (useCancelProposal as Vi.Mock).mockImplementation(() => ({ + mutateAsync: cancelMock, + })); + + renderComponent(, { + accountAddress: activeProposal.proposerAddress, + }); + + expect( + screen.queryByText(en.voteProposalUi.command.actionButton.cancel), + ).not.toBeInTheDocument(); + }); + it('lets user queue the BSC proposal', async () => { (useGetProposal as Vi.Mock).mockImplementation(() => ({ data: { diff --git a/apps/evm/src/utilities/index.ts b/apps/evm/src/utilities/index.ts index fd6080a3a8..6d386d867c 100755 --- a/apps/evm/src/utilities/index.ts +++ b/apps/evm/src/utilities/index.ts @@ -52,3 +52,4 @@ export * from './safeLazyLoad'; export * from './getProposalType'; export * from './getProposalState'; export * from './getUserVoteSupport'; +export * from './getProposalStateLabel';