Feat/671 Proposal listings page (#733)
* frontend-monorepo-671: Removed old proposal list intro text * frontend-monorepo-671: Proposals sorted into open and closed state * frontend-monorepo-671: Proposals also sorted by date, and sorting functions memoized * frontend-monorepo-671: Updated proposal header for new text and i18n support, updated test * frontend-monorepo-671: Eth wallet connect button full width * frontend-monorepo-671: Proposal tests for primary text fixed * Frontend-monorepo-671: Updated proposal description and tests. Included translations * Frontend-monorepo-671: Small structural refactor * frontend-monorepo-715: Added required translations * frontend-monorepo-715: Proposals list item details * frontend-monorepo-715: Proposals list item styling * frontend-monorepo-671: Tests for proposals-list-item-details.tsx * frontend-monorepo-671: Tests and tweaks for proposals-list.tsx * frontend-monorepo-671: Reusable test components pulled into test-helpers * frontend-monorepo-671: Proposals list text filter and tests (partially working tests) * frontend-monorepo-671: Refactoring generateProposal to clobber rather than merge old arrays * frontend-monorepo-671: Readded commented out tests * frontend-monorepo-671: Removed empty files * frontend-monorepo-671: Made more use of generateProposal overrides * frontend-monorepo-671: Run prettier * frontend-monorepo-671: Fixed linting errors * frontend-monorepo-671: PR suggestions * frontend-monorepo-671: Used 'describe' and improved test descriptions * frontend-monorepo-646: PR improvement * frontend-monorepo-671: Tweak to basic cypress tests * Frontend-monorepo-45: Adjusted proposal filter and tests to remove rationale * Frontend-monorepo-45: Removed accidentally duplicated test * frontend-monorepo-671: More clarity for freeform proposal header * frontend-monorepo-671: resolve master * frontend-monorepo-671: Added issue number in comment for proposal rationale * frontend-monorepo-671: Added issue number in another comment for proposal rationale * frontend-monorepo-671: Mock timers added for proposals-list-item-details.spec.tsx * frontend-monorepo-671: Mock timers added for proposals-list.spec.tsx * frontend-monorepo-671: Improved styling to differentiate open vs closed proposals * Fixed previous incorrect resolution of master
This commit is contained in:
parent
6f374264c0
commit
aa0be2b3e8
@ -69,8 +69,7 @@ Run `nx serve my-app` for a dev server. Navigate to the port specified in `app/<
|
||||
|
||||
### Using Apollo GraphQL and Generate Types
|
||||
|
||||
In order to generate the schemas for your GraphQL queries, you can run `nx run types:generate`.
|
||||
If it is the first time you are running the command, make sure you are setting up the environment variable from `apollo.config.js`.
|
||||
In order to generate the schemas for your GraphQL queries, you can run `NX_VEGA_URL=[YOUR URL HERE] nx run types:generate`.
|
||||
|
||||
```bash
|
||||
export NX_VEGA_URL=https://lb.testnet.vega.xyz/query
|
||||
|
@ -1,4 +1,5 @@
|
||||
const noProposals = '[data-testid="no-proposals"]';
|
||||
const noOpenProposals = '[data-testid="no-open-proposals"]';
|
||||
const noClosedProposals = '[data-testid="no-closed-proposals"]';
|
||||
|
||||
context('Governance Page - verify elements on page', function () {
|
||||
before('navigate to governance page', function () {
|
||||
@ -14,10 +15,13 @@ context('Governance Page - verify elements on page', function () {
|
||||
cy.verify_page_header('Governance');
|
||||
});
|
||||
|
||||
it('should have information box visible', function () {
|
||||
cy.get(noProposals)
|
||||
it('should have information visible', function () {
|
||||
cy.get(noOpenProposals)
|
||||
.should('be.visible')
|
||||
.and('have.text', 'There are no active network change proposals');
|
||||
.and('have.text', 'There are no open or yet to enact proposals');
|
||||
cy.get(noClosedProposals)
|
||||
.should('be.visible')
|
||||
.and('have.text', 'There are no enacted or rejected proposals');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -202,9 +202,9 @@ export const EthWallet = () => {
|
||||
<WalletCard dark={true}>
|
||||
<section data-testid="ethereum-wallet">
|
||||
<WalletCardHeader>
|
||||
<h1 className="text-h3 uppercase">{t('ethereumKey')}</h1>
|
||||
<h1 className="m-0 text-h3 uppercase">{t('ethereumKey')}</h1>
|
||||
{account && (
|
||||
<div className="px-4 text-right">
|
||||
<div className="place-self-end font-mono px-4 pb-2">
|
||||
<div
|
||||
className="font-mono"
|
||||
data-testid="ethereum-account-truncated"
|
||||
@ -237,7 +237,7 @@ export const EthWallet = () => {
|
||||
) : (
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
className="w-full px-28 border h-28"
|
||||
className="w-full"
|
||||
onClick={() =>
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
|
||||
|
@ -156,15 +156,20 @@
|
||||
"The vesting contract holds VEGA tokens until they have become unlocked.": "The vesting contract holds $VEGA tokens until they have become unlocked.",
|
||||
"Once unlocked they can be redeemed from the contract so that you can transfer them between wallets.": "Once unlocked they can be redeemed from the contract so that you can transfer them between wallets.",
|
||||
"Tokens are held in different <trancheLink>Tranches</trancheLink>. Each tranche has its own schedule for how the tokens are unlocked.": "Tokens are held in different <trancheLink>Tranches</trancheLink>. Each tranche has its own schedule for how the tokens are unlocked.",
|
||||
"proposedChangesToVegaNetwork": "This page lists proposed changes to the Vega network.",
|
||||
"vegaTokenHoldersCanVote": "$VEGA token holders can vote for or against proposals as well as make their own.",
|
||||
"requiredMajorityDescription": "Each proposal needs both a required majority of votes (e.g 66% but this differs by proposal type) and to meet a minimum threshold of votes.",
|
||||
"proposals": "Proposals",
|
||||
"proposedEnactment": "Proposed enactment",
|
||||
"Enacted": "Enacted",
|
||||
"enactedOn": "Enacted on",
|
||||
"status": "Status",
|
||||
"state": "State",
|
||||
"shouldPass": "Should pass",
|
||||
"Participation": "Participation",
|
||||
"Majority": "Majority",
|
||||
"not reached": "not reached",
|
||||
"openProposals": "Open proposals",
|
||||
"closedProposals": "Closed proposals",
|
||||
"noOpenProposals": "There are no open or yet to enact proposals",
|
||||
"noClosedProposals": "There are no enacted or rejected proposals",
|
||||
"participationNotMet": "Participation not met",
|
||||
"majorityNotMet": "Majority not met",
|
||||
"noProposals": "There are no active network change proposals",
|
||||
@ -201,7 +206,13 @@
|
||||
"voteState_Yes": "For",
|
||||
"voteState_No": "Against",
|
||||
"voteState_NotCast": "Not cast",
|
||||
"voteState_Enacted": "Enacted",
|
||||
"voteState_Passed": "Passed",
|
||||
"voteState_WaitingForNodeVote": "Waiting for node vote",
|
||||
"voteState_Open": "Open",
|
||||
"voteState_Declined": "Declined",
|
||||
"voteState_Failed": "Failed",
|
||||
"voteState_Rejected": "Rejected",
|
||||
"Token address": "Token address",
|
||||
"Vesting contract": "Vesting contract",
|
||||
"Total supply": "Total supply",
|
||||
@ -484,6 +495,7 @@
|
||||
"invalidAddress": "Looks like that address isn't a valid Ethereum address, please check and try again",
|
||||
"Signature": "Signature",
|
||||
"voteFailedReason": "Vote closed. Failed due to: ",
|
||||
"Passed": "Passed",
|
||||
"votePassed": "Vote passed.",
|
||||
"subjectToFurtherActions": "Vote passed {{daysAgo}} subject to further actions.",
|
||||
"transactionHashPrompt": "Transaction hash will appear here once the transaction is approved in your Ethereum wallet",
|
||||
@ -544,8 +556,6 @@
|
||||
"noPercentage": "No percentage",
|
||||
"proposalTerms": "Proposal terms",
|
||||
"currentlySetTo": "Currently set to ",
|
||||
"pass": "Pass",
|
||||
"fail": "Fail",
|
||||
"rankingScore": "Ranking score",
|
||||
"stakeScore": "Stake score",
|
||||
"performanceScore": "Performance",
|
||||
@ -559,5 +569,56 @@
|
||||
"status-tendermint": "Consensus",
|
||||
"status-ersatz": "Ersatz",
|
||||
"status-pending": "Pending",
|
||||
"status-unspecified": "Unspecified"
|
||||
"status-unspecified": "Unspecified",
|
||||
"Set to": "Set to",
|
||||
"pass": "pass",
|
||||
"fail": "fail",
|
||||
"New asset": "New asset",
|
||||
"Asset change": "Asset change",
|
||||
"New market": "New market",
|
||||
"Market change": "Market change",
|
||||
"Network parameter": "Network parameter",
|
||||
"Unknown proposal": "Unknown proposal",
|
||||
"Code": "Code",
|
||||
"settled future": "settled future",
|
||||
"Symbol": "Symbol",
|
||||
"Max faucet amount mint": "Max faucet amount mint",
|
||||
"left to vote": "left to vote",
|
||||
"View": "View",
|
||||
"CloseTimeTooSoon": "Close time too soon",
|
||||
"CloseTimeTooLate": "Close time too late",
|
||||
"EnactTimeTooSoon": "Enact time too soon",
|
||||
"EnactTimeTooLate": "Enact time too late",
|
||||
"InsufficientTokens": "Insufficient tokens",
|
||||
"InvalidInstrumentSecurity": "Invalid instrument security",
|
||||
"NoProduct": "No product",
|
||||
"UnsupportedProduct": "Unsupported product",
|
||||
"InvalidFutureMaturityTimestamp": "Invalid future maturity timestamp",
|
||||
"ProductMaturityIsPassed": "Product maturity is passed",
|
||||
"NoTradingMode": "No trading mode",
|
||||
"UnsupportedTradingMode": "Unsupported trading mode",
|
||||
"NodeValidationFailed": "Node validation failed",
|
||||
"MissingBuiltinAssetField": "Missing builtin asset field",
|
||||
"MissingERC20ContractAddress": "Missing ERC20 contract address",
|
||||
"InvalidAsset": "Invalid asset",
|
||||
"IncompatibleTimestamps": "Incompatible timestamps",
|
||||
"NoRiskParameters": "No risk parameters",
|
||||
"NetworkParameterInvalidKey": "Network parameter invalid key",
|
||||
"NetworkParameterInvalidValue": "Network parameter invalid value",
|
||||
"NetworkParameterValidationFailed": "Network parameter validation failed",
|
||||
"OpeningAuctionDurationTooSmall": "Opening auction duration too small",
|
||||
"OpeningAuctionDurationTooLarge": "Opening auction duration too large",
|
||||
"MarketMissingLiquidityCommitment": "Market missing liquidity commitment",
|
||||
"CouldNotInstantiateMarket": "Could not instantiate market",
|
||||
"InvalidFutureProduct": "Invalid future product",
|
||||
"MissingCommitmentAmount": "Missing commitment amount",
|
||||
"InvalidFeeAmount": "Invalid fee amount",
|
||||
"InvalidShape": "Invalid shape",
|
||||
"InvalidRiskParameter": "Invalid risk parameter",
|
||||
"MajorityThresholdNotReached": "Majority threshold not reached",
|
||||
"ParticipationThresholdNotReached": "Participation threshold not reached",
|
||||
"InvalidAssetDetails": "Invalid asset details",
|
||||
"FilterProposals": "Filter proposals",
|
||||
"FilterProposalsDescription": "Filter by proposal ID or proposer ID",
|
||||
"Freeform proposal": "Freeform proposal"
|
||||
}
|
||||
|
@ -1,101 +0,0 @@
|
||||
import { generateProposal } from '../../routes/governance/test-helpers/generate-proposals';
|
||||
import { getProposalName } from './proposal';
|
||||
|
||||
const proposal = generateProposal();
|
||||
|
||||
it('New market', () => {
|
||||
const name = getProposalName({
|
||||
...proposal,
|
||||
terms: {
|
||||
...proposal.terms,
|
||||
change: {
|
||||
__typename: 'NewMarket',
|
||||
decimalPlaces: 1,
|
||||
instrument: {
|
||||
__typename: 'InstrumentConfiguration',
|
||||
name: 'Some market',
|
||||
},
|
||||
metadata: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(name).toEqual('New Market: Some market');
|
||||
});
|
||||
|
||||
it('New asset', () => {
|
||||
const name = getProposalName({
|
||||
...proposal,
|
||||
terms: {
|
||||
...proposal.terms,
|
||||
change: {
|
||||
__typename: 'NewAsset',
|
||||
symbol: 'FAKE',
|
||||
source: {
|
||||
__typename: 'ERC20',
|
||||
contractAddress: '0x0',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(name).toEqual('New Asset: FAKE');
|
||||
});
|
||||
|
||||
it('Update market', () => {
|
||||
const name = getProposalName({
|
||||
...proposal,
|
||||
terms: {
|
||||
...proposal.terms,
|
||||
change: {
|
||||
__typename: 'UpdateMarket',
|
||||
marketId: 'MarketId',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(name).toEqual('Update Market: MarketId');
|
||||
});
|
||||
|
||||
it('Update network', () => {
|
||||
const name = getProposalName({
|
||||
...proposal,
|
||||
terms: {
|
||||
...proposal.terms,
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'key',
|
||||
value: 'value',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(name).toEqual('Update Network: key');
|
||||
});
|
||||
|
||||
it('Freeform network', () => {
|
||||
const name = getProposalName({
|
||||
...proposal,
|
||||
id: 'test-id',
|
||||
terms: {
|
||||
...proposal.terms,
|
||||
change: {
|
||||
__typename: 'NewFreeform',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(name).toEqual('Freeform: test-id');
|
||||
});
|
||||
|
||||
it("Renders unknown proposal if it's a different proposal type", () => {
|
||||
const name = getProposalName({
|
||||
...proposal,
|
||||
terms: {
|
||||
...proposal.terms,
|
||||
change: {
|
||||
// @ts-ignore unknown proposal
|
||||
__typename: 'Foo',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(name).toEqual('Unknown Proposal');
|
||||
});
|
@ -1,19 +0,0 @@
|
||||
import type { Proposals_proposals } from '../../routes/governance/proposals/__generated__/Proposals';
|
||||
|
||||
export function getProposalName(proposal: Proposals_proposals) {
|
||||
const { change } = proposal.terms;
|
||||
|
||||
if (change.__typename === 'NewAsset') {
|
||||
return `New Asset: ${change.symbol}`;
|
||||
} else if (change.__typename === 'NewMarket') {
|
||||
return `New Market: ${change.instrument.name}`;
|
||||
} else if (change.__typename === 'UpdateMarket') {
|
||||
return `Update Market: ${change.marketId}`;
|
||||
} else if (change.__typename === 'UpdateNetworkParameter') {
|
||||
return `Update Network: ${change.networkParameter.key}`;
|
||||
} else if (change.__typename === 'NewFreeform') {
|
||||
return `Freeform: ${proposal.id}`;
|
||||
}
|
||||
|
||||
return 'Unknown Proposal';
|
||||
}
|
@ -21,12 +21,36 @@ export interface ProposalFields_terms_change_NewFreeform {
|
||||
__typename: "NewFreeform";
|
||||
}
|
||||
|
||||
export interface ProposalFields_terms_change_NewMarket_instrument_futureProduct_settlementAsset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
export interface ProposalFields_terms_change_NewMarket_instrument_futureProduct {
|
||||
__typename: "FutureProduct";
|
||||
/**
|
||||
* Product asset ID
|
||||
*/
|
||||
settlementAsset: ProposalFields_terms_change_NewMarket_instrument_futureProduct_settlementAsset;
|
||||
}
|
||||
|
||||
export interface ProposalFields_terms_change_NewMarket_instrument {
|
||||
__typename: "InstrumentConfiguration";
|
||||
/**
|
||||
* Full and fairly descriptive name for the instrument
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18)
|
||||
*/
|
||||
code: string;
|
||||
/**
|
||||
* Future product specification
|
||||
*/
|
||||
futureProduct: ProposalFields_terms_change_NewMarket_instrument_futureProduct | null;
|
||||
}
|
||||
|
||||
export interface ProposalFields_terms_change_NewMarket {
|
||||
@ -70,6 +94,10 @@ export type ProposalFields_terms_change_NewAsset_source = ProposalFields_terms_c
|
||||
|
||||
export interface ProposalFields_terms_change_NewAsset {
|
||||
__typename: "NewAsset";
|
||||
/**
|
||||
* The full name of the asset (e.g: Great British Pound)
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
|
@ -6,11 +6,11 @@ import { ProposalState } from '../../../../__generated__/globalTypes';
|
||||
import { useVoteInformation } from '../../hooks';
|
||||
import type { Proposals_proposals } from '../../proposals/__generated__/Proposals';
|
||||
|
||||
const StatusPass = ({ children }: { children: React.ReactNode }) => (
|
||||
export const StatusPass = ({ children }: { children: React.ReactNode }) => (
|
||||
<span className="text-vega-green">{children}</span>
|
||||
);
|
||||
|
||||
const StatusFail = ({ children }: { children: React.ReactNode }) => (
|
||||
export const StatusFail = ({ children }: { children: React.ReactNode }) => (
|
||||
<span className="text-danger">{children}</span>
|
||||
);
|
||||
|
||||
|
@ -0,0 +1,277 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
import { ProposalHeader } from './proposal-header';
|
||||
import type { Proposals_proposals } from '../../proposals/__generated__/Proposals';
|
||||
|
||||
const renderComponent = (proposal: Proposals_proposals) => (
|
||||
<ProposalHeader proposal={proposal} />
|
||||
);
|
||||
|
||||
describe('Proposal header', () => {
|
||||
it('Renders New market proposal', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewMarket',
|
||||
decimalPlaces: 1,
|
||||
instrument: {
|
||||
__typename: 'InstrumentConfiguration',
|
||||
name: 'Some market',
|
||||
code: 'FX:BTCUSD/DEC99',
|
||||
futureProduct: {
|
||||
__typename: 'FutureProduct',
|
||||
settlementAsset: {
|
||||
__typename: 'Asset',
|
||||
symbol: 'tGBP',
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-header')).toHaveTextContent(
|
||||
'New market: Some market'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-details-one')).toHaveTextContent(
|
||||
'tGBP settled future.'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders Update market proposal', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarket',
|
||||
marketId: 'MarketId',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-header')).toHaveTextContent(
|
||||
'Market change: MarketId'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders New asset proposal - ERC20', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewAsset',
|
||||
name: 'Fake currency',
|
||||
symbol: 'FAKE',
|
||||
source: {
|
||||
__typename: 'ERC20',
|
||||
contractAddress: '0x0',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-header')).toHaveTextContent(
|
||||
'New asset: Fake currency'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-details-one')).toHaveTextContent(
|
||||
'Symbol: FAKE. ERC20 0x0'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders New asset proposal - BuiltInAsset', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewAsset',
|
||||
name: 'Fake currency',
|
||||
symbol: 'BIA',
|
||||
source: {
|
||||
__typename: 'BuiltinAsset',
|
||||
maxFaucetAmountMint: '300',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-header')).toHaveTextContent(
|
||||
'New asset: Fake currency'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-details-one')).toHaveTextContent(
|
||||
'Symbol: BIA. Max faucet amount mint: 300'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders Update network', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'Network key',
|
||||
value: 'Network value',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-header')).toHaveTextContent(
|
||||
'Network parameter'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-details-one')).toHaveTextContent(
|
||||
'Network key to Network value'
|
||||
);
|
||||
});
|
||||
|
||||
// Skipped until proposals have rationale - https://github.com/vegaprotocol/frontend-monorepo/issues/824
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('Renders Freeform network - short rationale', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
id: 'short',
|
||||
// rationale: {
|
||||
// hash: '0x0',
|
||||
// description: 'freeform description',
|
||||
// },
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewFreeform',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-header')).toHaveTextContent(
|
||||
'freeform description'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-details-one')).toBeEmptyDOMElement();
|
||||
expect(screen.getByTestId('proposal-details-two')).toHaveTextContent(
|
||||
'short'
|
||||
);
|
||||
});
|
||||
|
||||
// Skipped until proposals have rationale
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('Renders Freeform proposal - long rationale (105 chars)', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
id: 'long',
|
||||
// rationale: {
|
||||
// hash: '0x0',
|
||||
// description:
|
||||
// 'Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean dolor.',
|
||||
// },
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewFreeform',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
// 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-header')).toHaveTextContent(
|
||||
'Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean…'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-details-one')).toHaveTextContent(
|
||||
'dolor'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-details-two')).toHaveTextContent(
|
||||
'long'
|
||||
);
|
||||
});
|
||||
|
||||
// Skipped until proposals have rationale
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('Renders Freeform proposal - extra long rationale (165 chars)', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
id: 'extraLong',
|
||||
// rationale: {
|
||||
// hash: '0x0',
|
||||
// description:
|
||||
// 'Aenean sem odio, eleifend non sodales vitae, porttitor eu ex. Aliquam erat volutpat. Fusce pharetra libero quis risus lobortis, sed ornare leo efficitur turpis duis.',
|
||||
// },
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewFreeform',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
// For a rationale over 160 chars, we expect the header to be truncated at 100
|
||||
// chars with ellipsis and the details-one element to contain 60 chars and also
|
||||
// be truncated with an ellipsis.
|
||||
expect(screen.getByTestId('proposal-header')).toHaveTextContent(
|
||||
'Aenean sem odio, eleifend non sodales vitae, porttitor eu ex. Aliquam erat volutpat. Fusce pharetra…'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-details-one')).toHaveTextContent(
|
||||
'libero quis risus lobortis, sed ornare leo efficitur turpis…'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-details-two')).toHaveTextContent(
|
||||
'extraLong'
|
||||
);
|
||||
});
|
||||
|
||||
// Remove once proposals have rationale and re-enable above tests
|
||||
it('Renders Freeform proposal - id for title', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
id: 'freeform id',
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewFreeform',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-header')).toHaveTextContent(
|
||||
'Freeform proposal: freeform id'
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId('proposal-details-one')
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('proposal-details-two')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Renders unknown proposal if it's a different proposal type", () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
// @ts-ignore unknown proposal
|
||||
__typename: 'Foo',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-header')).toHaveTextContent(
|
||||
'Unknown proposal'
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,109 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { Proposals_proposals } from '../../proposals/__generated__/Proposals';
|
||||
|
||||
export const ProposalHeader = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposals_proposals;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { change } = proposal.terms;
|
||||
|
||||
let headerText: string;
|
||||
let detailsOne: ReactNode;
|
||||
let detailsTwo: ReactNode;
|
||||
|
||||
switch (change.__typename) {
|
||||
case 'NewMarket': {
|
||||
headerText = `${t('New market')}: ${change.instrument.name}`;
|
||||
detailsOne = (
|
||||
<>
|
||||
{t('Code')}: {change.instrument.code}.{' '}
|
||||
{change.instrument.futureProduct?.settlementAsset.symbol ? (
|
||||
<>
|
||||
<span className="font-semibold">
|
||||
{change.instrument.futureProduct.settlementAsset.symbol}
|
||||
</span>{' '}
|
||||
{t('settled future')}.
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'UpdateMarket': {
|
||||
headerText = `${t('Market change')}: ${change.marketId}`;
|
||||
break;
|
||||
}
|
||||
case 'NewAsset': {
|
||||
headerText = `${t('New asset')}: ${change.name}`;
|
||||
detailsOne = (
|
||||
<>
|
||||
{t('Symbol')}: {change.symbol}.{' '}
|
||||
<Lozenge>
|
||||
{change.source.__typename === 'ERC20'
|
||||
? `ERC20 ${change.source.contractAddress}`
|
||||
: `${t('Max faucet amount mint')}: ${
|
||||
change.source.maxFaucetAmountMint
|
||||
}`}
|
||||
</Lozenge>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'UpdateNetworkParameter': {
|
||||
headerText = `${t('Network parameter')}`;
|
||||
detailsOne = (
|
||||
<>
|
||||
<Lozenge>{change.networkParameter.key}</Lozenge> {t('to')}{' '}
|
||||
<Lozenge>{change.networkParameter.value}</Lozenge>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'NewFreeform': {
|
||||
// When rationale exists (https://github.com/vegaprotocol/frontend-monorepo/issues/824):
|
||||
// const description = proposal.rationale.description.trim();
|
||||
// const headerMaxLength = 100;
|
||||
// const descriptionOneMaxLength = 60;
|
||||
// const headerOverflow = description.length > headerMaxLength;
|
||||
// const descriptionOneOverflow =
|
||||
// description.length > headerMaxLength + descriptionOneMaxLength;
|
||||
//
|
||||
// headerText = `${description.substring(0, headerMaxLength - 1).trim()}${
|
||||
// headerOverflow ? '…' : ''
|
||||
// }`;
|
||||
// detailsOne = headerOverflow
|
||||
// ? `${description
|
||||
// .substring(
|
||||
// headerMaxLength - 1,
|
||||
// headerMaxLength + descriptionOneMaxLength - 1
|
||||
// )
|
||||
// .trim()}${descriptionOneOverflow ? '…' : ''}`
|
||||
// : '';
|
||||
// detailsTwo = `${proposal.id}`;
|
||||
headerText = proposal.id
|
||||
? `${t('Freeform proposal')}: ${proposal.id.trim()}`
|
||||
: `${t('Unknown proposal')}`;
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
headerText = `${t('Unknown proposal')}`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-ui text-white">
|
||||
<header data-testid="proposal-header">
|
||||
<h2 className="text-h5 font-semibold mb-4">{headerText}</h2>
|
||||
</header>
|
||||
{detailsOne && <div data-testid="proposal-details-one">{detailsOne}</div>}
|
||||
{detailsTwo && <div data-testid="proposal-details-two">{detailsTwo}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,5 +1,4 @@
|
||||
import { Heading } from '../../../../components/heading';
|
||||
import { getProposalName } from '../../../../lib/type-policies/proposal';
|
||||
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
|
||||
import type { Proposal_proposal } from '../../proposal/__generated__/Proposal';
|
||||
import type { RestProposalResponse } from '../../proposal/proposal-container';
|
||||
import { ProposalChangeTable } from '../proposal-change-table';
|
||||
@ -19,7 +18,7 @@ export const Proposal = ({ proposal, terms }: ProposalProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading title={getProposalName(proposal)} />
|
||||
<ProposalHeader proposal={proposal} />
|
||||
<ProposalChangeTable proposal={proposal} />
|
||||
<VoteDetails proposal={proposal} />
|
||||
<ProposalVotesTable proposal={proposal} />
|
||||
|
@ -0,0 +1 @@
|
||||
export { ProposalsListFilter } from './proposals-list-filter';
|
@ -0,0 +1,46 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState } from 'react';
|
||||
import { Button, FormGroup, Input } from '@vegaprotocol/ui-toolkit';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
interface ProposalsListFilterProps {
|
||||
setFilterString: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
export const ProposalsListFilter = ({
|
||||
setFilterString,
|
||||
}: ProposalsListFilterProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [filterVisible, setFilterVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<div data-testid="proposals-list-filter">
|
||||
{!filterVisible && (
|
||||
<Button
|
||||
onClick={() => setFilterVisible(true)}
|
||||
variant="inline-link"
|
||||
className="pl-0 pb-20"
|
||||
data-testid="set-proposals-filter-visible"
|
||||
>
|
||||
{t('FilterProposals')}
|
||||
</Button>
|
||||
)}
|
||||
{filterVisible && (
|
||||
<div data-testid="open-proposals-list-filter">
|
||||
<p>{t('FilterProposalsDescription')}</p>
|
||||
<FormGroup
|
||||
labelClassName="sr-only"
|
||||
label="Filter text input"
|
||||
labelFor="filter-input"
|
||||
>
|
||||
<Input
|
||||
data-testid="filter-input"
|
||||
id="filter-input"
|
||||
onChange={(e) => setFilterString(e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export { ProposalsListItem } from './proposals-list-item';
|
||||
export { ProposalsListItemDetails } from './proposals-list-item-details';
|
@ -0,0 +1,377 @@
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
ProposalRejectionReason,
|
||||
ProposalState,
|
||||
VoteValue,
|
||||
} from '@vegaprotocol/types';
|
||||
import {
|
||||
generateNoVotes,
|
||||
generateProposal,
|
||||
generateYesVotes,
|
||||
} from '../../test-helpers/generate-proposals';
|
||||
import { ProposalsListItemDetails } from './proposals-list-item-details';
|
||||
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
||||
import {
|
||||
mockPubkey,
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
fiveMinutes,
|
||||
fiveHours,
|
||||
fiveDays,
|
||||
lastWeek,
|
||||
nextWeek,
|
||||
} from '../../test-helpers/mocks';
|
||||
import type { Proposals_proposals } from '../../proposals/__generated__/Proposals';
|
||||
|
||||
const renderComponent = (
|
||||
proposal: Proposals_proposals,
|
||||
mock = networkParamsQueryMock
|
||||
) => (
|
||||
<Router>
|
||||
<MockedProvider mocks={[mock]}>
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposalsListItemDetails proposal={proposal} />
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
</MockedProvider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('Proposals list item details', () => {
|
||||
it('Renders proposal state: Enacted', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.Enacted,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Enacted');
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
format(lastWeek, DATE_FORMAT_DETAILED)
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Passed', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.Passed,
|
||||
terms: {
|
||||
closingDatetime: lastWeek.toString(),
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Passed');
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
`Enacts on ${format(nextWeek, DATE_FORMAT_DETAILED)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Waiting for node vote', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.WaitingForNodeVote,
|
||||
terms: {
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent(
|
||||
'Waiting for node vote'
|
||||
);
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
`Enacts on ${format(nextWeek, DATE_FORMAT_DETAILED)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - 5 minutes left to vote', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.Open,
|
||||
terms: {
|
||||
closingDatetime: fiveMinutes.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
'5 minutes left to vote'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - 5 hours left to vote', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.Open,
|
||||
terms: {
|
||||
closingDatetime: fiveHours.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
'5 hours left to vote'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - 5 days left to vote', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.Open,
|
||||
terms: {
|
||||
closingDatetime: fiveDays.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
'5 days left to vote'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - user voted for', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.Open,
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: {
|
||||
votes: [
|
||||
{
|
||||
__typename: 'Vote',
|
||||
value: VoteValue.Yes,
|
||||
datetime: lastWeek.toString(),
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
id: mockPubkey,
|
||||
stake: {
|
||||
__typename: 'PartyStake',
|
||||
currentStakeAvailable: '1000',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
no: generateNoVotes(0),
|
||||
},
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
'You voted For'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - user voted against', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.Open,
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
no: {
|
||||
votes: [
|
||||
{
|
||||
__typename: 'Vote',
|
||||
value: VoteValue.No,
|
||||
datetime: lastWeek.toString(),
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
id: mockPubkey,
|
||||
stake: {
|
||||
__typename: 'PartyStake',
|
||||
currentStakeAvailable: '1000',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
yes: generateYesVotes(0),
|
||||
},
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
'You voted Against'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - participation not reached', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.Open,
|
||||
terms: {
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
no: generateNoVotes(0),
|
||||
yes: generateYesVotes(0),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
'Participation not reached'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - majority not reached', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.Open,
|
||||
terms: {
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
no: generateNoVotes(1, 1000000000000000000),
|
||||
yes: generateYesVotes(1, 1000000000000000000),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
'Majority not reached'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - will pass', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.Open,
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: generateYesVotes(3000, 1000000000000000000),
|
||||
no: generateNoVotes(0),
|
||||
},
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent('Set to pass');
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - will fail', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.Open,
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: generateYesVotes(0),
|
||||
no: generateNoVotes(3000, 1000000000000000000),
|
||||
},
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent('Set to fail');
|
||||
});
|
||||
|
||||
it('Renders proposal state: Declined - participation not reached', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.Declined,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
no: generateNoVotes(0),
|
||||
yes: generateYesVotes(0),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Declined');
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
'Participation not reached'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Declined - majority not reached', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.Declined,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
no: generateNoVotes(1, 1000000000000000000),
|
||||
yes: generateYesVotes(1, 1000000000000000000),
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Declined');
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
'Majority not reached'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Rejected', () => {
|
||||
render(
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.Rejected,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
rejectionReason: ProposalRejectionReason.InvalidFutureProduct,
|
||||
})
|
||||
)
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Rejected');
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
'Invalid future product'
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,207 @@
|
||||
import classnames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVoteInformation } from '../../hooks';
|
||||
import { useUserVote } from '../vote-details/use-user-vote';
|
||||
import {
|
||||
StatusPass,
|
||||
StatusFail,
|
||||
} from '../current-proposal-status/current-proposal-status';
|
||||
import { format, formatDistanceToNowStrict } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
||||
import { ProposalState } from '../../../../__generated__/globalTypes';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { Proposals_proposals } from '../../proposals/__generated__/Proposals';
|
||||
|
||||
const MajorityNotReached = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
{t('Majority')} <StatusFail>{t('not reached')}</StatusFail>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const ParticipationNotReached = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
{t('Participation')} <StatusFail>{t('not reached')}</StatusFail>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProposalsListItemDetails = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposals_proposals;
|
||||
}) => {
|
||||
const { state } = proposal;
|
||||
const { willPass, majorityMet, participationMet } = useVoteInformation({
|
||||
proposal,
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const { voteState } = useUserVote(
|
||||
proposal.id,
|
||||
proposal.votes.yes.votes,
|
||||
proposal.votes.no.votes
|
||||
);
|
||||
|
||||
let proposalStatus: ReactNode;
|
||||
let voteDetails: ReactNode;
|
||||
let voteStatus: ReactNode;
|
||||
|
||||
switch (state) {
|
||||
case ProposalState.Enacted: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
{t('voteState_Enacted')} <Icon name={'tick'} />
|
||||
</>
|
||||
);
|
||||
voteDetails = (
|
||||
<>
|
||||
{format(
|
||||
new Date(proposal.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
)}
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case ProposalState.Passed: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
{t('voteState_Passed')} <Icon name={'tick'} />
|
||||
</>
|
||||
);
|
||||
voteDetails = proposal.terms.change.__typename !== 'NewFreeform' && (
|
||||
<>
|
||||
{t('toEnactOn')}{' '}
|
||||
{format(
|
||||
new Date(proposal.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
)}
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case ProposalState.WaitingForNodeVote: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
{t('voteState_WaitingForNodeVote')} <Icon name={'time'} />
|
||||
</>
|
||||
);
|
||||
voteDetails = proposal.terms.change.__typename !== 'NewFreeform' && (
|
||||
<>
|
||||
{t('toEnactOn')}{' '}
|
||||
{format(
|
||||
new Date(proposal.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
)}
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case ProposalState.Open: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
{t('voteState_Open')} <Icon name={'hand'} />
|
||||
</>
|
||||
);
|
||||
voteDetails = (voteState === 'Yes' && (
|
||||
<>
|
||||
{t('youVoted')} <StatusPass>{t('voteState_Yes')}</StatusPass>
|
||||
</>
|
||||
)) ||
|
||||
(voteState === 'No' && (
|
||||
<>
|
||||
{t('youVoted')} <StatusFail>{t('voteState_No')}</StatusFail>
|
||||
</>
|
||||
)) || (
|
||||
<>
|
||||
{formatDistanceToNowStrict(
|
||||
new Date(proposal.terms.closingDatetime)
|
||||
)}{' '}
|
||||
{t('left to vote')}
|
||||
</>
|
||||
);
|
||||
voteStatus =
|
||||
(!participationMet && <ParticipationNotReached />) ||
|
||||
(!majorityMet && <MajorityNotReached />) ||
|
||||
(willPass && (
|
||||
<>
|
||||
{t('Set to')} <StatusPass>{t('pass')}</StatusPass>
|
||||
</>
|
||||
)) ||
|
||||
(!willPass && (
|
||||
<>
|
||||
{t('Set to')} <StatusFail>{t('fail')}</StatusFail>
|
||||
</>
|
||||
));
|
||||
break;
|
||||
}
|
||||
case ProposalState.Declined: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
{t('voteState_Declined')} <Icon name={'cross'} />
|
||||
</>
|
||||
);
|
||||
voteStatus =
|
||||
(!participationMet && <ParticipationNotReached />) ||
|
||||
(!majorityMet && <MajorityNotReached />);
|
||||
break;
|
||||
}
|
||||
case ProposalState.Rejected: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
<StatusFail>{t('voteState_Rejected')}</StatusFail>{' '}
|
||||
<Icon name={'warning-sign'} />
|
||||
</>
|
||||
);
|
||||
voteStatus = proposal.rejectionReason && (
|
||||
<>{t(proposal.rejectionReason)}</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'grid grid-cols-[1fr_auto] items-start gap-4',
|
||||
'mt-4',
|
||||
'text-ui'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="col-start-1 row-start-1 flex items-center gap-4 text-white"
|
||||
data-testid="proposal-status"
|
||||
>
|
||||
{proposalStatus}
|
||||
</div>
|
||||
{voteDetails && (
|
||||
<div className="col-start-1 row-start-2" data-testid="vote-details">
|
||||
{voteDetails}
|
||||
</div>
|
||||
)}
|
||||
{voteStatus && (
|
||||
<div
|
||||
className="col-start-2 row-start-1 justify-self-end"
|
||||
data-testid="vote-status"
|
||||
>
|
||||
testing testing 123
|
||||
{voteStatus}
|
||||
</div>
|
||||
)}
|
||||
{proposal.id && (
|
||||
<div className="col-start-2 row-start-2 justify-self-end">
|
||||
<Link to={proposal.id}>
|
||||
<Button variant="secondary" data-testid="view-proposal-btn">
|
||||
{t('View')}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
|
||||
import { ProposalsListItemDetails } from './proposals-list-item-details';
|
||||
import type { Proposals_proposals } from '../../proposals/__generated__/Proposals';
|
||||
|
||||
interface ProposalsListItemProps {
|
||||
proposal: Proposals_proposals;
|
||||
}
|
||||
|
||||
export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
|
||||
if (!proposal || !proposal.id) return null;
|
||||
|
||||
return (
|
||||
<li
|
||||
className="py-20 border-b border-white-40"
|
||||
id={proposal.id}
|
||||
data-testid="proposals-list-item"
|
||||
>
|
||||
<ProposalHeader proposal={proposal} />
|
||||
<ProposalsListItemDetails proposal={proposal} />
|
||||
</li>
|
||||
);
|
||||
};
|
@ -0,0 +1,219 @@
|
||||
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 { ProposalsList } from './proposals-list';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import {
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
lastWeek,
|
||||
nextWeek,
|
||||
lastMonth,
|
||||
nextMonth,
|
||||
} from '../../test-helpers/mocks';
|
||||
import type { Proposals_proposals } from '../../proposals/__generated__/Proposals';
|
||||
|
||||
const openProposalClosesNextMonth = generateProposal({
|
||||
id: 'proposal1',
|
||||
state: ProposalState.Open,
|
||||
party: {
|
||||
id: 'zxcv',
|
||||
},
|
||||
terms: {
|
||||
closingDatetime: nextMonth.toString(),
|
||||
enactmentDatetime: nextMonth.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
const openProposalClosesNextWeek = generateProposal({
|
||||
id: 'proposal2',
|
||||
state: ProposalState.Open,
|
||||
party: {
|
||||
id: 'bvcx',
|
||||
},
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
const enactedProposalClosedLastWeek = generateProposal({
|
||||
id: 'proposal3',
|
||||
state: ProposalState.Enacted,
|
||||
terms: {
|
||||
closingDatetime: lastWeek.toString(),
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
const rejectedProposalClosedLastMonth = generateProposal({
|
||||
id: 'proposal4',
|
||||
state: ProposalState.Rejected,
|
||||
terms: {
|
||||
closingDatetime: lastMonth.toString(),
|
||||
enactmentDatetime: lastMonth.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
const failedProposal = generateProposal({
|
||||
id: 'proposal5',
|
||||
state: ProposalState.Failed,
|
||||
});
|
||||
|
||||
const renderComponent = (proposals: Proposals_proposals[]) => (
|
||||
<Router>
|
||||
<MockedProvider mocks={[networkParamsQueryMock]}>
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposalsList proposals={proposals} />
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
</MockedProvider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('Proposals list', () => {
|
||||
it('Culls failed proposals', () => {
|
||||
render(renderComponent([failedProposal]));
|
||||
expect(screen.queryByTestId('open-proposals')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('no-open-proposals')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('closed-proposals')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('no-closed-proposals')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Will hide filter if no proposals', () => {
|
||||
render(renderComponent([]));
|
||||
expect(
|
||||
screen.queryByTestId('proposals-list-filter')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Will show filter if there are proposals', () => {
|
||||
render(renderComponent([enactedProposalClosedLastWeek]));
|
||||
expect(screen.queryByTestId('proposals-list-filter')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Places proposals correctly in open or closed lists', () => {
|
||||
render(
|
||||
renderComponent([
|
||||
openProposalClosesNextWeek,
|
||||
openProposalClosesNextMonth,
|
||||
enactedProposalClosedLastWeek,
|
||||
rejectedProposalClosedLastMonth,
|
||||
])
|
||||
);
|
||||
const openProposals = within(screen.getByTestId('open-proposals'));
|
||||
const closedProposals = within(screen.getByTestId('closed-proposals'));
|
||||
expect(openProposals.getAllByTestId('proposals-list-item').length).toBe(2);
|
||||
expect(closedProposals.getAllByTestId('proposals-list-item').length).toBe(
|
||||
2
|
||||
);
|
||||
});
|
||||
|
||||
it('Orders proposals correctly by closingDateTime', () => {
|
||||
render(
|
||||
renderComponent([
|
||||
rejectedProposalClosedLastMonth,
|
||||
openProposalClosesNextMonth,
|
||||
openProposalClosesNextWeek,
|
||||
enactedProposalClosedLastWeek,
|
||||
])
|
||||
);
|
||||
const openProposals = within(screen.getByTestId('open-proposals'));
|
||||
const closedProposals = within(screen.getByTestId('closed-proposals'));
|
||||
const openProposalsItems = openProposals.getAllByTestId(
|
||||
'proposals-list-item'
|
||||
);
|
||||
const closedProposalsItems = closedProposals.getAllByTestId(
|
||||
'proposals-list-item'
|
||||
);
|
||||
expect(openProposalsItems[0]).toHaveAttribute('id', 'proposal1');
|
||||
expect(openProposalsItems[1]).toHaveAttribute('id', 'proposal2');
|
||||
expect(closedProposalsItems[0]).toHaveAttribute('id', 'proposal4');
|
||||
expect(closedProposalsItems[1]).toHaveAttribute('id', 'proposal3');
|
||||
});
|
||||
|
||||
it('Displays info on no proposals', () => {
|
||||
render(renderComponent([]));
|
||||
expect(screen.queryByTestId('open-proposals')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('no-open-proposals')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('closed-proposals')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('no-closed-proposals')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Displays info on no open proposals if only closed are present', () => {
|
||||
render(renderComponent([enactedProposalClosedLastWeek]));
|
||||
expect(screen.queryByTestId('open-proposals')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('no-open-proposals')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('closed-proposals')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('no-closed-proposals')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Displays info on no closed proposals if only open are present', () => {
|
||||
render(renderComponent([openProposalClosesNextWeek]));
|
||||
expect(screen.getByTestId('open-proposals')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('no-open-proposals')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('closed-proposals')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('no-closed-proposals')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Opens filter form when button is clicked', () => {
|
||||
render(
|
||||
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('set-proposals-filter-visible'));
|
||||
expect(
|
||||
screen.getByTestId('open-proposals-list-filter')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Filters list by text - party id', () => {
|
||||
render(
|
||||
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('set-proposals-filter-visible'));
|
||||
fireEvent.change(screen.getByTestId('filter-input'), {
|
||||
target: { value: 'bvcx' },
|
||||
});
|
||||
const container = screen.getByTestId('open-proposals');
|
||||
expect(container.querySelector('#proposal2')).toBeInTheDocument();
|
||||
expect(container.querySelector('#proposal1')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Filters list by text - proposal id', () => {
|
||||
render(
|
||||
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('set-proposals-filter-visible'));
|
||||
fireEvent.change(screen.getByTestId('filter-input'), {
|
||||
target: { value: 'proposal1' },
|
||||
});
|
||||
const container = screen.getByTestId('open-proposals');
|
||||
expect(container.querySelector('#proposal1')).toBeInTheDocument();
|
||||
expect(container.querySelector('#proposal2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Filters list by text - check for substring matching', () => {
|
||||
render(
|
||||
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('set-proposals-filter-visible'));
|
||||
fireEvent.change(screen.getByTestId('filter-input'), {
|
||||
target: { value: 'osal1' },
|
||||
});
|
||||
const container = screen.getByTestId('open-proposals');
|
||||
expect(container.querySelector('#proposal1')).toBeInTheDocument();
|
||||
expect(container.querySelector('#proposal2')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -1,84 +1,83 @@
|
||||
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
||||
import { format, isFuture } from 'date-fns';
|
||||
import { isFuture } from 'date-fns';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
|
||||
import { getProposalName } from '../../../../lib/type-policies/proposal';
|
||||
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';
|
||||
import { CurrentProposalState } from '../current-proposal-state';
|
||||
|
||||
interface ProposalsListProps {
|
||||
proposals: Proposals_proposals[];
|
||||
}
|
||||
|
||||
interface SortedProposalsProps {
|
||||
open: Proposals_proposals[];
|
||||
closed: Proposals_proposals[];
|
||||
}
|
||||
|
||||
export const ProposalsList = ({ proposals }: ProposalsListProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [filterString, setFilterString] = useState('');
|
||||
|
||||
if (proposals.length === 0) {
|
||||
return <p data-testid="no-proposals">{t('noProposals')}</p>;
|
||||
}
|
||||
const failedProposalsCulled = proposals.filter(
|
||||
({ state }) => state !== 'Failed'
|
||||
);
|
||||
|
||||
const sortedProposals = failedProposalsCulled.reduce(
|
||||
(acc: SortedProposalsProps, proposal) => {
|
||||
if (isFuture(new Date(proposal.terms.closingDatetime))) {
|
||||
acc.open.push(proposal);
|
||||
} else {
|
||||
acc.closed.push(proposal);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
open: [],
|
||||
closed: [],
|
||||
}
|
||||
);
|
||||
|
||||
const filterPredicate = (p: Proposals_proposals) =>
|
||||
p.id?.includes(filterString) ||
|
||||
p.party?.id?.toString().includes(filterString);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>{t('proposedChangesToVegaNetwork')}</p>
|
||||
<p>{t('vegaTokenHoldersCanVote')}</p>
|
||||
<p>{t('requiredMajorityDescription')}</p>
|
||||
<h2>{t('proposals')}</h2>
|
||||
<ul>
|
||||
{proposals.map((proposal) => (
|
||||
<ProposalListItem proposal={proposal} />
|
||||
))}
|
||||
</ul>
|
||||
<Heading title={t('pageTitleGovernance')} />
|
||||
{failedProposalsCulled.length > 0 && (
|
||||
<ProposalsListFilter setFilterString={setFilterString} />
|
||||
)}
|
||||
|
||||
<section className="mx-[-20px] p-20 bg-white-10">
|
||||
<h2 className="text-h4 mb-0">{t('openProposals')}</h2>
|
||||
{sortedProposals.open.length > 0 ? (
|
||||
<ul data-testid="open-proposals">
|
||||
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
|
||||
<ProposalsListItem key={proposal.id} proposal={proposal} />
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="mt-12 mb-0" data-testid="no-open-proposals">
|
||||
{t('noOpenProposals')}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="mx-[-20px] p-20">
|
||||
<h2 className="text-h4 mb-0">{t('closedProposals')}</h2>
|
||||
{sortedProposals.closed.length > 0 ? (
|
||||
<ul data-testid="closed-proposals">
|
||||
{sortedProposals.closed.filter(filterPredicate).map((proposal) => (
|
||||
<ProposalsListItem key={proposal.id} proposal={proposal} />
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="mt-12 mb-0" data-testid="no-closed-proposals">
|
||||
{t('noClosedProposals')}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface ProposalListItemProps {
|
||||
proposal: Proposals_proposals;
|
||||
}
|
||||
|
||||
const ProposalListItem = ({ proposal }: ProposalListItemProps) => {
|
||||
const { t } = useTranslation();
|
||||
if (!proposal || !proposal.id) return null;
|
||||
|
||||
return (
|
||||
<li className="last:mb-0 mb-24" key={proposal.id}>
|
||||
<Link to={proposal.id} className="underline text-white">
|
||||
<header>{getProposalName(proposal)}</header>
|
||||
</Link>
|
||||
<KeyValueTable muted={true}>
|
||||
<KeyValueTableRow>
|
||||
{t('state')}
|
||||
<span data-testid="governance-proposal-state">
|
||||
<CurrentProposalState proposal={proposal} />
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{isFuture(new Date(proposal.terms.closingDatetime))
|
||||
? t('closesOn')
|
||||
: t('closedOn')}
|
||||
|
||||
<span data-testid="governance-proposal-closingDate">
|
||||
{format(
|
||||
new Date(proposal.terms.closingDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{isFuture(new Date(proposal.terms.enactmentDatetime))
|
||||
? t('proposedEnactment')
|
||||
: t('enactedOn')}
|
||||
|
||||
<span data-testid="governance-proposal-enactmentDate">
|
||||
{format(
|
||||
new Date(proposal.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
@ -85,9 +85,7 @@ export const useVoteInformation = ({
|
||||
|
||||
const requiredMajorityPercentage = React.useMemo(
|
||||
() =>
|
||||
requiredMajority
|
||||
? new BigNumber(requiredMajority).multipliedBy(100)
|
||||
: new BigNumber(100),
|
||||
requiredMajority ? new BigNumber(requiredMajority) : new BigNumber(100),
|
||||
[requiredMajority]
|
||||
);
|
||||
|
||||
@ -155,7 +153,9 @@ export const useVoteInformation = ({
|
||||
const willPass = React.useMemo(
|
||||
() =>
|
||||
participationMet &&
|
||||
new BigNumber(yesPercentage).isGreaterThan(requiredMajorityPercentage),
|
||||
new BigNumber(yesPercentage).isGreaterThanOrEqualTo(
|
||||
requiredMajorityPercentage
|
||||
),
|
||||
[participationMet, requiredMajorityPercentage, yesPercentage]
|
||||
);
|
||||
|
||||
|
@ -20,6 +20,12 @@ export const PROPOSALS_FRAGMENT = gql`
|
||||
metadata
|
||||
instrument {
|
||||
name
|
||||
code
|
||||
futureProduct {
|
||||
settlementAsset {
|
||||
symbol
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on UpdateMarket {
|
||||
@ -27,12 +33,15 @@ export const PROPOSALS_FRAGMENT = gql`
|
||||
}
|
||||
... on NewAsset {
|
||||
__typename
|
||||
name
|
||||
symbol
|
||||
source {
|
||||
... on BuiltinAsset {
|
||||
__typename
|
||||
maxFaucetAmountMint
|
||||
}
|
||||
... on ERC20 {
|
||||
__typename
|
||||
contractAddress
|
||||
}
|
||||
}
|
||||
|
@ -21,12 +21,36 @@ export interface Proposal_proposal_terms_change_NewFreeform {
|
||||
__typename: "NewFreeform";
|
||||
}
|
||||
|
||||
export interface Proposal_proposal_terms_change_NewMarket_instrument_futureProduct_settlementAsset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
export interface Proposal_proposal_terms_change_NewMarket_instrument_futureProduct {
|
||||
__typename: "FutureProduct";
|
||||
/**
|
||||
* Product asset ID
|
||||
*/
|
||||
settlementAsset: Proposal_proposal_terms_change_NewMarket_instrument_futureProduct_settlementAsset;
|
||||
}
|
||||
|
||||
export interface Proposal_proposal_terms_change_NewMarket_instrument {
|
||||
__typename: "InstrumentConfiguration";
|
||||
/**
|
||||
* Full and fairly descriptive name for the instrument
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18)
|
||||
*/
|
||||
code: string;
|
||||
/**
|
||||
* Future product specification
|
||||
*/
|
||||
futureProduct: Proposal_proposal_terms_change_NewMarket_instrument_futureProduct | null;
|
||||
}
|
||||
|
||||
export interface Proposal_proposal_terms_change_NewMarket {
|
||||
@ -70,6 +94,10 @@ export type Proposal_proposal_terms_change_NewAsset_source = Proposal_proposal_t
|
||||
|
||||
export interface Proposal_proposal_terms_change_NewAsset {
|
||||
__typename: "NewAsset";
|
||||
/**
|
||||
* The full name of the asset (e.g: Great British Pound)
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
|
@ -21,12 +21,36 @@ export interface Proposals_proposals_terms_change_NewFreeform {
|
||||
__typename: "NewFreeform";
|
||||
}
|
||||
|
||||
export interface Proposals_proposals_terms_change_NewMarket_instrument_futureProduct_settlementAsset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
export interface Proposals_proposals_terms_change_NewMarket_instrument_futureProduct {
|
||||
__typename: "FutureProduct";
|
||||
/**
|
||||
* Product asset ID
|
||||
*/
|
||||
settlementAsset: Proposals_proposals_terms_change_NewMarket_instrument_futureProduct_settlementAsset;
|
||||
}
|
||||
|
||||
export interface Proposals_proposals_terms_change_NewMarket_instrument {
|
||||
__typename: "InstrumentConfiguration";
|
||||
/**
|
||||
* Full and fairly descriptive name for the instrument
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18)
|
||||
*/
|
||||
code: string;
|
||||
/**
|
||||
* Future product specification
|
||||
*/
|
||||
futureProduct: Proposals_proposals_terms_change_NewMarket_instrument_futureProduct | null;
|
||||
}
|
||||
|
||||
export interface Proposals_proposals_terms_change_NewMarket {
|
||||
@ -70,6 +94,10 @@ export type Proposals_proposals_terms_change_NewAsset_source = Proposals_proposa
|
||||
|
||||
export interface Proposals_proposals_terms_change_NewAsset {
|
||||
__typename: "NewAsset";
|
||||
/**
|
||||
* The full name of the asset (e.g: Great British Pound)
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { Heading } from '../../../components/heading';
|
||||
import compact from 'lodash/compact';
|
||||
import flow from 'lodash/flow';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
@ -64,10 +63,5 @@ export const ProposalsContainer = () => {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading title={t('pageTitleGovernance')} />
|
||||
<ProposalsList proposals={proposals} />
|
||||
</>
|
||||
);
|
||||
return <ProposalsList proposals={proposals} />;
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import * as faker from 'faker';
|
||||
import merge from 'lodash/merge';
|
||||
import isArray from 'lodash/isArray';
|
||||
import mergeWith from 'lodash/mergeWith';
|
||||
|
||||
import { ProposalState, VoteValue } from '../../../__generated__/globalTypes';
|
||||
import type { DeepPartial } from '../../../lib/type-helpers';
|
||||
@ -54,19 +55,28 @@ export function generateProposal(
|
||||
},
|
||||
};
|
||||
|
||||
return merge<ProposalFields, DeepPartial<ProposalFields>>(
|
||||
return mergeWith<ProposalFields, DeepPartial<ProposalFields>>(
|
||||
defaultProposal,
|
||||
override
|
||||
override,
|
||||
(objValue, srcValue) => {
|
||||
if (!isArray(objValue)) {
|
||||
return;
|
||||
}
|
||||
return srcValue;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export const generateYesVotes = (
|
||||
numberOfVotes = 5
|
||||
numberOfVotes = 5,
|
||||
fixedTokenValue?: number
|
||||
): ProposalFields_votes_yes => {
|
||||
return {
|
||||
__typename: 'ProposalVoteSide',
|
||||
totalNumber: faker.datatype.number({ min: 0, max: 100 }).toString(),
|
||||
totalTokens: faker.datatype.number({ min: 1, max: 10000 }).toString(),
|
||||
totalTokens: faker.datatype
|
||||
.number({ min: 1, max: 10000000000000000000000 })
|
||||
.toString(),
|
||||
votes: Array.from(Array(numberOfVotes)).map(() => {
|
||||
return {
|
||||
__typename: 'Vote',
|
||||
@ -76,12 +86,14 @@ export const generateYesVotes = (
|
||||
__typename: 'Party',
|
||||
stake: {
|
||||
__typename: 'PartyStake',
|
||||
currentStakeAvailable: faker.datatype
|
||||
.number({
|
||||
min: 1,
|
||||
max: 10000,
|
||||
})
|
||||
.toString(),
|
||||
currentStakeAvailable: fixedTokenValue
|
||||
? fixedTokenValue.toString()
|
||||
: faker.datatype
|
||||
.number({
|
||||
min: 1000000000000000000,
|
||||
max: 10000000000000000000000,
|
||||
})
|
||||
.toString(),
|
||||
},
|
||||
},
|
||||
datetime: faker.date.past().toISOString(),
|
||||
@ -90,11 +102,16 @@ export const generateYesVotes = (
|
||||
};
|
||||
};
|
||||
|
||||
export const generateNoVotes = (numberOfVotes = 5): ProposalFields_votes_no => {
|
||||
export const generateNoVotes = (
|
||||
numberOfVotes = 5,
|
||||
fixedTokenValue?: number
|
||||
): ProposalFields_votes_no => {
|
||||
return {
|
||||
__typename: 'ProposalVoteSide',
|
||||
totalNumber: faker.datatype.number({ min: 0, max: 100 }).toString(),
|
||||
totalTokens: faker.datatype.number({ min: 1, max: 10000 }).toString(),
|
||||
totalTokens: faker.datatype
|
||||
.number({ min: 1000000000000000000, max: 10000000000000000000000 })
|
||||
.toString(),
|
||||
votes: Array.from(Array(numberOfVotes)).map(() => {
|
||||
return {
|
||||
__typename: 'Vote',
|
||||
@ -104,12 +121,14 @@ export const generateNoVotes = (numberOfVotes = 5): ProposalFields_votes_no => {
|
||||
__typename: 'Party',
|
||||
stake: {
|
||||
__typename: 'PartyStake',
|
||||
currentStakeAvailable: faker.datatype
|
||||
.number({
|
||||
min: 1,
|
||||
max: 10000,
|
||||
})
|
||||
.toString(),
|
||||
currentStakeAvailable: fixedTokenValue
|
||||
? fixedTokenValue.toString()
|
||||
: faker.datatype
|
||||
.number({
|
||||
min: 1000000000000000000,
|
||||
max: 10000000000000000000000,
|
||||
})
|
||||
.toString(),
|
||||
},
|
||||
},
|
||||
datetime: faker.date.past().toISOString(),
|
||||
|
59
apps/token/src/routes/governance/test-helpers/mocks.ts
Normal file
59
apps/token/src/routes/governance/test-helpers/mocks.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { NETWORK_PARAMS_QUERY } from '@vegaprotocol/web3';
|
||||
import type { VegaKeyExtended } from '@vegaprotocol/wallet';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import type { NetworkParamsQuery } from '@vegaprotocol/web3';
|
||||
|
||||
export const mockPubkey = '0x123';
|
||||
const mockKeypair = {
|
||||
pub: mockPubkey,
|
||||
} as VegaKeyExtended;
|
||||
|
||||
export const mockWalletContext = {
|
||||
keypair: mockKeypair,
|
||||
keypairs: [mockKeypair],
|
||||
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
selectPublicKey: jest.fn(),
|
||||
connector: null,
|
||||
};
|
||||
|
||||
const mockEthereumConfig = {
|
||||
network_id: '3',
|
||||
chain_id: '3',
|
||||
confirmations: 3,
|
||||
collateral_bridge_contract: {
|
||||
address: 'bridge address',
|
||||
},
|
||||
};
|
||||
|
||||
export const networkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
|
||||
request: {
|
||||
query: NETWORK_PARAMS_QUERY,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
networkParameters: [
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'blockchains.ethereumConfig',
|
||||
value: JSON.stringify(mockEthereumConfig),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const oneMinute = 1000 * 60;
|
||||
const oneHour = oneMinute * 60;
|
||||
const oneDay = oneHour * 24;
|
||||
const oneWeek = oneDay * 7;
|
||||
const oneMonth = oneWeek * 4;
|
||||
|
||||
export const fiveMinutes = new Date(oneMinute * 5);
|
||||
export const fiveHours = new Date(oneHour * 5);
|
||||
export const fiveDays = new Date(oneDay * 5);
|
||||
export const lastWeek = new Date(-oneWeek);
|
||||
export const nextWeek = new Date(oneWeek);
|
||||
export const lastMonth = new Date(-oneMonth);
|
||||
export const nextMonth = new Date(oneMonth);
|
@ -58,6 +58,7 @@ export const useOrderEdit = (order: OrderFields | null) => {
|
||||
timeInForce: VegaWalletOrderTimeInForce[order.timeInForce],
|
||||
// @ts-ignore fix me please!
|
||||
sizeDelta: 0,
|
||||
// @ts-ignore fix me please!
|
||||
expiresAt: order.expiresAt
|
||||
? {
|
||||
value: toNanoSeconds(new Date(order.expiresAt)), // Wallet expects timestamp in nanoseconds
|
||||
|
@ -8,7 +8,8 @@ export interface SplashProps {
|
||||
export const Splash = ({ children }: SplashProps) => {
|
||||
const splashClasses = classNames(
|
||||
'w-full h-full',
|
||||
'flex items-center justify-center'
|
||||
'flex items-center justify-center',
|
||||
'text-white'
|
||||
);
|
||||
return <div className={splashClasses}>{children}</div>;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user