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:
parent
070f2a2045
commit
0a3509faad
@ -18,7 +18,7 @@ context(
|
||||
});
|
||||
|
||||
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 () {
|
||||
|
@ -3,24 +3,28 @@ import classNames from 'classnames';
|
||||
interface HeadingProps {
|
||||
title?: string;
|
||||
centerContent?: boolean;
|
||||
marginTop?: boolean;
|
||||
marginBottom?: boolean;
|
||||
}
|
||||
|
||||
export const Heading = ({
|
||||
title,
|
||||
centerContent = true,
|
||||
marginTop = true,
|
||||
marginBottom = true,
|
||||
}: HeadingProps) => {
|
||||
if (!title) return null;
|
||||
|
||||
return (
|
||||
<header
|
||||
className={classNames('mt-10 mb-6', {
|
||||
className={classNames('mb-6', {
|
||||
'mx-auto': centerContent,
|
||||
'mt-10': marginTop,
|
||||
})}
|
||||
>
|
||||
<h1
|
||||
className={classNames('font-alpha calt text-5xl', {
|
||||
'mt-0': !marginTop,
|
||||
'mb-0': !marginBottom,
|
||||
})}
|
||||
>
|
||||
|
@ -16,7 +16,7 @@ export const ProgressBar = ({ value }: ProgressBarProps) => {
|
||||
className="relative h-2 bg-neutral-600 rounded-full overflow-hidden"
|
||||
>
|
||||
<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 }}
|
||||
/>
|
||||
</div>
|
||||
|
24
apps/token/src/hooks/use-refresh-validators.ts
Normal file
24
apps/token/src/hooks/use-refresh-validators.ts
Normal 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]);
|
||||
};
|
@ -11,7 +11,7 @@
|
||||
"pageTitle404": "Page not found",
|
||||
"pageTitleNotPermitted": "Can not proceed!",
|
||||
"pageTitleDisassociate": "Disassociate $VEGA tokens from a Vega key",
|
||||
"pageTitleGovernance": "Governance",
|
||||
"pageTitleProposals": "Proposals",
|
||||
"pageTitleDepositLp": "Deposit liquidity token for $VEGA rewards",
|
||||
"pageTitleWithdrawLp": "Withdraw SLP and Rewards",
|
||||
"pageTitleRewards": "Rewards",
|
||||
@ -530,6 +530,8 @@
|
||||
"validatorTableIntro": "View the validator profile pitches and discussion",
|
||||
"onTheForum": "on the forum",
|
||||
"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.",
|
||||
"ethTransactionModalTitle": "Ethereum Transactions",
|
||||
"confirmed": "Confirmed",
|
||||
@ -719,5 +721,13 @@
|
||||
"Score": "Score",
|
||||
"performancePenalty": "Performance penalty",
|
||||
"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"
|
||||
}
|
||||
|
@ -1,147 +1,219 @@
|
||||
import React from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Heading, SubHeading } from '../../components/heading';
|
||||
import { ExternalLinks } from '@vegaprotocol/react-helpers';
|
||||
import { useAppState } from '../../contexts/app-state/app-state-context';
|
||||
import {
|
||||
AsyncRenderer,
|
||||
Button,
|
||||
ExternalLink,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
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 { TokenDetails } from './token-details';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { toBigNum } from '@vegaprotocol/react-helpers';
|
||||
import { useNodeDataQuery } from './__generated___/NodeData';
|
||||
import {
|
||||
ExternalLinks,
|
||||
removePaginationWrapper,
|
||||
} 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) => {
|
||||
useDocumentTitle(name);
|
||||
const nodesToShow = 6;
|
||||
|
||||
const HomeProposals = ({
|
||||
proposals,
|
||||
}: {
|
||||
proposals: ProposalFieldsFragment[];
|
||||
}) => {
|
||||
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 (
|
||||
<>
|
||||
<Heading title={t('pageTitleHome')} />
|
||||
<HomeSection>
|
||||
<TokenDetails
|
||||
totalSupply={appState.totalSupply}
|
||||
totalAssociated={totalAssociated}
|
||||
/>
|
||||
</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>
|
||||
<section className="mb-16" data-testid="home-proposals">
|
||||
<Heading title={t('Proposals')} />
|
||||
<h3 className="mb-6">{t('homeProposalsIntro')}</h3>
|
||||
<div className="flex items-center mb-8 gap-8">
|
||||
<Link to={`${Routes.PROPOSALS}`}>
|
||||
<Button size="md">{t('homeProposalsButtonText')}</Button>
|
||||
</Link>
|
||||
</HomeSection>
|
||||
<HomeSection>
|
||||
<SubHeading title={t('Use your Vega tokens')} />
|
||||
<p>
|
||||
{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>
|
||||
|
||||
<ExternalLink href={ExternalLinks.GOVERNANCE_PAGE}>
|
||||
{t(`readMoreGovernance`)}
|
||||
</ExternalLink>
|
||||
</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 }) => {
|
||||
return <section className="mb-12">{children}</section>;
|
||||
const HomeNodes = ({
|
||||
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;
|
||||
|
@ -39,6 +39,7 @@ describe('Proposal header', () => {
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||
'New some market'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent('New market');
|
||||
expect(screen.getByTestId('proposal-description')).toHaveTextContent(
|
||||
'A new some market'
|
||||
);
|
||||
@ -66,6 +67,9 @@ describe('Proposal header', () => {
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||
'New market id'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent(
|
||||
'Update market'
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId('proposal-description')
|
||||
).not.toBeInTheDocument();
|
||||
@ -99,6 +103,7 @@ describe('Proposal header', () => {
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||
'New asset: Fake currency'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent('New asset');
|
||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||
'Symbol: FAKE. ERC20 0x0'
|
||||
);
|
||||
@ -125,6 +130,7 @@ describe('Proposal header', () => {
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||
'Unknown proposal'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent('New asset');
|
||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||
'Symbol: BIA. Max faucet amount mint: 300'
|
||||
);
|
||||
@ -153,6 +159,9 @@ describe('Proposal header', () => {
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||
'Network parameter'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent(
|
||||
'Network parameter'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||
'Network key to Network value'
|
||||
);
|
||||
@ -175,6 +184,7 @@ describe('Proposal header', () => {
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent('0x0');
|
||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform');
|
||||
expect(
|
||||
screen.queryByTestId('proposal-description')
|
||||
).not.toBeInTheDocument();
|
||||
@ -202,6 +212,7 @@ describe('Proposal header', () => {
|
||||
// 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.
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent('0x0');
|
||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform');
|
||||
expect(screen.getByTestId('proposal-description')).toHaveTextContent(
|
||||
'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-type')).toHaveTextContent('Freeform');
|
||||
expect(
|
||||
screen.queryByTestId('proposal-description')
|
||||
).not.toBeInTheDocument();
|
||||
@ -247,6 +259,9 @@ describe('Proposal header', () => {
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent(
|
||||
'Update asset'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||
'Update asset'
|
||||
);
|
@ -1,20 +1,23 @@
|
||||
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 { SubHeading } from '../../../../components/heading';
|
||||
import { Heading, SubHeading } from '../../../../components/heading';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
|
||||
export const ProposalHeader = ({
|
||||
proposal,
|
||||
useSubHeading = true,
|
||||
}: {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
useSubHeading?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const change = proposal?.terms.change;
|
||||
|
||||
let details: ReactNode;
|
||||
let proposalType: ReactNode;
|
||||
|
||||
let title = proposal?.rationale.title.trim();
|
||||
let description = proposal?.rationale.description.trim();
|
||||
@ -27,6 +30,7 @@ export const ProposalHeader = ({
|
||||
|
||||
switch (change?.__typename) {
|
||||
case 'NewMarket': {
|
||||
proposalType = t('NewMarket');
|
||||
details = (
|
||||
<>
|
||||
{t('Code')}: {change.instrument.code}.{' '}
|
||||
@ -45,10 +49,12 @@ export const ProposalHeader = ({
|
||||
break;
|
||||
}
|
||||
case 'UpdateMarket': {
|
||||
proposalType = t('UpdateMarket');
|
||||
details = `${t('Market change')}: ${change.marketId}`;
|
||||
break;
|
||||
}
|
||||
case 'NewAsset': {
|
||||
proposalType = t('NewAsset');
|
||||
details = (
|
||||
<>
|
||||
{t('Symbol')}: {change.symbol}.{' '}
|
||||
@ -65,6 +71,7 @@ export const ProposalHeader = ({
|
||||
break;
|
||||
}
|
||||
case 'UpdateNetworkParameter': {
|
||||
proposalType = t('NetworkParameter');
|
||||
const parametersClasses = 'font-mono leading-none';
|
||||
details = (
|
||||
<>
|
||||
@ -80,10 +87,12 @@ export const ProposalHeader = ({
|
||||
break;
|
||||
}
|
||||
case 'NewFreeform': {
|
||||
proposalType = t('Freeform');
|
||||
details = `${t('FreeformProposal')}: ${proposal?.id}`;
|
||||
break;
|
||||
}
|
||||
case 'UpdateAsset': {
|
||||
proposalType = t('UpdateAsset');
|
||||
details = (
|
||||
<>
|
||||
`${t('Update asset')}`;
|
||||
@ -96,14 +105,28 @@ export const ProposalHeader = ({
|
||||
|
||||
return (
|
||||
<div className="text-sm mb-2">
|
||||
<header data-testid="proposal-title">
|
||||
<SubHeading title={titleContent || t('Unknown proposal')} />
|
||||
</header>
|
||||
{description && (
|
||||
<div className="mb-4" data-testid="proposal-description">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
<div data-testid="proposal-title">
|
||||
{useSubHeading ? (
|
||||
<header>
|
||||
<SubHeading title={titleContent || t('Unknown proposal')} />
|
||||
</header>
|
||||
) : (
|
||||
<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>}
|
||||
</div>
|
||||
);
|
@ -74,7 +74,7 @@ export const Proposal = ({ proposal }: ProposalProps) => {
|
||||
return (
|
||||
<AsyncRenderer data={params} loading={loading} error={error}>
|
||||
<section data-testid="proposal">
|
||||
<ProposalHeader proposal={proposal} />
|
||||
<ProposalHeader proposal={proposal} useSubHeading={false} />
|
||||
<div className="mb-10">
|
||||
<ProposalChangeTable proposal={proposal} />
|
||||
</div>
|
@ -192,7 +192,6 @@ describe('Proposals list item details', () => {
|
||||
networkParamsQueryMock,
|
||||
createUserVoteQueryMock(proposal?.id, VoteValue.VALUE_YES),
|
||||
]);
|
||||
screen.debug();
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
|
||||
expect(await screen.findByText('You voted')).toBeInTheDocument();
|
@ -1,3 +1,4 @@
|
||||
import { RoundedWrapper } from '@vegaprotocol/ui-toolkit';
|
||||
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
|
||||
import { ProposalsListItemDetails } from './proposals-list-item-details';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
@ -11,13 +12,11 @@ export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
|
||||
if (!proposal || !proposal.id) return null;
|
||||
|
||||
return (
|
||||
<li
|
||||
className="py-4 border-b border-neutral-400 last:border-0"
|
||||
id={proposal.id}
|
||||
data-testid="proposals-list-item"
|
||||
>
|
||||
<ProposalHeader proposal={proposal} />
|
||||
<ProposalsListItemDetails proposal={proposal} />
|
||||
<li id={proposal.id} data-testid="proposals-list-item">
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
<ProposalHeader proposal={proposal} />
|
||||
<ProposalsListItemDetails proposal={proposal} />
|
||||
</RoundedWrapper>
|
||||
</li>
|
||||
);
|
||||
};
|
@ -81,7 +81,7 @@ afterAll(() => {
|
||||
describe('Proposals list', () => {
|
||||
it('Render a page title and link to the make proposal form', () => {
|
||||
render(renderComponent([]));
|
||||
expect(screen.getByText('Governance')).toBeInTheDocument();
|
||||
expect(screen.getByText('Proposals')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('new-proposal-link')).toBeInTheDocument();
|
||||
});
|
||||
|
@ -51,7 +51,7 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
|
||||
<Heading
|
||||
centerContent={false}
|
||||
marginBottom={false}
|
||||
title={t('pageTitleGovernance')}
|
||||
title={t('pageTitleProposals')}
|
||||
/>
|
||||
<Link
|
||||
className="xs:justify-self-end"
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user