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:
parent
b528cf08ed
commit
4a3b256456
@ -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?",
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export { ProposalsList } from './proposals-list';
|
export { ProposalsList } from './proposals-list';
|
||||||
|
export { RejectedProposalsList } from './rejected-proposals-list';
|
||||||
|
@ -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([
|
||||||
|
@ -91,6 +91,10 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<Link className="underline" to={'/governance/rejected'}>
|
||||||
|
{t('seeRejectedProposals')}
|
||||||
|
</Link>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
4
apps/token/src/routes/governance/rejected/index.tsx
Normal file
4
apps/token/src/routes/governance/rejected/index.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export {
|
||||||
|
RejectedProposalsContainer,
|
||||||
|
RejectedProposalsContainer as default,
|
||||||
|
} from './rejected-proposals-container';
|
@ -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} />;
|
||||||
|
};
|
@ -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 /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user