Feat/1012 rejected proposals page (#1040)

* Feat/1012: Rejected proposals page

* Feat/1012: Test for rejected proposals link on main proposals list

* Feat/1012: Translate 'see rejected proposals' link
This commit is contained in:
Sam Keen 2022-08-22 17:25:40 +01:00 committed by GitHub
parent b528cf08ed
commit 4a3b256456
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 204 additions and 0 deletions

View File

@ -16,6 +16,7 @@
"pageTitleDepositLp": "Deposit liquidity token for $VEGA rewards", "pageTitleDepositLp": "Deposit liquidity token for $VEGA rewards",
"pageTitleWithdrawLp": "Withdraw SLP and Rewards", "pageTitleWithdrawLp": "Withdraw SLP and Rewards",
"pageTitleRewards": "Rewards", "pageTitleRewards": "Rewards",
"pageTitleRejectedProposals": "Rejected proposals",
"Vesting": "Vesting", "Vesting": "Vesting",
"unstaked": "Unstaked", "unstaked": "Unstaked",
"of": "of", "of": "of",
@ -173,6 +174,7 @@
"closedProposals": "Closed proposals", "closedProposals": "Closed proposals",
"noOpenProposals": "There are no open or yet to enact proposals", "noOpenProposals": "There are no open or yet to enact proposals",
"noClosedProposals": "There are no enacted or rejected proposals", "noClosedProposals": "There are no enacted or rejected proposals",
"noRejectedProposals": "No rejected proposals",
"participationNotMet": "Participation not met", "participationNotMet": "Participation not met",
"majorityNotMet": "Majority not met", "majorityNotMet": "Majority not met",
"noProposals": "There are no active network change proposals", "noProposals": "There are no active network change proposals",
@ -483,6 +485,7 @@
"Switch to form for removal at end of epoch": "Switch to remove at end of epoch", "Switch to form for removal at end of epoch": "Switch to remove at end of epoch",
"Nominate a validator": "Nominate validator", "Nominate a validator": "Nominate validator",
"View Governance proposals": "View proposals", "View Governance proposals": "View proposals",
"seeRejectedProposals": "See rejected proposals",
"vegaWallet": "Vega Wallet", "vegaWallet": "Vega Wallet",
"rewardsComingSoon": "Rewards is coming soon", "rewardsComingSoon": "Rewards is coming soon",
"associationChoice": "You have $VEGA tokens held by the vesting contract. Would you like to associate those or associate $VEGA directly from your wallet?", "associationChoice": "You have $VEGA tokens held by the vesting contract. Would you like to associate those or associate $VEGA directly from your wallet?",

View File

@ -1 +1,2 @@
export { ProposalsList } from './proposals-list'; export { ProposalsList } from './proposals-list';
export { RejectedProposalsList } from './rejected-proposals-list';

View File

@ -110,6 +110,11 @@ describe('Proposals list', () => {
expect(screen.queryByTestId('proposals-list-filter')).toBeInTheDocument(); expect(screen.queryByTestId('proposals-list-filter')).toBeInTheDocument();
}); });
it('Will render a link to rejected proposals', () => {
render(renderComponent([]));
expect(screen.getByText('See rejected proposals')).toBeInTheDocument();
});
it('Places proposals correctly in open or closed lists', () => { it('Places proposals correctly in open or closed lists', () => {
render( render(
renderComponent([ renderComponent([

View File

@ -91,6 +91,10 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
</p> </p>
)} )}
</section> </section>
<Link className="underline" to={'/governance/rejected'}>
{t('seeRejectedProposals')}
</Link>
</> </>
); );
}; };

View File

@ -0,0 +1,78 @@
import { generateProposal } from '../../test-helpers/generate-proposals';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { BrowserRouter as Router } from 'react-router-dom';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { RejectedProposalsList } from './rejected-proposals-list';
import { ProposalState } from '@vegaprotocol/types';
import { render, screen, within } from '@testing-library/react';
import {
mockWalletContext,
networkParamsQueryMock,
nextWeek,
lastMonth,
} from '../../test-helpers/mocks';
import type { Proposals_proposals } from '../../proposals/__generated__/Proposals';
const rejectedProposalClosesNextWeek = generateProposal({
id: 'rejected1',
state: ProposalState.Open,
party: {
id: 'bvcx',
},
terms: {
closingDatetime: nextWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
});
const rejectedProposalClosedLastMonth = generateProposal({
id: 'rejected2',
state: ProposalState.Rejected,
terms: {
closingDatetime: lastMonth.toString(),
enactmentDatetime: lastMonth.toString(),
},
});
const renderComponent = (proposals: Proposals_proposals[]) => (
<Router>
<MockedProvider mocks={[networkParamsQueryMock]}>
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<RejectedProposalsList proposals={proposals} />
</VegaWalletContext.Provider>
</AppStateProvider>
</MockedProvider>
</Router>
);
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});
afterAll(() => {
jest.useRealTimers();
});
describe('Rejected proposals list', () => {
it('Renders a list of proposals', () => {
render(
renderComponent([
rejectedProposalClosedLastMonth,
rejectedProposalClosesNextWeek,
])
);
const rejectedProposals = within(screen.getByTestId('rejected-proposals'));
const rejectedProposalsItems = rejectedProposals.getAllByTestId(
'proposals-list-item'
);
expect(rejectedProposalsItems).toHaveLength(2);
});
it('Displays text when there are no proposals', () => {
render(renderComponent([]));
expect(screen.queryByTestId('rejected-proposals')).not.toBeInTheDocument();
expect(screen.getByTestId('no-rejected-proposals')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,41 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Heading } from '../../../../components/heading';
import { ProposalsListItem } from '../proposals-list-item';
import { ProposalsListFilter } from '../proposals-list-filter';
import type { Proposals_proposals } from '../../proposals/__generated__/Proposals';
interface ProposalsListProps {
proposals: Proposals_proposals[];
}
export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
const { t } = useTranslation();
const [filterString, setFilterString] = useState('');
const filterPredicate = (p: Proposals_proposals) =>
p.id?.includes(filterString) ||
p.party?.id?.toString().includes(filterString);
return (
<>
<Heading title={t('pageTitleRejectedProposals')} />
<ProposalsListFilter setFilterString={setFilterString} />
<section className="mx-[-20px] p-20">
{proposals.length > 0 ? (
<ul data-testid="rejected-proposals">
{proposals.filter(filterPredicate).map((proposal) => (
<ProposalsListItem key={proposal.id} proposal={proposal} />
))}
</ul>
) : (
<p className="mt-12 mb-0" data-testid="no-rejected-proposals">
{t('noRejectedProposals')}
</p>
)}
</section>
</>
);
};

View File

@ -0,0 +1,4 @@
export {
RejectedProposalsContainer,
RejectedProposalsContainer as default,
} from './rejected-proposals-container';

View File

@ -0,0 +1,60 @@
import { useQuery } from '@apollo/client';
import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
import compact from 'lodash/compact';
import filter from 'lodash/filter';
import flow from 'lodash/flow';
import orderBy from 'lodash/orderBy';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { PROPOSALS_QUERY } from '../proposals';
import { SplashLoader } from '../../../components/splash-loader';
import { RejectedProposalsList } from '../components/proposals-list';
import type { Proposals } from '../proposals/__generated__/Proposals';
export const RejectedProposalsContainer = () => {
const { t } = useTranslation();
const { data, loading, error } = useQuery<Proposals, never>(PROPOSALS_QUERY, {
pollInterval: 5000,
errorPolicy: 'ignore', // this is to get around some backend issues and should be removed in future
});
const proposals = React.useMemo(() => {
if (!data?.proposals?.length) {
return [];
}
return flow([
compact,
(arr) => filter(arr, ({ state }) => state === 'Rejected'),
(arr) =>
orderBy(
arr,
[
(p) => new Date(p.terms.enactmentDatetime).getTime(),
(p) => new Date(p.terms.closingDatetime).getTime(),
(p) => p.id,
],
['desc', 'desc', 'desc']
),
])(data.proposals);
}, [data]);
if (error) {
return (
<Callout intent={Intent.Danger} title={t('Something went wrong')}>
<pre>{error.message}</pre>
</Callout>
);
}
if (loading) {
return (
<Splash>
<SplashLoader />
</Splash>
);
}
return <RejectedProposalsList proposals={proposals} />;
};

View File

@ -115,6 +115,13 @@ const LazyGovernanceProposals = React.lazy(
) )
); );
const LazyRejectedGovernanceProposals = React.lazy(
() =>
import(
/* webpackChunkName: "route-governance-proposals", webpackPrefetch: true */ './governance/rejected'
)
);
const LazyGovernancePropose = React.lazy( const LazyGovernancePropose = React.lazy(
() => () =>
import( import(
@ -226,6 +233,7 @@ const routerConfig = [
children: [ children: [
{ path: ':proposalId', element: <LazyGovernanceProposal /> }, { path: ':proposalId', element: <LazyGovernanceProposal /> },
{ path: 'propose', element: <LazyGovernancePropose /> }, { path: 'propose', element: <LazyGovernancePropose /> },
{ path: 'rejected', element: <LazyRejectedGovernanceProposals /> },
{ index: true, element: <LazyGovernanceProposals /> }, { index: true, element: <LazyGovernanceProposals /> },
], ],
}, },