feat(2034): governance home (#2299)

* feat(2034): quickly-assembled governance home

* feat(2034): tweaks

* feat(2034): tidied up new homepage

* feat(2034): e2e test tweak

* feat(2034): tweaks from PR comments and adjusted epoch progress bg-colour

* feat(2034): fix linting
This commit is contained in:
Sam Keen 2022-12-16 10:11:14 +00:00 committed by GitHub
parent 070f2a2045
commit 0a3509faad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
129 changed files with 526 additions and 260 deletions

View File

@ -18,7 +18,7 @@ context(
}); });
it('should have GOVERNANCE header visible', function () { it('should have GOVERNANCE header visible', function () {
cy.verify_page_header('Governance'); cy.verify_page_header('Proposals');
}); });
it('should be able to see a working link for - find out more about Vega governance', function () { it('should be able to see a working link for - find out more about Vega governance', function () {

View File

@ -3,24 +3,28 @@ import classNames from 'classnames';
interface HeadingProps { interface HeadingProps {
title?: string; title?: string;
centerContent?: boolean; centerContent?: boolean;
marginTop?: boolean;
marginBottom?: boolean; marginBottom?: boolean;
} }
export const Heading = ({ export const Heading = ({
title, title,
centerContent = true, centerContent = true,
marginTop = true,
marginBottom = true, marginBottom = true,
}: HeadingProps) => { }: HeadingProps) => {
if (!title) return null; if (!title) return null;
return ( return (
<header <header
className={classNames('mt-10 mb-6', { className={classNames('mb-6', {
'mx-auto': centerContent, 'mx-auto': centerContent,
'mt-10': marginTop,
})} })}
> >
<h1 <h1
className={classNames('font-alpha calt text-5xl', { className={classNames('font-alpha calt text-5xl', {
'mt-0': !marginTop,
'mb-0': !marginBottom, 'mb-0': !marginBottom,
})} })}
> >

View File

@ -16,7 +16,7 @@ export const ProgressBar = ({ value }: ProgressBarProps) => {
className="relative h-2 bg-neutral-600 rounded-full overflow-hidden" className="relative h-2 bg-neutral-600 rounded-full overflow-hidden"
> >
<div <div
className="rounded-full bg-clouds bg-vega-yellow h-full absolute transition-[width] ease-in-out" className="rounded-full bg-gradient-to-r from-[#4e05cf] to-vega-pink h-full absolute transition-[width] ease-in-out"
style={{ width }} style={{ width }}
/> />
</div> </div>

View File

@ -0,0 +1,24 @@
import type { ObservableQuery } from '@apollo/client';
import { useEffect } from 'react';
export const useRefreshValidators = (
epochExpiry: string | undefined,
refetch: ObservableQuery['refetch']
) => {
return useEffect(() => {
const epochInterval = setInterval(() => {
if (!epochExpiry) return;
const now = Date.now();
const expiry = new Date(epochExpiry).getTime();
if (now > expiry) {
refetch();
clearInterval(epochInterval);
}
}, 10000);
return () => {
clearInterval(epochInterval);
};
}, [refetch, epochExpiry]);
};

View File

@ -11,7 +11,7 @@
"pageTitle404": "Page not found", "pageTitle404": "Page not found",
"pageTitleNotPermitted": "Can not proceed!", "pageTitleNotPermitted": "Can not proceed!",
"pageTitleDisassociate": "Disassociate $VEGA tokens from a Vega key", "pageTitleDisassociate": "Disassociate $VEGA tokens from a Vega key",
"pageTitleGovernance": "Governance", "pageTitleProposals": "Proposals",
"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",
@ -530,6 +530,8 @@
"validatorTableIntro": "View the validator profile pitches and discussion", "validatorTableIntro": "View the validator profile pitches and discussion",
"onTheForum": "on the forum", "onTheForum": "on the forum",
"readMoreStaking": "Read more about staking on Vega", "readMoreStaking": "Read more about staking on Vega",
"readMoreGovernance": "Read more about Vega governance",
"readMoreValidators": "Read more about validators",
"networkDown": "This site is not currently connecting to the network please try again later.", "networkDown": "This site is not currently connecting to the network please try again later.",
"ethTransactionModalTitle": "Ethereum Transactions", "ethTransactionModalTitle": "Ethereum Transactions",
"confirmed": "Confirmed", "confirmed": "Confirmed",
@ -719,5 +721,13 @@
"Score": "Score", "Score": "Score",
"performancePenalty": "Performance penalty", "performancePenalty": "Performance penalty",
"overstaked": "Overstaked", "overstaked": "Overstaked",
"overstakedPenalty": "Overstaked penalty" "overstakedPenalty": "Overstaked penalty",
"homeProposalsIntro": "Decisions on the Vega network are on-chain, with tokenholders creating proposals that other tokenholders vote to approve or reject.",
"homeProposalsButtonText": "Browse, vote, and propose",
"homeValidatorsIntro": "Vega runs on a delegated proof of stake blockchain, where validators earn fees for validating block transactions. Tokenholders can nominate validators by staking tokens to them.",
"homeValidatorsButtonText": "Browse, and stake",
"homeRewardsIntro": "Track rewards you've earned for trading, liquidity provision, market creation, and staking.",
"homeRewardsButtonText": "See rewards",
"homeVegaTokenIntro": "VEGA Token is a governance asset used to make and vote on proposals, and nominate validators.",
"homeVegaTokenButtonText": "Manage tokens"
} }

View File

@ -1,147 +1,219 @@
import React from 'react'; import { useMemo } from 'react';
import { Trans, useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import {
import { Heading, SubHeading } from '../../components/heading'; AsyncRenderer,
import { ExternalLinks } from '@vegaprotocol/react-helpers'; Button,
import { useAppState } from '../../contexts/app-state/app-state-context'; ExternalLink,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { useDocumentTitle } from '../../hooks/use-document-title'; import { useDocumentTitle } from '../../hooks/use-document-title';
import type { RouteChildProps } from '..'; import { useRefreshValidators } from '../../hooks/use-refresh-validators';
import { ProposalsListItem } from '../proposals/components/proposals-list-item';
import Routes from '../routes'; import Routes from '../routes';
import { TokenDetails } from './token-details'; import {
import { Button } from '@vegaprotocol/ui-toolkit'; ExternalLinks,
import { toBigNum } from '@vegaprotocol/react-helpers'; removePaginationWrapper,
import { useNodeDataQuery } from './__generated___/NodeData'; } from '@vegaprotocol/react-helpers';
import { useNodesQuery } from '../staking/home/__generated___/Nodes';
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
import { getNotRejectedProposals } from '../proposals/proposals/proposals-container';
import { Heading } from '../../components/heading';
import * as Schema from '@vegaprotocol/types';
import type { RouteChildProps } from '..';
import type { ProposalFieldsFragment } from '../proposals/proposals/__generated__/Proposals';
import type { NodesFragmentFragment } from '../staking/home/__generated___/Nodes';
const Home = ({ name }: RouteChildProps) => { const nodesToShow = 6;
useDocumentTitle(name);
const HomeProposals = ({
proposals,
}: {
proposals: ProposalFieldsFragment[];
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { appState } = useAppState();
const { data } = useNodeDataQuery();
const totalAssociated = React.useMemo(() => {
return toBigNum(data?.nodeData?.stakedTotal || '0', appState.decimals);
}, [appState.decimals, data?.nodeData?.stakedTotal]);
return ( return (
<> <section className="mb-16" data-testid="home-proposals">
<Heading title={t('pageTitleHome')} /> <Heading title={t('Proposals')} />
<HomeSection> <h3 className="mb-6">{t('homeProposalsIntro')}</h3>
<TokenDetails <div className="flex items-center mb-8 gap-8">
totalSupply={appState.totalSupply} <Link to={`${Routes.PROPOSALS}`}>
totalAssociated={totalAssociated} <Button size="md">{t('homeProposalsButtonText')}</Button>
/>
</HomeSection>
<HomeSection>
<SubHeading title={t('Token Vesting')} />
<p>
{t(
'The vesting contract holds VEGA tokens until they have become unlocked.'
)}
</p>
<p>
<Trans
i18nKey="Tokens are held in different <trancheLink>Tranches</trancheLink>. Each tranche has its own schedule for how the tokens are unlocked."
components={{
trancheLink: (
<Link
data-testid="tranches-link"
to={Routes.TRANCHES}
className="underline text-white"
/>
),
}}
/>
</p>
<p>
{t(
'Once unlocked they can be redeemed from the contract so that you can transfer them between wallets.'
)}
</p>
<Link to={Routes.REDEEM}>
<Button
variant="primary"
size="md"
data-testid="check-vesting-page-btn"
>
{t('Check to see if you can redeem unlocked VEGA tokens')}
</Button>
</Link> </Link>
</HomeSection>
<HomeSection> <ExternalLink href={ExternalLinks.GOVERNANCE_PAGE}>
<SubHeading title={t('Use your Vega tokens')} /> {t(`readMoreGovernance`)}
<p> </ExternalLink>
{t(
'To use your tokens on the Vega network they need to be associated with a Vega wallet/key.'
)}
</p>
<p>
{t(
'This can happen both while held in the vesting contract as well as when redeemed.'
)}
</p>
<p>
<a
data-testid="get-vega-wallet-link"
href={ExternalLinks.VEGA_WALLET_URL}
className="underline text-white"
target="_blank"
rel="nofollow noreferrer"
>
{t('Get a Vega wallet')}
</a>
</p>
<p>
<Link
data-testid="associate-vega-tokens-link-on-homepage"
to={`${Routes.VALIDATORS}/associate`}
className="underline text-white"
>
{t('Associate VEGA tokens')}
</Link>
</p>
</HomeSection>
<div className="flex gap-12">
<div className="flex-1">
<HomeSection>
<SubHeading title={t('Staking')} />
<p>
{t(
'VEGA token holders can nominate a validator node and receive staking rewards.'
)}
</p>
<p>
<Link to={Routes.VALIDATORS}>
<Button size="md" data-testid="staking-button-on-homepage">
{t('Nominate a validator')}
</Button>
</Link>
</p>
</HomeSection>
</div>
<div className="flex-1">
<HomeSection>
<SubHeading title={t('Governance')} />
<p>
{t(
'VEGA token holders can vote on proposed changes to the network and create proposals.'
)}
</p>
<p>
<Link to={Routes.PROPOSALS}>
<Button size="md" data-testid="governance-button-on-homepage">
{t('View Governance proposals')}
</Button>
</Link>
</p>
</HomeSection>
</div>
</div> </div>
</> <ul data-testid="home-proposal-list">
{proposals.map((proposal) => (
<ProposalsListItem key={proposal.id} proposal={proposal} />
))}
</ul>
</section>
); );
}; };
export default Home; interface HomeNodesProps {
activeNodes: NodesFragmentFragment[];
consensusNodes: NodesFragmentFragment[];
trimmedActiveNodes: NodesFragmentFragment[];
}
export const HomeSection = ({ children }: { children: React.ReactNode }) => { const HomeNodes = ({
return <section className="mb-12">{children}</section>; activeNodes,
consensusNodes,
trimmedActiveNodes,
}: HomeNodesProps) => {
const { t } = useTranslation();
const highlightedNodeData = [
{ title: t('active nodes'), length: activeNodes.length },
{ title: t('consensus nodes'), length: consensusNodes.length },
];
return (
<section className="mb-12" data-testid="home-validators">
<Heading title={t('Validators')} />
<h3 className="mb-6">{t('homeValidatorsIntro')}</h3>
<div className="flex items-center mb-8 gap-8">
<Link to={Routes.STAKING}>
<Button size="md">{t('homeValidatorsButtonText')}</Button>
</Link>
<ExternalLink href={ExternalLinks.VALIDATOR_FORUM}>
{t(`readMoreValidators`)}
</ExternalLink>
</div>
<div className="grid grid-cols-[repeat(6,_1fr)] items-center gap-x-6 gap-y-2">
{highlightedNodeData.map(({ title, length }, index) => (
<div key={index} className="col-span-3">
<Link to={Routes.VALIDATORS}>
<RoundedWrapper paddingBottom={true}>
<div className="flex flex-col items-center m-[-1rem] px-4 py-6 hover:bg-neutral-800">
<span className="text-5xl">{length}</span>
<span className="text-sm uppercase text-neutral-400">
{title}
</span>
</div>
</RoundedWrapper>
</Link>
</div>
))}
{trimmedActiveNodes.map(({ id, avatarUrl, name }) => (
<div key={id} className="col-span-2">
<Link to={`${Routes.VALIDATORS}/${id}`}>
<RoundedWrapper paddingBottom={true} border={false}>
<div className="flex items-center justify-center m-[-1rem] p-4 bg-neutral-900 hover:bg-neutral-800">
{avatarUrl && (
<img
className="h-6 w-6 rounded-full mr-2"
src={avatarUrl}
alt={`Avatar icon for ${name}`}
/>
)}
<span className="text-sm">{name}</span>
</div>
</RoundedWrapper>
</Link>
</div>
))}
</div>
{activeNodes.length > nodesToShow && (
<Link to={Routes.STAKING}>
<span className="underline">
And {activeNodes.length - nodesToShow} more...
</span>
</Link>
)}
</section>
);
}; };
const GovernanceHome = ({ name }: RouteChildProps) => {
useDocumentTitle(name);
const { t } = useTranslation();
const {
data: proposalsData,
loading: proposalsLoading,
error: proposalsError,
} = useProposalsQuery({
pollInterval: 5000,
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
});
const {
data: validatorsData,
error: validatorsError,
loading: validatorsLoading,
refetch,
} = useNodesQuery();
useRefreshValidators(validatorsData?.epoch.timestamps.expiry, refetch);
const proposals = useMemo(
() =>
proposalsData
? getNotRejectedProposals<ProposalFieldsFragment>(
proposalsData.proposalsConnection
).slice(0, 3)
: [],
[proposalsData]
);
const activeNodes = removePaginationWrapper(
validatorsData?.nodesConnection.edges
);
const trimmedActiveNodes = activeNodes?.slice(0, nodesToShow);
const consensusNodes = activeNodes.filter(
(node) =>
node.rankingScore.status ===
Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT
);
return (
<AsyncRenderer
loading={proposalsLoading || validatorsLoading}
error={proposalsError || validatorsError}
data={proposalsData && validatorsData}
>
<HomeProposals proposals={proposals} />
<HomeNodes
activeNodes={activeNodes}
consensusNodes={consensusNodes}
trimmedActiveNodes={trimmedActiveNodes}
/>
<section className="grid grid-cols-2 gap-12 mb-16">
<div data-testid="home-rewards">
<Heading title={t('Rewards')} marginTop={false} />
<h3 className="mb-6">{t('homeRewardsIntro')}</h3>
<div className="flex items-center mb-8 gap-4">
<Link to={Routes.REWARDS}>
<Button size="md">{t('homeRewardsButtonText')}</Button>
</Link>
</div>
</div>
<div data-testid="home-vega-token">
<Heading title={t('VEGA Token')} marginTop={false} />
<h3 className="mb-6">{t('homeVegaTokenIntro')}</h3>
<div className="flex items-center mb-8 gap-4">
<Link to={Routes.WITHDRAWALS}>
<Button size="md">{t('homeVegaTokenButtonText')}</Button>
</Link>
</div>
</div>
</section>
</AsyncRenderer>
);
};
export default GovernanceHome;

View File

@ -39,6 +39,7 @@ describe('Proposal header', () => {
expect(screen.getByTestId('proposal-title')).toHaveTextContent( expect(screen.getByTestId('proposal-title')).toHaveTextContent(
'New some market' 'New some market'
); );
expect(screen.getByTestId('proposal-type')).toHaveTextContent('New market');
expect(screen.getByTestId('proposal-description')).toHaveTextContent( expect(screen.getByTestId('proposal-description')).toHaveTextContent(
'A new some market' 'A new some market'
); );
@ -66,6 +67,9 @@ describe('Proposal header', () => {
expect(screen.getByTestId('proposal-title')).toHaveTextContent( expect(screen.getByTestId('proposal-title')).toHaveTextContent(
'New market id' 'New market id'
); );
expect(screen.getByTestId('proposal-type')).toHaveTextContent(
'Update market'
);
expect( expect(
screen.queryByTestId('proposal-description') screen.queryByTestId('proposal-description')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
@ -99,6 +103,7 @@ describe('Proposal header', () => {
expect(screen.getByTestId('proposal-title')).toHaveTextContent( expect(screen.getByTestId('proposal-title')).toHaveTextContent(
'New asset: Fake currency' 'New asset: Fake currency'
); );
expect(screen.getByTestId('proposal-type')).toHaveTextContent('New asset');
expect(screen.getByTestId('proposal-details')).toHaveTextContent( expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'Symbol: FAKE. ERC20 0x0' 'Symbol: FAKE. ERC20 0x0'
); );
@ -125,6 +130,7 @@ describe('Proposal header', () => {
expect(screen.getByTestId('proposal-title')).toHaveTextContent( expect(screen.getByTestId('proposal-title')).toHaveTextContent(
'Unknown proposal' 'Unknown proposal'
); );
expect(screen.getByTestId('proposal-type')).toHaveTextContent('New asset');
expect(screen.getByTestId('proposal-details')).toHaveTextContent( expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'Symbol: BIA. Max faucet amount mint: 300' 'Symbol: BIA. Max faucet amount mint: 300'
); );
@ -153,6 +159,9 @@ describe('Proposal header', () => {
expect(screen.getByTestId('proposal-title')).toHaveTextContent( expect(screen.getByTestId('proposal-title')).toHaveTextContent(
'Network parameter' 'Network parameter'
); );
expect(screen.getByTestId('proposal-type')).toHaveTextContent(
'Network parameter'
);
expect(screen.getByTestId('proposal-details')).toHaveTextContent( expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'Network key to Network value' 'Network key to Network value'
); );
@ -175,6 +184,7 @@ describe('Proposal header', () => {
) )
); );
expect(screen.getByTestId('proposal-title')).toHaveTextContent('0x0'); expect(screen.getByTestId('proposal-title')).toHaveTextContent('0x0');
expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform');
expect( expect(
screen.queryByTestId('proposal-description') screen.queryByTestId('proposal-description')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
@ -202,6 +212,7 @@ describe('Proposal header', () => {
// For a rationale over 100 chars, we expect the header to be truncated at // For a rationale over 100 chars, we expect the header to be truncated at
// 100 chars with ellipsis and the details-one element to contain the rest. // 100 chars with ellipsis and the details-one element to contain the rest.
expect(screen.getByTestId('proposal-title')).toHaveTextContent('0x0'); expect(screen.getByTestId('proposal-title')).toHaveTextContent('0x0');
expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform');
expect(screen.getByTestId('proposal-description')).toHaveTextContent( expect(screen.getByTestId('proposal-description')).toHaveTextContent(
'Class aptent taciti sociosqu ad litora torquent per conubia' 'Class aptent taciti sociosqu ad litora torquent per conubia'
); );
@ -226,6 +237,7 @@ describe('Proposal header', () => {
) )
); );
expect(screen.getByTestId('proposal-title')).toHaveTextContent('freeform'); expect(screen.getByTestId('proposal-title')).toHaveTextContent('freeform');
expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform');
expect( expect(
screen.queryByTestId('proposal-description') screen.queryByTestId('proposal-description')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
@ -247,6 +259,9 @@ describe('Proposal header', () => {
}) })
) )
); );
expect(screen.getByTestId('proposal-type')).toHaveTextContent(
'Update asset'
);
expect(screen.getByTestId('proposal-details')).toHaveTextContent( expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'Update asset' 'Update asset'
); );

View File

@ -1,20 +1,23 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Lozenge } from '@vegaprotocol/ui-toolkit'; import { Intent, Lozenge } from '@vegaprotocol/ui-toolkit';
import { shorten } from '@vegaprotocol/react-helpers'; import { shorten } from '@vegaprotocol/react-helpers';
import { SubHeading } from '../../../../components/heading'; import { Heading, SubHeading } from '../../../../components/heading';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
export const ProposalHeader = ({ export const ProposalHeader = ({
proposal, proposal,
useSubHeading = true,
}: { }: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal']; proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
useSubHeading?: boolean;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const change = proposal?.terms.change; const change = proposal?.terms.change;
let details: ReactNode; let details: ReactNode;
let proposalType: ReactNode;
let title = proposal?.rationale.title.trim(); let title = proposal?.rationale.title.trim();
let description = proposal?.rationale.description.trim(); let description = proposal?.rationale.description.trim();
@ -27,6 +30,7 @@ export const ProposalHeader = ({
switch (change?.__typename) { switch (change?.__typename) {
case 'NewMarket': { case 'NewMarket': {
proposalType = t('NewMarket');
details = ( details = (
<> <>
{t('Code')}: {change.instrument.code}.{' '} {t('Code')}: {change.instrument.code}.{' '}
@ -45,10 +49,12 @@ export const ProposalHeader = ({
break; break;
} }
case 'UpdateMarket': { case 'UpdateMarket': {
proposalType = t('UpdateMarket');
details = `${t('Market change')}: ${change.marketId}`; details = `${t('Market change')}: ${change.marketId}`;
break; break;
} }
case 'NewAsset': { case 'NewAsset': {
proposalType = t('NewAsset');
details = ( details = (
<> <>
{t('Symbol')}: {change.symbol}.{' '} {t('Symbol')}: {change.symbol}.{' '}
@ -65,6 +71,7 @@ export const ProposalHeader = ({
break; break;
} }
case 'UpdateNetworkParameter': { case 'UpdateNetworkParameter': {
proposalType = t('NetworkParameter');
const parametersClasses = 'font-mono leading-none'; const parametersClasses = 'font-mono leading-none';
details = ( details = (
<> <>
@ -80,10 +87,12 @@ export const ProposalHeader = ({
break; break;
} }
case 'NewFreeform': { case 'NewFreeform': {
proposalType = t('Freeform');
details = `${t('FreeformProposal')}: ${proposal?.id}`; details = `${t('FreeformProposal')}: ${proposal?.id}`;
break; break;
} }
case 'UpdateAsset': { case 'UpdateAsset': {
proposalType = t('UpdateAsset');
details = ( details = (
<> <>
`${t('Update asset')}`; `${t('Update asset')}`;
@ -96,14 +105,28 @@ export const ProposalHeader = ({
return ( return (
<div className="text-sm mb-2"> <div className="text-sm mb-2">
<header data-testid="proposal-title"> <div data-testid="proposal-title">
<SubHeading title={titleContent || t('Unknown proposal')} /> {useSubHeading ? (
</header> <header>
{description && ( <SubHeading title={titleContent || t('Unknown proposal')} />
<div className="mb-4" data-testid="proposal-description"> </header>
{description} ) : (
</div> <Heading title={titleContent || t('Unknown proposal')} />
)} )}
</div>
<div className="flex items-center gap-2 mb-4">
{proposalType && (
<div data-testid="proposal-type">
<Lozenge variant={Intent.None}>{proposalType}</Lozenge>
</div>
)}
{description && (
<div data-testid="proposal-description">{description}</div>
)}
</div>
{details && <div data-testid="proposal-details">{details}</div>} {details && <div data-testid="proposal-details">{details}</div>}
</div> </div>
); );

View File

@ -74,7 +74,7 @@ export const Proposal = ({ proposal }: ProposalProps) => {
return ( return (
<AsyncRenderer data={params} loading={loading} error={error}> <AsyncRenderer data={params} loading={loading} error={error}>
<section data-testid="proposal"> <section data-testid="proposal">
<ProposalHeader proposal={proposal} /> <ProposalHeader proposal={proposal} useSubHeading={false} />
<div className="mb-10"> <div className="mb-10">
<ProposalChangeTable proposal={proposal} /> <ProposalChangeTable proposal={proposal} />
</div> </div>

View File

@ -192,7 +192,6 @@ describe('Proposals list item details', () => {
networkParamsQueryMock, networkParamsQueryMock,
createUserVoteQueryMock(proposal?.id, VoteValue.VALUE_YES), createUserVoteQueryMock(proposal?.id, VoteValue.VALUE_YES),
]); ]);
screen.debug();
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
expect(await screen.findByText('You voted')).toBeInTheDocument(); expect(await screen.findByText('You voted')).toBeInTheDocument();

View File

@ -1,3 +1,4 @@
import { RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { ProposalHeader } from '../proposal-detail-header/proposal-header'; import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import { ProposalsListItemDetails } from './proposals-list-item-details'; import { ProposalsListItemDetails } from './proposals-list-item-details';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
@ -11,13 +12,11 @@ export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
if (!proposal || !proposal.id) return null; if (!proposal || !proposal.id) return null;
return ( return (
<li <li id={proposal.id} data-testid="proposals-list-item">
className="py-4 border-b border-neutral-400 last:border-0" <RoundedWrapper paddingBottom={true}>
id={proposal.id} <ProposalHeader proposal={proposal} />
data-testid="proposals-list-item" <ProposalsListItemDetails proposal={proposal} />
> </RoundedWrapper>
<ProposalHeader proposal={proposal} />
<ProposalsListItemDetails proposal={proposal} />
</li> </li>
); );
}; };

View File

@ -81,7 +81,7 @@ afterAll(() => {
describe('Proposals list', () => { describe('Proposals list', () => {
it('Render a page title and link to the make proposal form', () => { it('Render a page title and link to the make proposal form', () => {
render(renderComponent([])); render(renderComponent([]));
expect(screen.getByText('Governance')).toBeInTheDocument(); expect(screen.getByText('Proposals')).toBeInTheDocument();
expect(screen.getByTestId('new-proposal-link')).toBeInTheDocument(); expect(screen.getByTestId('new-proposal-link')).toBeInTheDocument();
}); });

View File

@ -51,7 +51,7 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
<Heading <Heading
centerContent={false} centerContent={false}
marginBottom={false} marginBottom={false}
title={t('pageTitleGovernance')} title={t('pageTitleProposals')}
/> />
<Link <Link
className="xs:justify-self-end" className="xs:justify-self-end"

Some files were not shown because too many files have changed in this diff Show More