Feat/800 better proposal ux round 1 (#1223)
* Feat/800: Configured routes for different proposal types * Feat/800: Refactored propose.tsx to be a hub for the new proposal routes * Feat/800: Link style tweak for proposals guide * Feat/927: Hydrating the new proposal forms * chore: fix typings for shared components * Feat/927: Proposal forms built with reusable components * Feat/800: Updated network params, added new method to get param keys as well as values, updated generated types * Feat/800: Updated and built more reusable proposal form components * Feat/800: Removed old catchall file of reusable proposal form components * Feat/800: Added utils for vote deadline and enactment timestamps * Feat/800: Readded necessary 'color-scheme-dark' class for dark-theme date and datetime inputs * Feat/800: Tweak to icon positioning on the dialog component * Feat/800: Regenerated types for network params * Feat/800: Added iso8601-duration package for working with API vote deadline and enactment data * Feat/800: Reworked the proposal forms * Feat/800: Couple of translation additions * Feat/800: Another translation addition * Feat/800: Raw proposal test (tests as per old proposal form test) * Feat/800: Some basic proposal form tests * Feat/800: Fixing small types error in test * Feat/800: Updating simple proposal form tests * Feat/800: Set up env-specific proposal docs links * Feat/800: Deadlines to the bottom of proposal forms * Feat/800: Another type error from API changes fixed * Feat/800: Added the spam protection min tokens network param to proposal forms requests, and the min requirements now displays the larger value of spam protection or min proposer balance * Feat/800: Network param value change now a textarea * Feat/800: Improved way to pass docs url * Feat/800: Added useful explorer links to proposal forms * Feat/800: Show current value of selected network param in proposal form * Feat/800: Removed wallet-confirmation buffer, suspect it is not necessary * Feat/800: Condense vote and enactment ui into single component for simpler state management, memoised some stuff * Feat/800: Set a default select option for the market selector * Feat/800: For network params that aren't JSON, display the network param current value in a readonly text input rather than the syntax highlighter * Feat/800: Corrected network param form network params * Feat/800: Timestamp functions for closing, enactment and validation, with tests * Feat/800: More translations * Feat/800: Added validation options to proposal-form-vote-and-enactment-deadline.tsx, generally improved the component and added tests * Feat/800: Improved workings of proposal-form-min-requirements.tsx and wrote tests * Feat/800: Tests for the other reusable form components * Feat/800: Improved tests for the proposal forms * Feat/800: Corrected mistake in get-enactment-timestamp.ts * Feat/800: Fixed type issues that were preventing spotting an error * Feat/800: Added some extra docs links * Feat/800: Using renamed Dialog passed in from useProposalSubmit * refactor: network params to return an object * fix: update net param form, remove old net params obj * fix: check for params before rendering * Feat/800: Rename and simplify isJson based off PR comment * Feat/800: Small tweaks to timestamp helpers based on PR comments * Feat/800: Removed proposal-docs-link.tsx * Feat/800: Used bignumber for min-requirements calculation * Feat/800: Update tests to work with merged network params branch * Feat/800: Removed unnecessary translations splitting * Feat/800: Removed unwanted test * Feat/800: Removed unwanted tests * Feat/800: Consistent arrow functions * Feat/800: Sorted links * Feat/800: Removed unnecessary typecasting * Feat/800: Refactored routing * Feat/800: Refactored mocking of proposal forms to use MockedProvider * Feat/800: Correct response from the raw proposal submission test * Feat/800: Removed console.logs added for debugging * Feat/800: Result of running 'nx format:write' * Feat/800: Cleaning up lint warnings * Update apps/token/.env.stagnet3 Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com> * Feat/800: Added extra test for docs link in the proposal-form-terms component * Feat/800: Removed stray console.log * Feat/800: Added basic render test for proposal-form-terms and cleaned up a bit * Feat/800: Added tests for the correct form components for the different forms * Feat/800: Split up the proposal-form-vote-and-enactment-deadline components inside the file to make things a bit more readable * Feat/800: router config webpack chunk name tweak * Feat/800: Lint issue fixed * Feat/800: Fixed timing issue with get-[deadline]-timestamp tests * Feat/800: Setting a system time in proposal-form-vote-and-enactment-deadline.spec.tsx that doesn't get affected by British Summer Time * Feat/800: Skipping a cypress test as the newProposalButton no longer takes a user directly to a proposal form. Leaving it in the codebase as the test is very likely to be updated. * Feat/800: Adding comment on why I've skipped a cypress test Co-authored-by: Dexter <dexter@vega.xyz> Co-authored-by: Matthew Russell <mattrussell36@gmail.com> Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>
This commit is contained in:
parent
e3254564ae
commit
4ef8218267
@ -10,6 +10,7 @@ import {
|
||||
formatNumber,
|
||||
t,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { suitableForSyntaxHighlighter } from '@vegaprotocol/react-helpers';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import type {
|
||||
NetworkParametersQuery,
|
||||
@ -58,7 +59,7 @@ export const NetworkParameterRow = ({
|
||||
}: {
|
||||
row: NetworkParametersQuery_networkParameters;
|
||||
}) => {
|
||||
const isSyntaxRow = isJsonObject(value);
|
||||
const isSyntaxRow = suitableForSyntaxHighlighter(value);
|
||||
|
||||
return (
|
||||
<KeyValueTableRow
|
||||
@ -85,14 +86,6 @@ export const NetworkParameterRow = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const isJsonObject = (str: string) => {
|
||||
try {
|
||||
return JSON.parse(str) && Object.keys(JSON.parse(str)).length > 0;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const NETWORK_PARAMETERS_QUERY = gql`
|
||||
query NetworkParametersQuery {
|
||||
networkParameters {
|
||||
|
@ -63,7 +63,10 @@ context(
|
||||
.and('have.text', 'There are no enacted or rejected proposals');
|
||||
});
|
||||
|
||||
it('should be able to see a connect wallet button - if vega wallet disconnected and new proposal button selected', function () {
|
||||
// Skipping this test for now, the new proposal button no longer takes a user directly
|
||||
// to a proposal form, instead it takes them to a page where they can select a proposal type.
|
||||
// Keeping this test here for now as it can be repurposed to test the new proposal forms.
|
||||
it.skip('should be able to see a connect wallet button - if vega wallet disconnected and new proposal button selected', function () {
|
||||
cy.get(newProposalButton).should('be.visible').click();
|
||||
cy.get(connectToVegaWalletButton)
|
||||
.should('be.visible')
|
||||
|
@ -7,3 +7,4 @@ NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9
|
||||
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/docs/testnet
|
||||
|
@ -7,3 +7,4 @@ NX_ETHEREUM_PROVIDER_URL=https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9
|
||||
NX_ETHERSCAN_URL=https://etherscan.io
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/docs/mainnet
|
||||
|
@ -7,6 +7,7 @@ NX_VEGA_ENV=STAGNET3
|
||||
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
|
||||
NX_VEGA_EXPLORER_URL=https://staging3.explorer.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/docs/testnet
|
||||
|
||||
# App flags
|
||||
NX_EXPLORER_ASSETS=1
|
||||
|
@ -7,3 +7,4 @@ NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9
|
||||
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/docs/testnet
|
||||
|
@ -48,6 +48,8 @@ export const ENV = {
|
||||
// Environment
|
||||
dsn: windowOrDefault('NX_SENTRY_DSN'),
|
||||
urlConnect: TRUTHY.includes(windowOrDefault('NX_ETH_URL_CONNECT')),
|
||||
explorerUrl: windowOrDefault('NX_VEGA_EXPLORER'),
|
||||
docsUrl: windowOrDefault('NX_VEGA_DOCS_URL'),
|
||||
ethWalletMnemonic: windowOrDefault('NX_ETH_WALLET_MNEMONIC'),
|
||||
localProviderUrl: windowOrDefault('NX_LOCAL_PROVIDER_URL'),
|
||||
flags: {
|
||||
|
@ -1,3 +1,3 @@
|
||||
export * from './flags';
|
||||
export * from './links';
|
||||
export * from './network-params';
|
||||
export * from './env';
|
||||
|
@ -11,4 +11,5 @@ export const Links = {
|
||||
STAKING_GUIDE:
|
||||
'https://docs.vega.xyz/docs/mainnet/concepts/vega-chain/#staking-on-vega',
|
||||
GOVERNANCE_PAGE: 'https://vega.xyz/governance',
|
||||
PROPOSALS_GUIDE: 'https://docs.vega.xyz/docs/mainnet/tutorials/proposals',
|
||||
};
|
||||
|
@ -1,25 +0,0 @@
|
||||
export const NetworkParams = {
|
||||
ETHEREUM_CONFIG: 'blockchains.ethereumConfig',
|
||||
REWARD_ASSET: 'reward.asset',
|
||||
REWARD_PAYOUT_DURATION: 'reward.staking.delegation.payoutDelay',
|
||||
GOV_UPDATE_MARKET_REQUIRED_MAJORITY:
|
||||
'governance.proposal.updateMarket.requiredMajority',
|
||||
GOV_UPDATE_MARKET_REQUIRED_PARTICIPATION:
|
||||
'governance.proposal.updateMarket.requiredParticipation',
|
||||
GOV_NEW_MARKET_REQUIRED_MAJORITY:
|
||||
'governance.proposal.market.requiredMajority',
|
||||
GOV_NEW_MARKET_REQUIRED_PARTICIPATION:
|
||||
'governance.proposal.market.requiredParticipation',
|
||||
GOV_ASSET_REQUIRED_MAJORITY: 'governance.proposal.asset.requiredMajority',
|
||||
GOV_ASSET_REQUIRED_PARTICIPATION:
|
||||
'governance.proposal.asset.requiredParticipation',
|
||||
GOV_UPDATE_NET_PARAM_REQUIRED_MAJORITY:
|
||||
'governance.proposal.updateNetParam.requiredMajority',
|
||||
GOV_UPDATE_NET_PARAM_REQUIRED_PARTICIPATION:
|
||||
'governance.proposal.updateNetParam.requiredParticipation',
|
||||
GOV_FREEFORM_REQUIRED_PARTICIPATION:
|
||||
'governance.proposal.freeform.requiredParticipation',
|
||||
GOV_FREEFORM_REQUIRED_MAJORITY:
|
||||
'governance.proposal.freeform.requiredMajority',
|
||||
VALIDATOR_DELEGATION_MIN_AMOUNT: 'validators.delegation.minAmount',
|
||||
};
|
@ -630,8 +630,59 @@
|
||||
"FilterProposalsDescription": "Filter by proposal ID or proposer ID",
|
||||
"Freeform proposal": "Freeform proposal",
|
||||
"NewProposal": "New proposal",
|
||||
"MinProposalRequirements": "You must have at least 1 VEGA associated to make a proposal",
|
||||
"ProposalTypeQuestion": "What type of proposal would you like to make?",
|
||||
"NetworkParameterProposal": "Update network parameter proposal",
|
||||
"NewMarketProposal": "New market proposal",
|
||||
"UpdateMarketProposal": "Update market proposal",
|
||||
"NewAssetProposal": "New asset proposal",
|
||||
"NewFreeformProposal": "New freeform proposal",
|
||||
"NewRawProposal": "New raw proposal",
|
||||
"MinProposalRequirements": "You must have at least {{value}} VEGA associated to make a proposal",
|
||||
"totalSupply": "Total Supply",
|
||||
"viaWallet": "via wallet",
|
||||
"viaContract": "via vesting"
|
||||
"viaContract": "via vesting",
|
||||
"ProposalDocsPrefix": "For guidance on how to make proposals, see",
|
||||
"NetworkParameter": "Network parameter",
|
||||
"NewMarket": "New market",
|
||||
"UpdateMarket": "Update market",
|
||||
"NewAsset": "New asset",
|
||||
"Freeform": "Freeform",
|
||||
"RawProposal": "Let me choose (raw proposal)",
|
||||
"UseMin": "Use minimum",
|
||||
"UseMax": "Use maximum",
|
||||
"Proposal": "Proposal",
|
||||
"ProposalRationale": "Proposal rationale",
|
||||
"ProposalTitle": "Title",
|
||||
"ProposalTitleText": "Tell people what you are proposing and why (100 characters or less)",
|
||||
"ProposalsGuide": "proposals guide",
|
||||
"ProposalDescription": "Description",
|
||||
"ProposalDescriptionText": "Full justification for what you are proposing (20,000 characters or less). Markdown is recommended. When linking to external resources please use IPFS",
|
||||
"ProposalTerms": "Proposal terms (JSON format)",
|
||||
"ProposalTermsText": "For more information visit",
|
||||
"ProposalReference": "Reference",
|
||||
"ProposalVoteTitle": "Vote deadline",
|
||||
"ProposalVoteAndEnactmentTitle": "Vote deadline and enactment",
|
||||
"ProposalVoteDeadline": "Time till voting closes",
|
||||
"ProposalEnactmentDeadline": "Time till enactment (after vote close)",
|
||||
"ProposalValidationDeadline": "Time till ERC-20 asset validation. Maximum value is affected by the vote deadline.",
|
||||
"ThisWillSetVotingDeadlineTo": "This will set the voting deadline to",
|
||||
"ThisWillSetEnactmentDeadlineTo": "This will set the enactment date to",
|
||||
"ThisWillSetValidationDeadlineTo": "This will set the validation deadline to",
|
||||
"Hours": "hours",
|
||||
"ThisWillAdd2MinutesToAllowTimeToConfirmInWallet": "Note: we add 2 minutes of extra time when you choose the minimum value. This gives you time to confirm the proposal in your wallet.",
|
||||
"SelectAMarketToChange": "Select a market to change",
|
||||
"MarketName": "Market name",
|
||||
"MarketCode": "Market code",
|
||||
"MarketId": "Market ID",
|
||||
"ProposeNewMarketTerms": "terms.changes.newMarket (JSON format)",
|
||||
"ProposeUpdateMarketTerms": "terms.updateMarket.changes (JSON format)",
|
||||
"SelectAParameterToChange": "Select a parameter to change",
|
||||
"SelectParameter": "Select parameter",
|
||||
"SelectMarket": "Select market",
|
||||
"CurrentValue": "Current value",
|
||||
"NewProposedValue": "New proposed value",
|
||||
"MoreProposalsInfo": "To see Explorer data on proposals visit",
|
||||
"MoreNetParamsInfo": "To see Explorer data on network params visit",
|
||||
"MoreMarketsInfo": "To see Explorer data on existing markets visit",
|
||||
"MoreAssetsInfo": "To see Explorer data on existing assets visit"
|
||||
}
|
||||
|
@ -6,8 +6,11 @@ import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { Heading } from '../../components/heading';
|
||||
import { SplashLoader } from '../../components/splash-loader';
|
||||
import { ENV } from '../../config/env';
|
||||
import type { RouteChildProps } from '../index';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
|
||||
const Contracts = () => {
|
||||
const Contracts = ({ name }: RouteChildProps) => {
|
||||
useDocumentTitle(name);
|
||||
const { config } = useEthereumConfig();
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
export * from './proposal-form-subheader';
|
||||
export * from './proposal-form-min-requirements';
|
||||
export * from './proposal-form-title';
|
||||
export * from './proposal-form-description';
|
||||
export * from './proposal-form-terms';
|
||||
export * from './proposal-form-submit';
|
||||
export * from './proposal-form-transaction-dialog';
|
||||
export * from './proposal-form-vote-and-enactment-deadline';
|
@ -0,0 +1,15 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalFormDescription } from './proposal-form-description';
|
||||
|
||||
describe('Proposal Form Description', () => {
|
||||
it('should display error text', () => {
|
||||
const register = jest.fn();
|
||||
render(
|
||||
<ProposalFormDescription
|
||||
registerField={register('proposalDescription')}
|
||||
errorMessage="Error text"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Error text')).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,32 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FormGroup, InputError, TextArea } from '@vegaprotocol/ui-toolkit';
|
||||
import type { UseFormRegisterReturn } from 'react-hook-form';
|
||||
|
||||
interface ProposalFormDescriptionProps {
|
||||
registerField: UseFormRegisterReturn<'proposalDescription'>;
|
||||
errorMessage: string | undefined;
|
||||
}
|
||||
|
||||
export const ProposalFormDescription = ({
|
||||
registerField: register,
|
||||
errorMessage,
|
||||
}: ProposalFormDescriptionProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<FormGroup
|
||||
label={t('ProposalDescription')}
|
||||
labelDescription={t('ProposalDescriptionText')}
|
||||
labelFor="proposal-description"
|
||||
>
|
||||
<TextArea
|
||||
id="proposal-description"
|
||||
maxLength={20000}
|
||||
className="min-h-[200px]"
|
||||
hasError={Boolean(errorMessage)}
|
||||
data-testid="proposal-description"
|
||||
{...register}
|
||||
/>
|
||||
{errorMessage && <InputError intent="danger">{errorMessage}</InputError>}
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
@ -0,0 +1,32 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { ProposalFormMinRequirements } from './proposal-form-min-requirements';
|
||||
|
||||
describe('ProposalFormMinRequirements', () => {
|
||||
it('should render successfully with spam protection value, if larger', () => {
|
||||
const { baseElement } = render(
|
||||
<ProposalFormMinRequirements
|
||||
minProposerBalance="1000000000000000000"
|
||||
spamProtectionMin="2000000000000000000"
|
||||
/>
|
||||
);
|
||||
expect(baseElement).toBeTruthy();
|
||||
|
||||
expect(baseElement).toHaveTextContent(
|
||||
'You must have at least 2 VEGA associated to make a proposal'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render successfully with min proposer value, if larger', () => {
|
||||
const { baseElement } = render(
|
||||
<ProposalFormMinRequirements
|
||||
minProposerBalance="3000000000000000000"
|
||||
spamProtectionMin="1000000000000000000"
|
||||
/>
|
||||
);
|
||||
expect(baseElement).toBeTruthy();
|
||||
|
||||
expect(baseElement).toHaveTextContent(
|
||||
'You must have at least 3 VEGA associated to make a proposal'
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,30 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { addDecimal } from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface ProposalFormMinRequirementsProps {
|
||||
minProposerBalance: string | undefined;
|
||||
spamProtectionMin: string | undefined;
|
||||
}
|
||||
|
||||
export const ProposalFormMinRequirements = ({
|
||||
minProposerBalance,
|
||||
spamProtectionMin,
|
||||
}: ProposalFormMinRequirementsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const minProposerBalanceFormatted =
|
||||
minProposerBalance && new BigNumber(addDecimal(minProposerBalance, 18));
|
||||
const spamProtectionMinFormatted =
|
||||
spamProtectionMin && new BigNumber(addDecimal(spamProtectionMin, 18));
|
||||
|
||||
const larger =
|
||||
Number(minProposerBalanceFormatted) > (spamProtectionMinFormatted || 0)
|
||||
? minProposerBalanceFormatted
|
||||
: spamProtectionMinFormatted;
|
||||
|
||||
return (
|
||||
<p className="mb-4" data-testid="min-proposal-requirements">
|
||||
{t('MinProposalRequirements', { value: larger })}
|
||||
</p>
|
||||
);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface ProposalFormSubheaderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const ProposalFormSubheader = ({
|
||||
children,
|
||||
}: ProposalFormSubheaderProps) => <h2 className="mt-8 mb-4">{children}</h2>;
|
@ -0,0 +1,24 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
interface ProposalFormSubmitProps {
|
||||
isSubmitting: boolean;
|
||||
}
|
||||
|
||||
export const ProposalFormSubmit = ({
|
||||
isSubmitting,
|
||||
}: ProposalFormSubmitProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="mt-10 my-20">
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
data-testid="proposal-submit"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? t('Submitting') : t('Submit')} {t('Proposal')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,51 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalFormTerms } from './proposal-form-terms';
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
useEnvironment: () => ({
|
||||
VEGA_DOCS_URL: 'https://docs.vega.xyz',
|
||||
}),
|
||||
}));
|
||||
|
||||
const renderComponent = () => {
|
||||
const register = jest.fn();
|
||||
render(
|
||||
<ProposalFormTerms
|
||||
registerField={register('proposalTerms')}
|
||||
errorMessage="Error text"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Proposal Form Terms', () => {
|
||||
it('should render', () => {
|
||||
renderComponent();
|
||||
expect(screen.getByTestId('proposal-terms')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display error text', () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText('Error text')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the generic docs link if no custom override', () => {
|
||||
renderComponent();
|
||||
expect(
|
||||
screen.getByText('https://docs.vega.xyz/tutorials/proposals')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the custom docs link if provided', () => {
|
||||
const register = jest.fn();
|
||||
render(
|
||||
<ProposalFormTerms
|
||||
registerField={register('proposalTerms')}
|
||||
errorMessage="Error text"
|
||||
customDocLink="/custom"
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
screen.getByText('https://docs.vega.xyz/tutorials/proposals/custom')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,51 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
FormGroup,
|
||||
InputError,
|
||||
Link,
|
||||
TextArea,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import type { UseFormRegisterReturn } from 'react-hook-form';
|
||||
|
||||
interface ProposalFormTermsProps {
|
||||
registerField: UseFormRegisterReturn<'proposalTerms'>;
|
||||
errorMessage: string | undefined;
|
||||
labelOverride?: string;
|
||||
customDocLink?: string;
|
||||
}
|
||||
|
||||
export const ProposalFormTerms = ({
|
||||
registerField: register,
|
||||
errorMessage,
|
||||
labelOverride,
|
||||
customDocLink,
|
||||
}: ProposalFormTermsProps) => {
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<FormGroup
|
||||
label={labelOverride || t('ProposalTerms')}
|
||||
labelFor="proposal-terms"
|
||||
>
|
||||
{VEGA_DOCS_URL && (
|
||||
<div className="mt-[-4px] mb-2 text-sm font-light">
|
||||
<span className="mr-1">{t('ProposalTermsText')}</span>
|
||||
<Link
|
||||
href={`${VEGA_DOCS_URL}/tutorials/proposals${customDocLink || ''}`}
|
||||
target="_blank"
|
||||
>{`${VEGA_DOCS_URL}/tutorials/proposals${customDocLink || ''}`}</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TextArea
|
||||
id="proposal-terms"
|
||||
className="min-h-[200px]"
|
||||
hasError={Boolean(errorMessage)}
|
||||
data-testid="proposal-terms"
|
||||
{...register}
|
||||
/>
|
||||
{errorMessage && <InputError intent="danger">{errorMessage}</InputError>}
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
@ -0,0 +1,15 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalFormTitle } from './proposal-form-title';
|
||||
|
||||
describe('Proposal Form Title', () => {
|
||||
it('should display error text', () => {
|
||||
const register = jest.fn();
|
||||
render(
|
||||
<ProposalFormTitle
|
||||
registerField={register('proposalTitle')}
|
||||
errorMessage="Error text"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Error text')).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,31 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import type { UseFormRegisterReturn } from 'react-hook-form';
|
||||
|
||||
interface ProposalFormTitleProps {
|
||||
registerField: UseFormRegisterReturn<'proposalTitle'>;
|
||||
errorMessage: string | undefined;
|
||||
}
|
||||
|
||||
export const ProposalFormTitle = ({
|
||||
registerField: register,
|
||||
errorMessage,
|
||||
}: ProposalFormTitleProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<FormGroup
|
||||
label={t('ProposalTitle')}
|
||||
labelFor="proposal-title"
|
||||
labelDescription={t('ProposalTitleText')}
|
||||
>
|
||||
<Input
|
||||
id="proposal-title"
|
||||
maxLength={100}
|
||||
hasError={Boolean(errorMessage)}
|
||||
data-testid="proposal-title"
|
||||
{...register}
|
||||
/>
|
||||
{errorMessage && <InputError intent="danger">{errorMessage}</InputError>}
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
import {
|
||||
getProposalDialogIcon,
|
||||
getProposalDialogIntent,
|
||||
getProposalDialogTitle,
|
||||
} from '@vegaprotocol/governance';
|
||||
import type { ProposalEvent_busEvents_event_Proposal } from '@vegaprotocol/governance';
|
||||
import type { DialogProps } from '@vegaprotocol/wallet';
|
||||
|
||||
interface ProposalFormTransactionDialogProps {
|
||||
finalizedProposal: ProposalEvent_busEvents_event_Proposal | null;
|
||||
TransactionDialog: (props: DialogProps) => JSX.Element;
|
||||
}
|
||||
|
||||
export const ProposalFormTransactionDialog = ({
|
||||
finalizedProposal,
|
||||
TransactionDialog,
|
||||
}: ProposalFormTransactionDialogProps) => (
|
||||
<div data-testid="proposal-transaction-dialog">
|
||||
<TransactionDialog
|
||||
title={getProposalDialogTitle(finalizedProposal?.state)}
|
||||
intent={getProposalDialogIntent(finalizedProposal?.state)}
|
||||
icon={getProposalDialogIcon(finalizedProposal?.state)}
|
||||
>
|
||||
{finalizedProposal?.rejectionReason ? (
|
||||
<p>{finalizedProposal.rejectionReason}</p>
|
||||
) : undefined}
|
||||
</TransactionDialog>
|
||||
</div>
|
||||
);
|
@ -0,0 +1,194 @@
|
||||
import { render, screen, fireEvent, act } from '@testing-library/react';
|
||||
import { ProposalFormVoteAndEnactmentDeadline } from './proposal-form-vote-and-enactment-deadline';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2022-01-01T00:00:00.000Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
const minVoteDeadline = '1h0m0s';
|
||||
const maxVoteDeadline = '5h0m0s';
|
||||
const minEnactDeadline = '1h0m0s';
|
||||
const maxEnactDeadline = '4h0m0s';
|
||||
|
||||
const renderComponent = () => {
|
||||
const register = jest.fn();
|
||||
render(
|
||||
<ProposalFormVoteAndEnactmentDeadline
|
||||
voteRegister={register('proposalVoteDeadline')}
|
||||
voteErrorMessage={undefined}
|
||||
voteMinClose={minVoteDeadline}
|
||||
voteMaxClose={maxVoteDeadline}
|
||||
enactmentRegister={register('proposalEnactmentDeadline')}
|
||||
enactmentErrorMessage={undefined}
|
||||
enactmentMinClose={minEnactDeadline}
|
||||
enactmentMaxClose={maxEnactDeadline}
|
||||
validationRequired={true}
|
||||
validationRegister={register('proposalValidationDeadline')}
|
||||
validationErrorMessage={undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Proposal form vote, validation and enactment deadline', () => {
|
||||
it('should render all component parts successfully', () => {
|
||||
renderComponent();
|
||||
expect(screen.getByTestId('proposal-vote-deadline')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-enactment-deadline')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-validation-deadline')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have the correct initial values', () => {
|
||||
renderComponent();
|
||||
const deadlineInput = screen.getByTestId('proposal-vote-deadline');
|
||||
expect(deadlineInput).toHaveValue(1);
|
||||
const enactmentDeadlineInput = screen.getByTestId(
|
||||
'proposal-enactment-deadline'
|
||||
);
|
||||
expect(enactmentDeadlineInput).toHaveValue(1);
|
||||
const validationDeadlineInput = screen.getByTestId(
|
||||
'proposal-validation-deadline'
|
||||
);
|
||||
expect(validationDeadlineInput).toHaveValue(0);
|
||||
});
|
||||
|
||||
it('should use the voting max and min values when the buttons are clicked', () => {
|
||||
renderComponent();
|
||||
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
|
||||
const maxButton = screen.getByTestId('max-vote');
|
||||
const minButton = screen.getByTestId('min-vote');
|
||||
fireEvent.click(maxButton);
|
||||
expect(voteDeadlineInput).toHaveValue(5);
|
||||
fireEvent.click(minButton);
|
||||
expect(voteDeadlineInput).toHaveValue(1);
|
||||
});
|
||||
|
||||
it('should use the validation max and min values when the buttons are clicked', () => {
|
||||
renderComponent();
|
||||
const validationDeadlineInput = screen.getByTestId(
|
||||
'proposal-validation-deadline'
|
||||
);
|
||||
const maxButton = screen.getByTestId('max-validation');
|
||||
const minButton = screen.getByTestId('min-validation');
|
||||
fireEvent.click(maxButton);
|
||||
// Note: the validation max value is determined by the vote deadline max value,
|
||||
// which will currently be the default value of 1
|
||||
expect(validationDeadlineInput).toHaveValue(1);
|
||||
fireEvent.click(minButton);
|
||||
expect(validationDeadlineInput).toHaveValue(0);
|
||||
});
|
||||
|
||||
it('should update the validation deadline max value when the vote deadline value changes', () => {
|
||||
renderComponent();
|
||||
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
|
||||
const validationDeadlineInput = screen.getByTestId(
|
||||
'proposal-validation-deadline'
|
||||
);
|
||||
const maxButton = screen.getByTestId('max-validation');
|
||||
fireEvent.change(voteDeadlineInput, { target: { value: 2 } });
|
||||
fireEvent.click(maxButton);
|
||||
expect(validationDeadlineInput).toHaveValue(2);
|
||||
});
|
||||
|
||||
it('should use the enactment max and min values when the buttons are clicked', () => {
|
||||
renderComponent();
|
||||
const enactmentDeadlineInput = screen.getByTestId(
|
||||
'proposal-enactment-deadline'
|
||||
);
|
||||
const maxButton = screen.getByTestId('max-enactment');
|
||||
const minButton = screen.getByTestId('min-enactment');
|
||||
fireEvent.click(maxButton);
|
||||
expect(enactmentDeadlineInput).toHaveValue(4);
|
||||
fireEvent.click(minButton);
|
||||
expect(enactmentDeadlineInput).toHaveValue(1);
|
||||
});
|
||||
|
||||
it('should show the 2 mins extra message when the vote deadline is set to minimum', () => {
|
||||
renderComponent();
|
||||
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
|
||||
const minButton = screen.getByTestId('min-vote');
|
||||
fireEvent.click(minButton);
|
||||
expect(voteDeadlineInput).toHaveValue(1);
|
||||
expect(screen.getByTestId('voting-2-mins-extra')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should show the 2 mins extra message when the validation deadline is set to minimum', () => {
|
||||
renderComponent();
|
||||
const validationDeadlineInput = screen.getByTestId(
|
||||
'proposal-validation-deadline'
|
||||
);
|
||||
const minButton = screen.getByTestId('min-validation');
|
||||
fireEvent.click(minButton);
|
||||
expect(validationDeadlineInput).toHaveValue(0);
|
||||
expect(screen.getByTestId('validation-2-mins-extra')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should show the correct datetimes', () => {
|
||||
renderComponent();
|
||||
// Should be adding 2 mins to the vote deadline as the minimum is set by
|
||||
// default, and we add 2 mins for wallet confirmation
|
||||
expect(screen.getByTestId('voting-date')).toHaveTextContent(
|
||||
'1/1/2022, 1:02:00 AM'
|
||||
);
|
||||
expect(screen.getByTestId('validation-date')).toHaveTextContent(
|
||||
'1/1/2022, 12:02:00 AM'
|
||||
);
|
||||
expect(screen.getByTestId('enactment-date')).toHaveTextContent(
|
||||
'1/1/2022, 2:00:00 AM'
|
||||
);
|
||||
});
|
||||
|
||||
it('should be updating every second, so show the correct datetimes when 30 seconds have passed', () => {
|
||||
renderComponent();
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(30000);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('voting-date')).toHaveTextContent(
|
||||
'1/1/2022, 1:02:30 AM'
|
||||
);
|
||||
expect(screen.getByTestId('validation-date')).toHaveTextContent(
|
||||
'1/1/2022, 12:02:30 AM'
|
||||
);
|
||||
expect(screen.getByTestId('enactment-date')).toHaveTextContent(
|
||||
'1/1/2022, 2:00:30 AM'
|
||||
);
|
||||
});
|
||||
|
||||
it('update the vote deadline date and the enactment deadline date when the vote deadline is changed', () => {
|
||||
renderComponent();
|
||||
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
|
||||
fireEvent.change(voteDeadlineInput, { target: { value: 2 } });
|
||||
expect(screen.getByTestId('voting-date')).toHaveTextContent(
|
||||
'1/1/2022, 2:00:00 AM'
|
||||
);
|
||||
expect(screen.getByTestId('enactment-date')).toHaveTextContent(
|
||||
'1/1/2022, 3:00:00 AM'
|
||||
);
|
||||
});
|
||||
|
||||
it('updates the validation deadline max and date when the vote deadline max is changed', () => {
|
||||
renderComponent();
|
||||
const voteDeadlineMinButton = screen.getByTestId('min-vote');
|
||||
const voteDeadlineMaxButton = screen.getByTestId('max-vote');
|
||||
const validationDeadlineMaxButton = screen.getByTestId('max-validation');
|
||||
const validationDeadlineInput = screen.getByTestId(
|
||||
'proposal-validation-deadline'
|
||||
);
|
||||
fireEvent.click(voteDeadlineMaxButton);
|
||||
fireEvent.click(validationDeadlineMaxButton);
|
||||
expect(screen.getByTestId('validation-date')).toHaveTextContent(
|
||||
'1/1/2022, 5:00:00 AM'
|
||||
);
|
||||
expect(validationDeadlineInput).toHaveValue(5);
|
||||
fireEvent.click(voteDeadlineMinButton);
|
||||
expect(screen.getByTestId('validation-date')).toHaveTextContent(
|
||||
'1/1/2022, 1:00:00 AM'
|
||||
);
|
||||
expect(validationDeadlineInput).toHaveValue(1);
|
||||
});
|
||||
});
|
@ -0,0 +1,446 @@
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { parse as ISO8601Parse, toSeconds } from 'iso8601-duration';
|
||||
import {
|
||||
ButtonLink,
|
||||
FormGroup,
|
||||
Input,
|
||||
InputError,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { addHours, addMinutes } from 'date-fns';
|
||||
import { ProposalFormSubheader } from './proposal-form-subheader';
|
||||
import type { UseFormRegisterReturn } from 'react-hook-form';
|
||||
|
||||
interface DeadlineProps {
|
||||
vote: number;
|
||||
enactment: number;
|
||||
validation: number;
|
||||
}
|
||||
|
||||
interface DeadlineDatesProps {
|
||||
vote: Date;
|
||||
enactment: Date;
|
||||
validation: Date;
|
||||
}
|
||||
|
||||
interface ValidationFormProps {
|
||||
validationRegister:
|
||||
| UseFormRegisterReturn<'proposalValidationDeadline'>
|
||||
| undefined;
|
||||
deadlines: DeadlineProps;
|
||||
deadlineDates: DeadlineDatesProps;
|
||||
updateValidationDeadlineAndDate: (hours: number) => void;
|
||||
validationErrorMessage: string | undefined;
|
||||
}
|
||||
|
||||
const ValidationForm = ({
|
||||
validationRegister,
|
||||
deadlines,
|
||||
deadlineDates,
|
||||
updateValidationDeadlineAndDate,
|
||||
validationErrorMessage,
|
||||
}: ValidationFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label={t('ProposalValidationDeadline')}
|
||||
labelFor="proposal-validation-deadline"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative w-28">
|
||||
<Input
|
||||
{...validationRegister}
|
||||
id="proposal-validation-deadline"
|
||||
type="number"
|
||||
value={deadlines.validation}
|
||||
min={0}
|
||||
max={deadlines.vote}
|
||||
onChange={(e) => {
|
||||
updateValidationDeadlineAndDate(Number(e.target.value));
|
||||
}}
|
||||
data-testid="proposal-validation-deadline"
|
||||
className="pr-12"
|
||||
/>
|
||||
<span className="absolute right-2 bottom-1/2 translate-y-1/2 text-sm">
|
||||
{t('Hours')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<ButtonLink
|
||||
data-testid="min-validation"
|
||||
onClick={() => updateValidationDeadlineAndDate(0)}
|
||||
>
|
||||
{t('UseMin')}
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
data-testid="max-validation"
|
||||
onClick={() => updateValidationDeadlineAndDate(deadlines.vote)}
|
||||
>
|
||||
{t('UseMax')}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
{validationErrorMessage && (
|
||||
<InputError intent="danger">{validationErrorMessage}</InputError>
|
||||
)}
|
||||
|
||||
{deadlineDates.validation && (
|
||||
<p className="mt-2 text-sm text-white">
|
||||
<span className="font-light">
|
||||
{t('ThisWillSetValidationDeadlineTo')}
|
||||
</span>
|
||||
<span data-testid="validation-date" className="pl-2">
|
||||
{deadlineDates.validation?.toLocaleString()}
|
||||
</span>
|
||||
{deadlines.validation === 0 && (
|
||||
<span
|
||||
data-testid="validation-2-mins-extra"
|
||||
className="block mt-4 font-light"
|
||||
>
|
||||
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
interface EnactmentFormProps {
|
||||
enactmentRegister:
|
||||
| UseFormRegisterReturn<'proposalEnactmentDeadline'>
|
||||
| undefined;
|
||||
deadlines: DeadlineProps;
|
||||
deadlineDates: DeadlineDatesProps;
|
||||
updateEnactmentDeadlineAndDate: (hours: number) => void;
|
||||
enactmentErrorMessage: string | undefined;
|
||||
minEnactmentHours: number;
|
||||
maxEnactmentHours: number;
|
||||
}
|
||||
|
||||
const EnactmentForm = ({
|
||||
enactmentRegister,
|
||||
deadlines,
|
||||
deadlineDates,
|
||||
updateEnactmentDeadlineAndDate,
|
||||
enactmentErrorMessage,
|
||||
minEnactmentHours,
|
||||
maxEnactmentHours,
|
||||
}: EnactmentFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label={t('ProposalEnactmentDeadline')}
|
||||
labelFor="proposal-enactment-deadline"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative w-28">
|
||||
<Input
|
||||
{...enactmentRegister}
|
||||
id="proposal-enactment-deadline"
|
||||
type="number"
|
||||
value={deadlines.enactment}
|
||||
min={minEnactmentHours}
|
||||
max={maxEnactmentHours}
|
||||
onChange={(e) => {
|
||||
updateEnactmentDeadlineAndDate(Number(e.target.value));
|
||||
}}
|
||||
data-testid="proposal-enactment-deadline"
|
||||
className="pr-12"
|
||||
/>
|
||||
<span className="absolute right-2 bottom-1/2 translate-y-1/2 text-sm">
|
||||
{t('Hours')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<ButtonLink
|
||||
data-testid="min-enactment"
|
||||
onClick={() => updateEnactmentDeadlineAndDate(minEnactmentHours)}
|
||||
>
|
||||
{t('UseMin')}
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
data-testid="max-enactment"
|
||||
onClick={() => updateEnactmentDeadlineAndDate(maxEnactmentHours)}
|
||||
>
|
||||
{t('UseMax')}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
{enactmentErrorMessage && (
|
||||
<InputError intent="danger">{enactmentErrorMessage}</InputError>
|
||||
)}
|
||||
|
||||
{deadlineDates.enactment && (
|
||||
<p className="mt-2 text-sm text-white">
|
||||
<span className="font-light">
|
||||
{t('ThisWillSetEnactmentDeadlineTo')}
|
||||
</span>
|
||||
<span data-testid="enactment-date" className="pl-2">
|
||||
{deadlineDates.enactment?.toLocaleString()}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export interface ProposalFormVoteAndEnactmentDeadlineProps {
|
||||
voteRegister: UseFormRegisterReturn<'proposalVoteDeadline'>;
|
||||
voteErrorMessage: string | undefined;
|
||||
voteMinClose: string;
|
||||
voteMaxClose: string;
|
||||
enactmentRequired?: boolean;
|
||||
enactmentRegister?: UseFormRegisterReturn<'proposalEnactmentDeadline'>;
|
||||
enactmentErrorMessage?: string;
|
||||
enactmentMinClose?: string;
|
||||
enactmentMaxClose?: string;
|
||||
validationRequired?: boolean;
|
||||
validationRegister?: UseFormRegisterReturn<'proposalValidationDeadline'>;
|
||||
validationErrorMessage?: string;
|
||||
}
|
||||
|
||||
export const ProposalFormVoteAndEnactmentDeadline = ({
|
||||
voteRegister,
|
||||
voteErrorMessage,
|
||||
voteMinClose,
|
||||
voteMaxClose,
|
||||
enactmentRegister,
|
||||
enactmentErrorMessage,
|
||||
enactmentMinClose,
|
||||
enactmentMaxClose,
|
||||
validationRequired,
|
||||
validationRegister,
|
||||
validationErrorMessage,
|
||||
}: ProposalFormVoteAndEnactmentDeadlineProps) => {
|
||||
const {
|
||||
minVoteSeconds,
|
||||
maxVoteSeconds,
|
||||
minEnactmentSeconds,
|
||||
maxEnactmentSeconds,
|
||||
} = useMemo(
|
||||
() => ({
|
||||
minVoteSeconds: toSeconds(
|
||||
ISO8601Parse(`PT${voteMinClose.toUpperCase()}`)
|
||||
),
|
||||
maxVoteSeconds: toSeconds(
|
||||
ISO8601Parse(`PT${voteMaxClose.toUpperCase()}`)
|
||||
),
|
||||
minEnactmentSeconds:
|
||||
enactmentMinClose &&
|
||||
toSeconds(ISO8601Parse(`PT${enactmentMinClose.toUpperCase()}`)),
|
||||
maxEnactmentSeconds:
|
||||
enactmentMaxClose &&
|
||||
toSeconds(ISO8601Parse(`PT${enactmentMaxClose.toUpperCase()}`)),
|
||||
}),
|
||||
[voteMinClose, voteMaxClose, enactmentMinClose, enactmentMaxClose]
|
||||
);
|
||||
|
||||
// As we're rounding to hours for the simplified deadline ui presently, if vote deadline
|
||||
// is less than one hour, make it one hour.
|
||||
const { minVoteHours, maxVoteHours, minEnactmentHours, maxEnactmentHours } =
|
||||
useMemo(
|
||||
() => ({
|
||||
minVoteHours:
|
||||
Math.floor(minVoteSeconds / 3600) > 1
|
||||
? Math.floor(minVoteSeconds / 3600)
|
||||
: 1,
|
||||
maxVoteHours: Math.floor(maxVoteSeconds / 3600),
|
||||
minEnactmentHours:
|
||||
minEnactmentSeconds && Math.floor(minEnactmentSeconds / 3600) > 1
|
||||
? Math.floor(minEnactmentSeconds / 3600)
|
||||
: 1,
|
||||
maxEnactmentHours:
|
||||
maxEnactmentSeconds && Math.floor(maxEnactmentSeconds / 3600),
|
||||
}),
|
||||
[minVoteSeconds, maxVoteSeconds, minEnactmentSeconds, maxEnactmentSeconds]
|
||||
);
|
||||
|
||||
const [deadlines, setDeadlines] = useState<DeadlineProps>({
|
||||
vote: minVoteHours,
|
||||
enactment: minEnactmentHours,
|
||||
validation: 0,
|
||||
});
|
||||
|
||||
const [deadlineDates, setDeadlineDates] = useState<DeadlineDatesProps>({
|
||||
vote:
|
||||
deadlines.vote === minVoteHours
|
||||
? addHours(addMinutes(new Date(), 2), deadlines.vote)
|
||||
: addHours(new Date(), deadlines.vote),
|
||||
enactment: addHours(new Date(), deadlines.vote + deadlines.enactment),
|
||||
validation:
|
||||
deadlines.validation === 0
|
||||
? addHours(addMinutes(new Date(), 2), deadlines.validation)
|
||||
: addHours(new Date(), deadlines.validation),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setDeadlineDates((prev) => ({
|
||||
...prev,
|
||||
vote:
|
||||
deadlines.vote === minVoteHours
|
||||
? addHours(addMinutes(new Date(), 2), deadlines.vote)
|
||||
: addHours(new Date(), deadlines.vote),
|
||||
enactment: addHours(new Date(), deadlines.vote + deadlines.enactment),
|
||||
validation:
|
||||
deadlines.validation === 0
|
||||
? addHours(addMinutes(new Date(), 2), deadlines.validation)
|
||||
: addHours(new Date(), deadlines.validation),
|
||||
}));
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [deadlines, minVoteHours]);
|
||||
|
||||
const updateVoteDeadlineAndDate = (hours: number) => {
|
||||
// Validation, when needed, can only happen within the voting period. Therefore, if the
|
||||
// vote deadline is changed, the validation deadline must be changed to be within the
|
||||
// new vote deadline.
|
||||
setDeadlines((prev) => ({
|
||||
...prev,
|
||||
vote: hours,
|
||||
validation: Math.min(prev.validation, hours),
|
||||
}));
|
||||
|
||||
// If the vote deadline is set to minimum, add 2 mins to the date as we do
|
||||
// this on submission to allow time to confirm in the wallet. Amending the
|
||||
// vote deadline also changes the enactment date and potentially the validation
|
||||
// date.
|
||||
// The validation deadline date cannot be after the vote deadline date. Therefore,
|
||||
// if the vote deadline is changed, the validation deadline must potentially
|
||||
// be changed to be within the new vote deadline.
|
||||
setDeadlineDates((prev) => ({
|
||||
...prev,
|
||||
vote:
|
||||
hours === minVoteHours
|
||||
? addHours(addMinutes(new Date(), 2), hours)
|
||||
: addHours(new Date(), hours),
|
||||
enactment: addHours(new Date(), hours + deadlines.enactment),
|
||||
validation: addHours(new Date(), Math.min(hours, deadlines.validation)),
|
||||
}));
|
||||
};
|
||||
|
||||
const updateEnactmentDeadlineAndDate = (hours: number) => {
|
||||
setDeadlines((prev) => ({
|
||||
...prev,
|
||||
enactment: hours,
|
||||
}));
|
||||
|
||||
setDeadlineDates((prev) => ({
|
||||
...prev,
|
||||
enactment: addHours(deadlineDates.vote, hours),
|
||||
}));
|
||||
};
|
||||
|
||||
const updateValidationDeadlineAndDate = (hours: number) => {
|
||||
setDeadlines((prev) => ({
|
||||
...prev,
|
||||
validation: hours,
|
||||
}));
|
||||
|
||||
setDeadlineDates((prev) => ({
|
||||
...prev,
|
||||
validation:
|
||||
hours === 0
|
||||
? addHours(addMinutes(new Date(), 2), hours)
|
||||
: addHours(new Date(), hours),
|
||||
}));
|
||||
};
|
||||
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<ProposalFormSubheader>
|
||||
{enactmentRegister && enactmentMinClose && enactmentMaxClose
|
||||
? t('ProposalVoteAndEnactmentTitle')
|
||||
: t('ProposalVoteTitle')}
|
||||
</ProposalFormSubheader>
|
||||
|
||||
<FormGroup
|
||||
label={t('ProposalVoteDeadline')}
|
||||
labelFor="proposal-vote-deadline"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative w-28">
|
||||
<Input
|
||||
{...voteRegister}
|
||||
id="proposal-vote-deadline"
|
||||
type="number"
|
||||
value={deadlines.vote}
|
||||
min={minVoteHours}
|
||||
max={maxVoteHours}
|
||||
onChange={(e) => {
|
||||
updateVoteDeadlineAndDate(Number(e.target.value));
|
||||
}}
|
||||
data-testid="proposal-vote-deadline"
|
||||
className="pr-12"
|
||||
/>
|
||||
<span className="absolute right-2 bottom-1/2 translate-y-1/2 text-sm">
|
||||
{t('Hours')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<ButtonLink
|
||||
data-testid="min-vote"
|
||||
onClick={() => updateVoteDeadlineAndDate(minVoteHours)}
|
||||
>
|
||||
{t('UseMin')}
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
data-testid="max-vote"
|
||||
onClick={() => updateVoteDeadlineAndDate(maxVoteHours)}
|
||||
>
|
||||
{t('UseMax')}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
{voteErrorMessage && (
|
||||
<InputError intent="danger">{voteErrorMessage}</InputError>
|
||||
)}
|
||||
|
||||
{deadlineDates.vote && (
|
||||
<p className="mt-2 text-sm text-white">
|
||||
<span className="font-light">
|
||||
{t('ThisWillSetVotingDeadlineTo')}
|
||||
</span>
|
||||
<span data-testid="voting-date" className="pl-2">
|
||||
{deadlineDates.vote?.toLocaleString()}
|
||||
</span>
|
||||
{deadlines.vote === minVoteHours && (
|
||||
<span
|
||||
data-testid="voting-2-mins-extra"
|
||||
className="block mt-4 font-light"
|
||||
>
|
||||
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</FormGroup>
|
||||
|
||||
{validationRequired && (
|
||||
<ValidationForm
|
||||
validationRegister={validationRegister}
|
||||
deadlines={deadlines}
|
||||
deadlineDates={deadlineDates}
|
||||
updateValidationDeadlineAndDate={updateValidationDeadlineAndDate}
|
||||
validationErrorMessage={validationErrorMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{enactmentMinClose && enactmentMaxClose && maxEnactmentHours && (
|
||||
<EnactmentForm
|
||||
enactmentRegister={enactmentRegister}
|
||||
deadlines={deadlines}
|
||||
deadlineDates={deadlineDates}
|
||||
updateEnactmentDeadlineAndDate={updateEnactmentDeadlineAndDate}
|
||||
enactmentErrorMessage={enactmentErrorMessage}
|
||||
minEnactmentHours={minEnactmentHours}
|
||||
maxEnactmentHours={maxEnactmentHours}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,7 +1,6 @@
|
||||
import { useNetworkParams } from '@vegaprotocol/react-helpers';
|
||||
import { useNetworkParams, NetworkParams } from '@vegaprotocol/react-helpers';
|
||||
import React from 'react';
|
||||
|
||||
import { NetworkParams } from '../../../config';
|
||||
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
||||
import { BigNumber } from '../../../lib/bignumber';
|
||||
import { addDecimal } from '../../../lib/decimals';
|
||||
@ -16,63 +15,63 @@ const useProposalNetworkParams = ({
|
||||
}: {
|
||||
proposal: ProposalFields;
|
||||
}) => {
|
||||
const { data, loading } = useNetworkParams([
|
||||
NetworkParams.GOV_UPDATE_MARKET_REQUIRED_MAJORITY,
|
||||
NetworkParams.GOV_UPDATE_MARKET_REQUIRED_PARTICIPATION,
|
||||
NetworkParams.GOV_NEW_MARKET_REQUIRED_MAJORITY,
|
||||
NetworkParams.GOV_NEW_MARKET_REQUIRED_PARTICIPATION,
|
||||
NetworkParams.GOV_ASSET_REQUIRED_MAJORITY,
|
||||
NetworkParams.GOV_ASSET_REQUIRED_PARTICIPATION,
|
||||
NetworkParams.GOV_UPDATE_NET_PARAM_REQUIRED_MAJORITY,
|
||||
NetworkParams.GOV_UPDATE_NET_PARAM_REQUIRED_PARTICIPATION,
|
||||
NetworkParams.GOV_FREEFORM_REQUIRED_MAJORITY,
|
||||
NetworkParams.GOV_FREEFORM_REQUIRED_PARTICIPATION,
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredParticipation,
|
||||
NetworkParams.governance_proposal_market_requiredMajority,
|
||||
NetworkParams.governance_proposal_market_requiredParticipation,
|
||||
NetworkParams.governance_proposal_asset_requiredMajority,
|
||||
NetworkParams.governance_proposal_asset_requiredParticipation,
|
||||
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateNetParam_requiredParticipation,
|
||||
NetworkParams.governance_proposal_freeform_requiredMajority,
|
||||
NetworkParams.governance_proposal_freeform_requiredParticipation,
|
||||
]);
|
||||
if (loading || !data) {
|
||||
|
||||
if (!params) {
|
||||
return {
|
||||
requiredMajority: new BigNumber(1),
|
||||
requiredParticipation: new BigNumber(1),
|
||||
};
|
||||
}
|
||||
|
||||
const [
|
||||
updateMarketMajority,
|
||||
updateMarketParticipation,
|
||||
newMarketMajority,
|
||||
newMarketParticipation,
|
||||
assetMajority,
|
||||
assetParticipation,
|
||||
paramMajority,
|
||||
paramParticipation,
|
||||
freeformMajority,
|
||||
freeformParticipation,
|
||||
] = data;
|
||||
|
||||
switch (proposal.terms.change.__typename) {
|
||||
case 'UpdateMarket':
|
||||
return {
|
||||
requiredMajority: updateMarketMajority,
|
||||
requiredParticipation: new BigNumber(updateMarketParticipation),
|
||||
requiredMajority:
|
||||
params.governance_proposal_updateMarket_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'UpdateNetworkParameter':
|
||||
return {
|
||||
requiredMajority: paramMajority,
|
||||
requiredParticipation: new BigNumber(paramParticipation),
|
||||
requiredMajority:
|
||||
params.governance_proposal_updateNetParam_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateNetParam_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'NewAsset':
|
||||
return {
|
||||
requiredMajority: assetMajority,
|
||||
requiredParticipation: new BigNumber(assetParticipation),
|
||||
requiredMajority: params.governance_proposal_asset_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_asset_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'NewMarket':
|
||||
return {
|
||||
requiredMajority: newMarketMajority,
|
||||
requiredParticipation: new BigNumber(newMarketParticipation),
|
||||
requiredMajority: params.governance_proposal_market_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_market_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'NewFreeform':
|
||||
return {
|
||||
requiredMajority: freeformMajority,
|
||||
requiredParticipation: freeformParticipation,
|
||||
requiredMajority: params.governance_proposal_freeform_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_freeform_requiredParticipation
|
||||
),
|
||||
};
|
||||
default:
|
||||
throw new Error('Unknown proposal type');
|
||||
|
@ -0,0 +1,4 @@
|
||||
export {
|
||||
ProposeFreeform,
|
||||
ProposeFreeform as default,
|
||||
} from './propose-freeform';
|
@ -0,0 +1,109 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { ProposeFreeform } from './propose-freeform';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { mockWalletContext } from '../../test-helpers/mocks';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { MemoryRouter as Router } from 'react-router-dom';
|
||||
import { gql } from '@apollo/client';
|
||||
import type { NetworkParamsQuery } from '@vegaprotocol/web3';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
useEnvironment: () => ({
|
||||
VEGA_DOCS_URL: 'https://docs.vega.xyz',
|
||||
}),
|
||||
}));
|
||||
|
||||
const updateMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
|
||||
request: {
|
||||
query: gql`
|
||||
query NetworkParams {
|
||||
networkParameters {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
networkParameters: [
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.freeform.maxClose',
|
||||
value: '8760h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.freeform.maxEnact',
|
||||
value: '8760h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.freeform.minClose',
|
||||
value: '1h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.freeform.minEnact',
|
||||
value: '2h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.freeform.minProposerBalance',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'spam.protection.proposal.min.tokens',
|
||||
value: '1000000000000000000',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<Router>
|
||||
<MockedProvider mocks={[updateMarketNetworkParamsQueryMock]}>
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposeFreeform />
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
</MockedProvider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
// Note: form submission is tested in propose-raw.spec.tsx. Reusable form
|
||||
// components are tested in their own directory.
|
||||
|
||||
describe('Propose Freeform', () => {
|
||||
it('should render successfully', async () => {
|
||||
const { baseElement } = renderComponent();
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the title', async () => {
|
||||
renderComponent();
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('New freeform proposal')).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the form components', async () => {
|
||||
renderComponent();
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('freeform-proposal-form')).toBeTruthy()
|
||||
);
|
||||
expect(screen.getByTestId('min-proposal-requirements')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-docs-link')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-title')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-description')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-vote-deadline')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-submit')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-transaction-dialog')).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,135 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
getClosingTimestamp,
|
||||
useProposalSubmit,
|
||||
} from '@vegaprotocol/governance';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import {
|
||||
ProposalFormSubheader,
|
||||
ProposalFormMinRequirements,
|
||||
ProposalFormTitle,
|
||||
ProposalFormDescription,
|
||||
ProposalFormSubmit,
|
||||
ProposalFormTransactionDialog,
|
||||
ProposalFormVoteAndEnactmentDeadline,
|
||||
} from '../../components/propose';
|
||||
import { AsyncRenderer, Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { Heading } from '../../../../components/heading';
|
||||
import { VegaWalletContainer } from '../../../../components/vega-wallet-container';
|
||||
import { useNetworkParams, NetworkParams } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export interface FreeformProposalFormFields {
|
||||
proposalVoteDeadline: string;
|
||||
proposalTitle: string;
|
||||
proposalDescription: string;
|
||||
proposalTerms: string;
|
||||
proposalReference: string;
|
||||
}
|
||||
|
||||
const docsLink = 'freeform-proposal';
|
||||
|
||||
export const ProposeFreeform = () => {
|
||||
const { params, loading, error } = useNetworkParams([
|
||||
NetworkParams.governance_proposal_freeform_minClose,
|
||||
NetworkParams.governance_proposal_freeform_maxClose,
|
||||
NetworkParams.governance_proposal_freeform_minProposerBalance,
|
||||
NetworkParams.spam_protection_proposal_min_tokens,
|
||||
]);
|
||||
|
||||
const { VEGA_DOCS_URL, VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useForm<FreeformProposalFormFields>();
|
||||
const { finalizedProposal, submit, Dialog } = useProposalSubmit();
|
||||
|
||||
const onSubmit = async (fields: FreeformProposalFormFields) => {
|
||||
await submit({
|
||||
rationale: {
|
||||
title: fields.proposalTitle,
|
||||
description: fields.proposalDescription,
|
||||
},
|
||||
terms: {
|
||||
newFreeform: {},
|
||||
closingTimestamp: getClosingTimestamp(fields.proposalVoteDeadline),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncRenderer loading={loading} error={error} data={params}>
|
||||
<Heading title={t('NewFreeformProposal')} />
|
||||
<VegaWalletContainer>
|
||||
{() => (
|
||||
<>
|
||||
<ProposalFormMinRequirements
|
||||
minProposerBalance={
|
||||
params.governance_proposal_freeform_minProposerBalance
|
||||
}
|
||||
spamProtectionMin={params.spam_protection_proposal_min_tokens}
|
||||
/>
|
||||
|
||||
{VEGA_DOCS_URL && (
|
||||
<p className="text-sm" data-testid="proposal-docs-link">
|
||||
<span className="mr-1">{t('ProposalTermsText')}</span>
|
||||
<Link
|
||||
href={`${VEGA_DOCS_URL}/tutorials/proposals/${docsLink}`}
|
||||
target="_blank"
|
||||
>{`${VEGA_DOCS_URL}/tutorials/proposals/${docsLink}`}</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{VEGA_EXPLORER_URL && (
|
||||
<p className="text-sm">
|
||||
{t('MoreProposalsInfo')}{' '}
|
||||
<Link
|
||||
href={`${VEGA_EXPLORER_URL}/governance`}
|
||||
target="_blank"
|
||||
>{`${VEGA_EXPLORER_URL}/governance`}</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div data-testid="freeform-proposal-form">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<ProposalFormSubheader>
|
||||
{t('ProposalRationale')}
|
||||
</ProposalFormSubheader>
|
||||
|
||||
<ProposalFormTitle
|
||||
registerField={register('proposalTitle', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
errorMessage={errors?.proposalTitle?.message}
|
||||
/>
|
||||
<ProposalFormDescription
|
||||
registerField={register('proposalDescription', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
errorMessage={errors?.proposalDescription?.message}
|
||||
/>
|
||||
|
||||
<ProposalFormVoteAndEnactmentDeadline
|
||||
voteRegister={register('proposalVoteDeadline', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
voteErrorMessage={errors?.proposalVoteDeadline?.message}
|
||||
voteMinClose={params.governance_proposal_freeform_minClose}
|
||||
voteMaxClose={params.governance_proposal_freeform_maxClose}
|
||||
/>
|
||||
|
||||
<ProposalFormSubmit isSubmitting={isSubmitting} />
|
||||
<ProposalFormTransactionDialog
|
||||
finalizedProposal={finalizedProposal}
|
||||
TransactionDialog={Dialog}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</VegaWalletContainer>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export {
|
||||
ProposeNetworkParameter,
|
||||
ProposeNetworkParameter as default,
|
||||
} from './propose-network-parameter';
|
@ -0,0 +1,141 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { ProposeNetworkParameter } from './propose-network-parameter';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { mockWalletContext } from '../../test-helpers/mocks';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { MemoryRouter as Router } from 'react-router-dom';
|
||||
import { gql } from '@apollo/client';
|
||||
import type { NetworkParamsQuery } from '@vegaprotocol/web3';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
useEnvironment: () => ({
|
||||
VEGA_DOCS_URL: 'https://docs.vega.xyz',
|
||||
}),
|
||||
}));
|
||||
|
||||
const updateMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
|
||||
request: {
|
||||
query: gql`
|
||||
query NetworkParams {
|
||||
networkParameters {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
networkParameters: [
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.updateNetParam.maxClose',
|
||||
value: '8760h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.updateNetParam.maxEnact',
|
||||
value: '8760h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.updateNetParam.minClose',
|
||||
value: '1h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.updateNetParam.minEnact',
|
||||
value: '2h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.updateNetParam.minProposerBalance',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'spam.protection.proposal.min.tokens',
|
||||
value: '1000000000000000000',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<Router>
|
||||
<MockedProvider mocks={[updateMarketNetworkParamsQueryMock]}>
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposeNetworkParameter />
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
</MockedProvider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
// Note: form submission is tested in propose-raw.spec.tsx. Reusable form
|
||||
// components are tested in their own directory.
|
||||
|
||||
describe('Propose Network Parameter', () => {
|
||||
it('should render successfully', async () => {
|
||||
const { baseElement } = renderComponent();
|
||||
await expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the correct title', async () => {
|
||||
renderComponent();
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Update network parameter proposal')).toBeTruthy()
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the form components', async () => {
|
||||
renderComponent();
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('network-parameter-proposal-form')).toBeTruthy()
|
||||
);
|
||||
expect(screen.getByTestId('min-proposal-requirements')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-docs-link')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-title')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-description')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-vote-deadline')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-enactment-deadline')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-submit')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-transaction-dialog')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the network param select element with no initial value', async () => {
|
||||
renderComponent();
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('proposal-parameter-select')).toHaveValue('')
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the current param value and a new value input when the network param select element is changed', async () => {
|
||||
renderComponent();
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('proposal-parameter-select')).toHaveValue('')
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByTestId('proposal-parameter-select'), {
|
||||
target: {
|
||||
value: 'spam_protection_proposal_min_tokens',
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('proposal-parameter-select')).toHaveValue(
|
||||
'spam_protection_proposal_min_tokens'
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('selected-proposal-param-current-value')
|
||||
).toHaveValue('1000000000000000000');
|
||||
|
||||
expect(
|
||||
screen.getByTestId('selected-proposal-param-new-value')
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
@ -0,0 +1,280 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
suitableForSyntaxHighlighter,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
useProposalSubmit,
|
||||
getClosingTimestamp,
|
||||
getEnactmentTimestamp,
|
||||
} from '@vegaprotocol/governance';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import {
|
||||
ProposalFormSubheader,
|
||||
ProposalFormMinRequirements,
|
||||
ProposalFormTitle,
|
||||
ProposalFormDescription,
|
||||
ProposalFormSubmit,
|
||||
ProposalFormTransactionDialog,
|
||||
ProposalFormVoteAndEnactmentDeadline,
|
||||
} from '../../components/propose';
|
||||
import {
|
||||
AsyncRenderer,
|
||||
FormGroup,
|
||||
Input,
|
||||
InputError,
|
||||
Link,
|
||||
Select,
|
||||
SyntaxHighlighter,
|
||||
TextArea,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Heading } from '../../../../components/heading';
|
||||
import { VegaWalletContainer } from '../../../../components/vega-wallet-container';
|
||||
|
||||
interface SelectedNetworkParamCurrentValueProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
const SelectedNetworkParamCurrentValue = ({
|
||||
value,
|
||||
}: SelectedNetworkParamCurrentValueProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-white">{t('CurrentValue')}</p>
|
||||
|
||||
{suitableForSyntaxHighlighter(value) ? (
|
||||
<SyntaxHighlighter data={JSON.parse(value)} />
|
||||
) : (
|
||||
<Input
|
||||
value={value}
|
||||
data-testid="selected-proposal-param-current-value"
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface NetworkParameterProposalFormFields {
|
||||
proposalVoteDeadline: string;
|
||||
proposalEnactmentDeadline: string;
|
||||
proposalTitle: string;
|
||||
proposalDescription: string;
|
||||
proposalNetworkParameterKey: string;
|
||||
proposalNetworkParameterValue: string;
|
||||
proposalReference: string;
|
||||
}
|
||||
|
||||
const docsLink = '/network-parameter-proposal';
|
||||
|
||||
export const ProposeNetworkParameter = () => {
|
||||
const [selectedNetworkParam, setSelectedNetworkParam] = useState<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
|
||||
const {
|
||||
params,
|
||||
loading: networkParamsLoading,
|
||||
error: networkParamsError,
|
||||
} = useNetworkParams();
|
||||
|
||||
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useForm<NetworkParameterProposalFormFields>();
|
||||
const { finalizedProposal, submit, Dialog } = useProposalSubmit();
|
||||
|
||||
const selectedParamEntry = params
|
||||
? Object.entries(params).find(([key]) => key === selectedNetworkParam)
|
||||
: null;
|
||||
|
||||
const onSubmit = async (fields: NetworkParameterProposalFormFields) => {
|
||||
const acutalNetworkParamKey = fields.proposalNetworkParameterKey
|
||||
.split('_')
|
||||
.join('.');
|
||||
await submit({
|
||||
rationale: {
|
||||
title: fields.proposalTitle,
|
||||
description: fields.proposalDescription,
|
||||
},
|
||||
terms: {
|
||||
updateNetworkParameter: {
|
||||
changes: {
|
||||
key: acutalNetworkParamKey,
|
||||
value: fields.proposalNetworkParameterValue,
|
||||
},
|
||||
},
|
||||
closingTimestamp: getClosingTimestamp(fields.proposalVoteDeadline),
|
||||
enactmentTimestamp: getEnactmentTimestamp(
|
||||
fields.proposalVoteDeadline,
|
||||
fields.proposalEnactmentDeadline
|
||||
),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
loading={networkParamsLoading}
|
||||
error={networkParamsError}
|
||||
data={params}
|
||||
>
|
||||
<Heading title={t('NetworkParameterProposal')} />
|
||||
<VegaWalletContainer>
|
||||
{() => (
|
||||
<>
|
||||
<ProposalFormMinRequirements
|
||||
minProposerBalance={
|
||||
params.governance_proposal_updateNetParam_minProposerBalance
|
||||
}
|
||||
spamProtectionMin={params.spam_protection_proposal_min_tokens}
|
||||
/>
|
||||
|
||||
{VEGA_DOCS_URL && (
|
||||
<p className="text-sm" data-testid="proposal-docs-link">
|
||||
<span className="mr-1">{t('ProposalTermsText')}</span>
|
||||
<Link
|
||||
href={`${VEGA_DOCS_URL}/tutorials/proposals${docsLink}`}
|
||||
target="_blank"
|
||||
>{`${VEGA_DOCS_URL}/tutorials/proposals${docsLink}`}</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{VEGA_EXPLORER_URL && (
|
||||
<p className="text-sm">
|
||||
{t('MoreNetParamsInfo')}{' '}
|
||||
<Link
|
||||
href={`${VEGA_EXPLORER_URL}/network-parameters`}
|
||||
target="_blank"
|
||||
>{`${VEGA_EXPLORER_URL}/network-parameters`}</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div data-testid="network-parameter-proposal-form">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<ProposalFormSubheader>
|
||||
{t('ProposalRationale')}
|
||||
</ProposalFormSubheader>
|
||||
|
||||
<ProposalFormTitle
|
||||
registerField={register('proposalTitle', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
errorMessage={errors?.proposalTitle?.message}
|
||||
/>
|
||||
|
||||
<ProposalFormDescription
|
||||
registerField={register('proposalDescription', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
errorMessage={errors?.proposalDescription?.message}
|
||||
/>
|
||||
|
||||
<ProposalFormSubheader>
|
||||
{t('SelectAParameterToChange')}
|
||||
</ProposalFormSubheader>
|
||||
|
||||
<FormGroup
|
||||
label={t('SelectAParameterToChange')}
|
||||
labelFor="proposal-parameter-key"
|
||||
hideLabel={true}
|
||||
>
|
||||
<Select
|
||||
data-testid="proposal-parameter-select"
|
||||
id="proposal-parameter-key"
|
||||
{...register('proposalNetworkParameterKey', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
onChange={(e) => setSelectedNetworkParam(e.target.value)}
|
||||
value={selectedNetworkParam}
|
||||
>
|
||||
<option value="">{t('SelectParameter')}</option>
|
||||
{Object.keys(params).map((key) => {
|
||||
const actualKey = key.split('_').join('.');
|
||||
return (
|
||||
<option key={key} value={key}>
|
||||
{actualKey}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
{errors?.proposalNetworkParameterKey?.message && (
|
||||
<InputError intent="danger">
|
||||
{errors?.proposalNetworkParameterKey?.message}
|
||||
</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
|
||||
{selectedNetworkParam && (
|
||||
<div className="mt-[-10px]">
|
||||
{selectedParamEntry && (
|
||||
<SelectedNetworkParamCurrentValue
|
||||
value={selectedParamEntry[1]}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormGroup
|
||||
label={t('NewProposedValue')}
|
||||
labelFor="proposal-parameter-new-value"
|
||||
>
|
||||
<TextArea
|
||||
data-testid="selected-proposal-param-new-value"
|
||||
id="proposal-parameter-new-value"
|
||||
{...register('proposalNetworkParameterValue', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
/>
|
||||
{errors?.proposalNetworkParameterValue?.message && (
|
||||
<InputError intent="danger">
|
||||
{errors?.proposalNetworkParameterValue?.message}
|
||||
</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ProposalFormVoteAndEnactmentDeadline
|
||||
voteRegister={register('proposalVoteDeadline', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
voteErrorMessage={errors?.proposalVoteDeadline?.message}
|
||||
voteMinClose={
|
||||
params.governance_proposal_updateNetParam_minClose
|
||||
}
|
||||
voteMaxClose={
|
||||
params.governance_proposal_updateNetParam_maxClose
|
||||
}
|
||||
enactmentRegister={register('proposalEnactmentDeadline', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
enactmentErrorMessage={
|
||||
errors?.proposalEnactmentDeadline?.message
|
||||
}
|
||||
enactmentMinClose={
|
||||
params.governance_proposal_updateNetParam_minEnact
|
||||
}
|
||||
enactmentMaxClose={
|
||||
params.governance_proposal_updateNetParam_maxEnact
|
||||
}
|
||||
/>
|
||||
|
||||
<ProposalFormSubmit isSubmitting={isSubmitting} />
|
||||
<ProposalFormTransactionDialog
|
||||
finalizedProposal={finalizedProposal}
|
||||
TransactionDialog={Dialog}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</VegaWalletContainer>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export {
|
||||
ProposeNewAsset,
|
||||
ProposeNewAsset as default,
|
||||
} from './propose-new-asset';
|
@ -0,0 +1,112 @@
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { MemoryRouter as Router } from 'react-router-dom';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import { mockWalletContext } from '../../test-helpers/mocks';
|
||||
import { ProposeNewAsset } from './propose-new-asset';
|
||||
import { gql } from '@apollo/client';
|
||||
import type { NetworkParamsQuery } from '@vegaprotocol/web3';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
useEnvironment: () => ({
|
||||
VEGA_DOCS_URL: 'https://docs.vega.xyz',
|
||||
}),
|
||||
}));
|
||||
|
||||
const updateMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
|
||||
request: {
|
||||
query: gql`
|
||||
query NetworkParams {
|
||||
networkParameters {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
networkParameters: [
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.asset.maxClose',
|
||||
value: '8760h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.asset.maxEnact',
|
||||
value: '8760h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.asset.minClose',
|
||||
value: '1h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.asset.minEnact',
|
||||
value: '2h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.asset.minProposerBalance',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'spam.protection.proposal.min.tokens',
|
||||
value: '1000000000000000000',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<Router>
|
||||
<MockedProvider mocks={[updateMarketNetworkParamsQueryMock]}>
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposeNewAsset />
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
</MockedProvider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
// Note: form submission is tested in propose-raw.spec.tsx. Reusable form
|
||||
// components are tested in their own directory.
|
||||
|
||||
describe('Propose New Asset', () => {
|
||||
it('should render successfully', async () => {
|
||||
const { baseElement } = renderComponent();
|
||||
await expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the title', async () => {
|
||||
renderComponent();
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('New asset proposal')).toBeTruthy()
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the form components', async () => {
|
||||
renderComponent();
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('new-asset-proposal-form')).toBeTruthy()
|
||||
);
|
||||
expect(screen.getByTestId('min-proposal-requirements')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-docs-link')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-title')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-description')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-terms')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-vote-deadline')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-validation-deadline')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-enactment-deadline')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-submit')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-transaction-dialog')).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,196 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
useProposalSubmit,
|
||||
getClosingTimestamp,
|
||||
getEnactmentTimestamp,
|
||||
getValidationTimestamp,
|
||||
} from '@vegaprotocol/governance';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import {
|
||||
ProposalFormMinRequirements,
|
||||
ProposalFormTitle,
|
||||
ProposalFormDescription,
|
||||
ProposalFormTerms,
|
||||
ProposalFormSubmit,
|
||||
ProposalFormTransactionDialog,
|
||||
ProposalFormSubheader,
|
||||
ProposalFormVoteAndEnactmentDeadline,
|
||||
} from '../../components/propose';
|
||||
import { AsyncRenderer, Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { Heading } from '../../../../components/heading';
|
||||
import { VegaWalletContainer } from '../../../../components/vega-wallet-container';
|
||||
import { NetworkParams, useNetworkParams } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export interface NewAssetProposalFormFields {
|
||||
proposalVoteDeadline: string;
|
||||
proposalEnactmentDeadline: string;
|
||||
proposalValidationDeadline: string;
|
||||
proposalTitle: string;
|
||||
proposalDescription: string;
|
||||
proposalTerms: string;
|
||||
proposalReference: string;
|
||||
}
|
||||
|
||||
const docsLink = '/new-asset-proposal';
|
||||
|
||||
export const ProposeNewAsset = () => {
|
||||
const {
|
||||
params,
|
||||
loading: networkParamsLoading,
|
||||
error: networkParamsError,
|
||||
} = useNetworkParams([
|
||||
NetworkParams.governance_proposal_asset_minClose,
|
||||
NetworkParams.governance_proposal_asset_maxClose,
|
||||
NetworkParams.governance_proposal_asset_minEnact,
|
||||
NetworkParams.governance_proposal_asset_maxEnact,
|
||||
NetworkParams.governance_proposal_asset_minProposerBalance,
|
||||
NetworkParams.spam_protection_proposal_min_tokens,
|
||||
]);
|
||||
|
||||
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useForm<NewAssetProposalFormFields>();
|
||||
const { finalizedProposal, submit, Dialog } = useProposalSubmit();
|
||||
|
||||
const onSubmit = async (fields: NewAssetProposalFormFields) => {
|
||||
await submit({
|
||||
rationale: {
|
||||
title: fields.proposalTitle,
|
||||
description: fields.proposalDescription,
|
||||
},
|
||||
terms: {
|
||||
newAsset: {
|
||||
...JSON.parse(fields.proposalTerms),
|
||||
},
|
||||
closingTimestamp: getClosingTimestamp(fields.proposalVoteDeadline),
|
||||
enactmentTimestamp: getEnactmentTimestamp(
|
||||
fields.proposalVoteDeadline,
|
||||
fields.proposalEnactmentDeadline
|
||||
),
|
||||
validationTimestamp: getValidationTimestamp(
|
||||
fields.proposalValidationDeadline
|
||||
),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
loading={networkParamsLoading}
|
||||
error={networkParamsError}
|
||||
data={params}
|
||||
>
|
||||
<Heading title={t('NewAssetProposal')} />
|
||||
<VegaWalletContainer>
|
||||
{() => (
|
||||
<>
|
||||
<ProposalFormMinRequirements
|
||||
minProposerBalance={
|
||||
params.governance_proposal_asset_minProposerBalance
|
||||
}
|
||||
spamProtectionMin={params.spam_protection_proposal_min_tokens}
|
||||
/>
|
||||
|
||||
{VEGA_DOCS_URL && (
|
||||
<p className="text-sm" data-testid="proposal-docs-link">
|
||||
<span className="mr-1">{t('ProposalTermsText')}</span>
|
||||
<Link
|
||||
href={`${VEGA_DOCS_URL}/tutorials/proposals${docsLink}`}
|
||||
target="_blank"
|
||||
>{`${VEGA_DOCS_URL}/tutorials/proposals${docsLink}`}</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{VEGA_EXPLORER_URL && (
|
||||
<p className="text-sm">
|
||||
{t('MoreAssetsInfo')}{' '}
|
||||
<Link
|
||||
href={`${VEGA_EXPLORER_URL}/assets`}
|
||||
target="_blank"
|
||||
>{`${VEGA_EXPLORER_URL}/assets`}</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div data-testid="new-asset-proposal-form">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<ProposalFormSubheader>
|
||||
{t('ProposalRationale')}
|
||||
</ProposalFormSubheader>
|
||||
|
||||
<ProposalFormTitle
|
||||
registerField={register('proposalTitle', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
errorMessage={errors?.proposalTitle?.message}
|
||||
/>
|
||||
|
||||
<ProposalFormDescription
|
||||
registerField={register('proposalDescription', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
errorMessage={errors?.proposalDescription?.message}
|
||||
/>
|
||||
|
||||
<ProposalFormSubheader>{t('NewAsset')}</ProposalFormSubheader>
|
||||
|
||||
<ProposalFormTerms
|
||||
registerField={register('proposalTerms', {
|
||||
required: t('Required'),
|
||||
validate: {
|
||||
validateJson: (value) => {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return t('Must be valid JSON');
|
||||
}
|
||||
},
|
||||
},
|
||||
})}
|
||||
labelOverride={'Terms.newAsset (JSON format)'}
|
||||
errorMessage={errors?.proposalTerms?.message}
|
||||
customDocLink={docsLink}
|
||||
/>
|
||||
|
||||
<ProposalFormVoteAndEnactmentDeadline
|
||||
voteRegister={register('proposalVoteDeadline', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
voteErrorMessage={errors?.proposalVoteDeadline?.message}
|
||||
voteMinClose={params.governance_proposal_asset_minClose}
|
||||
voteMaxClose={params.governance_proposal_asset_maxClose}
|
||||
enactmentRegister={register('proposalEnactmentDeadline', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
enactmentErrorMessage={
|
||||
errors?.proposalEnactmentDeadline?.message
|
||||
}
|
||||
enactmentMinClose={params.governance_proposal_asset_minEnact}
|
||||
enactmentMaxClose={params.governance_proposal_asset_maxEnact}
|
||||
validationRequired={true}
|
||||
validationRegister={register('proposalValidationDeadline', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
validationErrorMessage={
|
||||
errors?.proposalValidationDeadline?.message
|
||||
}
|
||||
/>
|
||||
|
||||
<ProposalFormSubmit isSubmitting={isSubmitting} />
|
||||
<ProposalFormTransactionDialog
|
||||
finalizedProposal={finalizedProposal}
|
||||
TransactionDialog={Dialog}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</VegaWalletContainer>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export {
|
||||
ProposeNewMarket,
|
||||
ProposeNewMarket as default,
|
||||
} from './propose-new-market';
|
@ -0,0 +1,104 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { ProposeNewMarket } from './propose-new-market';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { mockWalletContext } from '../../test-helpers/mocks';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import type { NetworkParamsQuery } from '@vegaprotocol/web3';
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
useEnvironment: () => ({
|
||||
VEGA_DOCS_URL: 'https://docs.vega.xyz',
|
||||
}),
|
||||
}));
|
||||
|
||||
const newMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
|
||||
request: {
|
||||
query: gql`
|
||||
query NetworkParams {
|
||||
networkParameters {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
networkParameters: [
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.market.maxClose',
|
||||
value: '8760h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.market.maxEnact',
|
||||
value: '8760h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.market.minClose',
|
||||
value: '1h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.market.minEnact',
|
||||
value: '2h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.market.minProposerBalance',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'spam.protection.proposal.min.tokens',
|
||||
value: '1000000000000000000',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<Router>
|
||||
<MockedProvider mocks={[newMarketNetworkParamsQueryMock]}>
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposeNewMarket />
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
</MockedProvider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
// Note: form submission is tested in propose-raw.spec.tsx. Reusable form
|
||||
// components are tested in their own directory.
|
||||
|
||||
describe('Propose New Market', () => {
|
||||
it('should render successfully', async () => {
|
||||
const { baseElement } = renderComponent();
|
||||
await expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the form components', async () => {
|
||||
renderComponent();
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('new-market-proposal-form')).toBeTruthy()
|
||||
);
|
||||
expect(screen.getByTestId('min-proposal-requirements')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-docs-link')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-title')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-description')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-terms')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-vote-deadline')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-enactment-deadline')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-submit')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-transaction-dialog')).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,184 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
useProposalSubmit,
|
||||
getClosingTimestamp,
|
||||
getEnactmentTimestamp,
|
||||
} from '@vegaprotocol/governance';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import {
|
||||
ProposalFormMinRequirements,
|
||||
ProposalFormTitle,
|
||||
ProposalFormDescription,
|
||||
ProposalFormTerms,
|
||||
ProposalFormSubmit,
|
||||
ProposalFormTransactionDialog,
|
||||
ProposalFormSubheader,
|
||||
ProposalFormVoteAndEnactmentDeadline,
|
||||
} from '../../components/propose';
|
||||
import { AsyncRenderer, Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { Heading } from '../../../../components/heading';
|
||||
import { VegaWalletContainer } from '../../../../components/vega-wallet-container';
|
||||
import { NetworkParams, useNetworkParams } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export interface NewMarketProposalFormFields {
|
||||
proposalVoteDeadline: string;
|
||||
proposalEnactmentDeadline: string;
|
||||
proposalTitle: string;
|
||||
proposalDescription: string;
|
||||
proposalTerms: string;
|
||||
proposalReference: string;
|
||||
}
|
||||
|
||||
const docsLink = '/new-market-proposal';
|
||||
|
||||
export const ProposeNewMarket = () => {
|
||||
const {
|
||||
params,
|
||||
loading: networkParamsLoading,
|
||||
error: networkParamsError,
|
||||
} = useNetworkParams([
|
||||
NetworkParams.governance_proposal_market_maxClose,
|
||||
NetworkParams.governance_proposal_market_minClose,
|
||||
NetworkParams.governance_proposal_market_maxEnact,
|
||||
NetworkParams.governance_proposal_market_minEnact,
|
||||
NetworkParams.governance_proposal_market_minProposerBalance,
|
||||
NetworkParams.spam_protection_proposal_min_tokens,
|
||||
]);
|
||||
|
||||
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useForm<NewMarketProposalFormFields>();
|
||||
const { finalizedProposal, submit, Dialog } = useProposalSubmit();
|
||||
|
||||
const onSubmit = async (fields: NewMarketProposalFormFields) => {
|
||||
await submit({
|
||||
rationale: {
|
||||
title: fields.proposalTitle,
|
||||
description: fields.proposalDescription,
|
||||
},
|
||||
terms: {
|
||||
newMarket: {
|
||||
...JSON.parse(fields.proposalTerms),
|
||||
},
|
||||
closingTimestamp: getClosingTimestamp(fields.proposalVoteDeadline),
|
||||
enactmentTimestamp: getEnactmentTimestamp(
|
||||
fields.proposalVoteDeadline,
|
||||
fields.proposalEnactmentDeadline
|
||||
),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
loading={networkParamsLoading}
|
||||
error={networkParamsError}
|
||||
data={params}
|
||||
>
|
||||
<Heading title={t('NewMarketProposal')} />
|
||||
<VegaWalletContainer>
|
||||
{() => (
|
||||
<>
|
||||
<ProposalFormMinRequirements
|
||||
minProposerBalance={
|
||||
params.governance_proposal_market_minProposerBalance
|
||||
}
|
||||
spamProtectionMin={params.spam_protection_proposal_min_tokens}
|
||||
/>
|
||||
|
||||
{VEGA_DOCS_URL && (
|
||||
<p className="text-sm" data-testid="proposal-docs-link">
|
||||
<span className="mr-1">{t('ProposalTermsText')}</span>
|
||||
<Link
|
||||
href={`${VEGA_DOCS_URL}/tutorials/proposals/${docsLink}`}
|
||||
target="_blank"
|
||||
>{`${VEGA_DOCS_URL}/tutorials/proposals/${docsLink}`}</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{VEGA_EXPLORER_URL && (
|
||||
<p className="text-sm">
|
||||
{t('MoreMarketsInfo')}{' '}
|
||||
<Link
|
||||
href={`${VEGA_EXPLORER_URL}/markets`}
|
||||
target="_blank"
|
||||
>{`${VEGA_EXPLORER_URL}/markets`}</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div data-testid="new-market-proposal-form">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<ProposalFormSubheader>
|
||||
{t('ProposalRationale')}
|
||||
</ProposalFormSubheader>
|
||||
|
||||
<ProposalFormTitle
|
||||
registerField={register('proposalTitle', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
errorMessage={errors?.proposalTitle?.message}
|
||||
/>
|
||||
|
||||
<ProposalFormDescription
|
||||
registerField={register('proposalDescription', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
errorMessage={errors?.proposalDescription?.message}
|
||||
/>
|
||||
|
||||
<ProposalFormSubheader>{t('NewMarket')}</ProposalFormSubheader>
|
||||
|
||||
<ProposalFormTerms
|
||||
registerField={register('proposalTerms', {
|
||||
required: t('Required'),
|
||||
validate: {
|
||||
validateJson: (value) => {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return t('Must be valid JSON');
|
||||
}
|
||||
},
|
||||
},
|
||||
})}
|
||||
labelOverride={'Terms.newMarket (JSON format)'}
|
||||
errorMessage={errors?.proposalTerms?.message}
|
||||
customDocLink={docsLink}
|
||||
/>
|
||||
|
||||
<ProposalFormVoteAndEnactmentDeadline
|
||||
voteRegister={register('proposalVoteDeadline', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
voteErrorMessage={errors?.proposalVoteDeadline?.message}
|
||||
voteMinClose={params.governance_proposal_market_minClose}
|
||||
voteMaxClose={params.governance_proposal_market_maxClose}
|
||||
enactmentRegister={register('proposalEnactmentDeadline', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
enactmentErrorMessage={
|
||||
errors?.proposalEnactmentDeadline?.message
|
||||
}
|
||||
enactmentMinClose={params.governance_proposal_market_minEnact}
|
||||
enactmentMaxClose={params.governance_proposal_market_maxEnact}
|
||||
/>
|
||||
|
||||
<ProposalFormSubmit isSubmitting={isSubmitting} />
|
||||
<ProposalFormTransactionDialog
|
||||
finalizedProposal={finalizedProposal}
|
||||
TransactionDialog={Dialog}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</VegaWalletContainer>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
30
apps/token/src/routes/governance/propose/propose.spec.tsx
Normal file
30
apps/token/src/routes/governance/propose/propose.spec.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { Propose } from './propose';
|
||||
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<Router>
|
||||
<Propose />
|
||||
</Router>
|
||||
);
|
||||
|
||||
describe('Propose', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = renderComponent();
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the heading, proposal type question and options', () => {
|
||||
renderComponent();
|
||||
expect(
|
||||
screen.getByText('What type of proposal would you like to make?')
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('Network parameter')).toBeTruthy();
|
||||
expect(screen.getByText('New market')).toBeTruthy();
|
||||
expect(screen.getByText('Update market')).toBeTruthy();
|
||||
expect(screen.getByText('New asset')).toBeTruthy();
|
||||
expect(screen.getByText('Freeform')).toBeTruthy();
|
||||
expect(screen.getByText('Let me choose (raw proposal)')).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,21 +1,86 @@
|
||||
import Routes from '../../routes';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ProposalForm } from '@vegaprotocol/governance';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { Heading } from '../../../components/heading';
|
||||
import { VegaWalletContainer } from '../../../components/vega-wallet-container';
|
||||
|
||||
export const Propose = () => {
|
||||
const { VEGA_DOCS_URL, VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading title={t('NewProposal')} />
|
||||
<VegaWalletContainer>
|
||||
{() => (
|
||||
<>
|
||||
<p>{t('MinProposalRequirements')}</p>
|
||||
<ProposalForm />
|
||||
</>
|
||||
)}
|
||||
</VegaWalletContainer>
|
||||
<section className="pb-6">
|
||||
<Heading title={t('NewProposal')} />
|
||||
<div className="text-sm">
|
||||
{VEGA_DOCS_URL && (
|
||||
<p>
|
||||
<span className="mr-1">{t('ProposalTermsText')}</span>
|
||||
<Link
|
||||
href={`${VEGA_DOCS_URL}/tutorials/proposals`}
|
||||
target="_blank"
|
||||
>{`${VEGA_DOCS_URL}/tutorials/proposals`}</Link>
|
||||
</p>
|
||||
)}
|
||||
{VEGA_EXPLORER_URL && (
|
||||
<p>
|
||||
{t('MoreProposalsInfo')}{' '}
|
||||
<Link
|
||||
href={`${VEGA_EXPLORER_URL}/governance`}
|
||||
target="_blank"
|
||||
>{`${VEGA_EXPLORER_URL}/governance`}</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-h5">{t('ProposalTypeQuestion')}</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
<Link href={`${Routes.GOVERNANCE}/propose/network-parameter`}>
|
||||
{t('NetworkParameter')}
|
||||
</Link>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<Link href={`${Routes.GOVERNANCE}/propose/new-market`}>
|
||||
{t('NewMarket')}
|
||||
</Link>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<Link href={`${Routes.GOVERNANCE}/propose/update-market`}>
|
||||
{t('UpdateMarket')}
|
||||
</Link>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<Link href={`${Routes.GOVERNANCE}/propose/new-asset`}>
|
||||
{t('NewAsset')}
|
||||
</Link>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<Link href={`${Routes.GOVERNANCE}/propose/freeform`}>
|
||||
{t('Freeform')}
|
||||
</Link>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<Link href={`${Routes.GOVERNANCE}/propose/raw`}>
|
||||
{t('RawProposal')}
|
||||
</Link>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
1
apps/token/src/routes/governance/propose/raw/index.tsx
Normal file
1
apps/token/src/routes/governance/propose/raw/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { ProposeRaw, ProposeRaw as default } from './propose-raw';
|
@ -1,5 +1,7 @@
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { addHours, getTime } from 'date-fns';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
@ -8,11 +10,11 @@ import {
|
||||
ProposalRejectionReason,
|
||||
ProposalState,
|
||||
} from '@vegaprotocol/types';
|
||||
import { ProposalForm } from './proposal-form';
|
||||
import { PROPOSAL_EVENT_SUB } from './proposals-hooks';
|
||||
import type { ProposalEvent } from './proposals-hooks/__generated__/ProposalEvent';
|
||||
import { ProposeRaw } from './propose-raw';
|
||||
import { PROPOSAL_EVENT_SUB } from '@vegaprotocol/governance';
|
||||
import type { ProposalEvent } from '@vegaprotocol/governance';
|
||||
|
||||
describe('ProposalForm', () => {
|
||||
describe('Raw proposal form', () => {
|
||||
const pubkey = '0x123';
|
||||
const mockProposalEvent: MockedResponse<ProposalEvent> = {
|
||||
request: {
|
||||
@ -44,18 +46,20 @@ describe('ProposalForm', () => {
|
||||
};
|
||||
const setup = (mockSendTx = jest.fn()) => {
|
||||
return render(
|
||||
<MockedProvider mocks={[mockProposalEvent]}>
|
||||
<VegaWalletContext.Provider
|
||||
value={
|
||||
{
|
||||
keypair: { pub: pubkey },
|
||||
sendTx: mockSendTx,
|
||||
} as unknown as VegaWalletContextShape
|
||||
}
|
||||
>
|
||||
<ProposalForm />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
<AppStateProvider>
|
||||
<MockedProvider mocks={[mockProposalEvent]}>
|
||||
<VegaWalletContext.Provider
|
||||
value={
|
||||
{
|
||||
keypair: { pub: pubkey },
|
||||
sendTx: mockSendTx,
|
||||
} as unknown as VegaWalletContextShape
|
||||
}
|
||||
>
|
||||
<ProposeRaw />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</AppStateProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -125,8 +129,8 @@ describe('ProposalForm', () => {
|
||||
value: '300',
|
||||
},
|
||||
},
|
||||
closingTimestamp: 1657721401,
|
||||
enactmentTimestamp: 1657807801,
|
||||
closingTimestamp: Math.floor(getTime(addHours(new Date(), 2)) / 1000),
|
||||
enactmentTimestamp: Math.floor(getTime(addHours(new Date(), 3)) / 1000),
|
||||
},
|
||||
});
|
||||
fireEvent.change(screen.getByTestId('proposal-data'), {
|
107
apps/token/src/routes/governance/propose/raw/propose-raw.tsx
Normal file
107
apps/token/src/routes/governance/propose/raw/propose-raw.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { Heading } from '../../../../components/heading';
|
||||
import { VegaWalletContainer } from '../../../../components/vega-wallet-container';
|
||||
import {
|
||||
FormGroup,
|
||||
InputError,
|
||||
Link,
|
||||
TextArea,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useProposalSubmit } from '@vegaprotocol/governance';
|
||||
import {
|
||||
ProposalFormSubmit,
|
||||
ProposalFormTransactionDialog,
|
||||
} from '../../components/propose';
|
||||
|
||||
export interface RawProposalFormFields {
|
||||
rawProposalData: string;
|
||||
}
|
||||
|
||||
export const ProposeRaw = () => {
|
||||
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useForm<RawProposalFormFields>();
|
||||
const { finalizedProposal, submit, Dialog } = useProposalSubmit();
|
||||
|
||||
const hasError = Boolean(errors.rawProposalData?.message);
|
||||
|
||||
const onSubmit = async (fields: RawProposalFormFields) => {
|
||||
await submit(JSON.parse(fields.rawProposalData));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading title={t('NewRawProposal')} />
|
||||
<VegaWalletContainer>
|
||||
{() => (
|
||||
<>
|
||||
{VEGA_DOCS_URL && (
|
||||
<p className="text-sm" data-testid="proposal-docs-link">
|
||||
<span className="mr-1">{t('ProposalTermsText')}</span>
|
||||
<Link
|
||||
href={`${VEGA_DOCS_URL}/tutorials/proposals`}
|
||||
target="_blank"
|
||||
>{`${VEGA_DOCS_URL}/tutorials/proposals`}</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{VEGA_EXPLORER_URL && (
|
||||
<p className="text-sm">
|
||||
{t('MoreProposalsInfo')}{' '}
|
||||
<Link
|
||||
href={`${VEGA_EXPLORER_URL}/governance`}
|
||||
target="_blank"
|
||||
>{`${VEGA_EXPLORER_URL}/governance`}</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div data-testid="raw-proposal-form">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormGroup
|
||||
label="Make a proposal by submitting JSON"
|
||||
labelFor="proposal-data"
|
||||
>
|
||||
<TextArea
|
||||
id="proposal-data"
|
||||
className="min-h-[200px]"
|
||||
hasError={hasError}
|
||||
data-testid="proposal-data"
|
||||
{...register('rawProposalData', {
|
||||
required: t('Required'),
|
||||
validate: {
|
||||
validateJson: (value) => {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return t('Must be valid JSON');
|
||||
}
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.rawProposalData?.message && (
|
||||
<InputError intent="danger">
|
||||
{errors.rawProposalData?.message}
|
||||
</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
<ProposalFormSubmit isSubmitting={isSubmitting} />
|
||||
<ProposalFormTransactionDialog
|
||||
finalizedProposal={finalizedProposal}
|
||||
TransactionDialog={Dialog}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</VegaWalletContainer>
|
||||
</>
|
||||
);
|
||||
};
|
57
apps/token/src/routes/governance/propose/update-market/__generated__/ProposalMarketsQuery.ts
generated
Normal file
57
apps/token/src/routes/governance/propose/update-market/__generated__/ProposalMarketsQuery.ts
generated
Normal file
@ -0,0 +1,57 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: ProposalMarketsQuery
|
||||
// ====================================================
|
||||
|
||||
export interface ProposalMarketsQuery_marketsConnection_edges_node_tradableInstrument_instrument {
|
||||
__typename: "Instrument";
|
||||
/**
|
||||
* 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) (string)
|
||||
*/
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface ProposalMarketsQuery_marketsConnection_edges_node_tradableInstrument {
|
||||
__typename: "TradableInstrument";
|
||||
/**
|
||||
* An instance of, or reference to, a fully specified instrument.
|
||||
*/
|
||||
instrument: ProposalMarketsQuery_marketsConnection_edges_node_tradableInstrument_instrument;
|
||||
}
|
||||
|
||||
export interface ProposalMarketsQuery_marketsConnection_edges_node {
|
||||
__typename: "Market";
|
||||
/**
|
||||
* Market ID
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* An instance of, or reference to, a tradable instrument.
|
||||
*/
|
||||
tradableInstrument: ProposalMarketsQuery_marketsConnection_edges_node_tradableInstrument;
|
||||
}
|
||||
|
||||
export interface ProposalMarketsQuery_marketsConnection_edges {
|
||||
__typename: "MarketEdge";
|
||||
node: ProposalMarketsQuery_marketsConnection_edges_node;
|
||||
}
|
||||
|
||||
export interface ProposalMarketsQuery_marketsConnection {
|
||||
__typename: "MarketConnection";
|
||||
/**
|
||||
* The markets in this connection
|
||||
*/
|
||||
edges: ProposalMarketsQuery_marketsConnection_edges[];
|
||||
}
|
||||
|
||||
export interface ProposalMarketsQuery {
|
||||
marketsConnection: ProposalMarketsQuery_marketsConnection;
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
export {
|
||||
ProposeUpdateMarket,
|
||||
ProposeUpdateMarket as default,
|
||||
} from './propose-update-market';
|
@ -0,0 +1,220 @@
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { MemoryRouter as Router } from 'react-router-dom';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import { mockWalletContext } from '../../test-helpers/mocks';
|
||||
import { ProposeUpdateMarket } from './propose-update-market';
|
||||
import type { NetworkParamsQuery } from '@vegaprotocol/web3';
|
||||
import { MARKETS_QUERY } from './propose-update-market';
|
||||
import type { ProposalMarketsQuery } from './__generated__/ProposalMarketsQuery';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
const updateMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
|
||||
request: {
|
||||
query: gql`
|
||||
query NetworkParams {
|
||||
networkParameters {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
networkParameters: [
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.updateMarket.maxClose',
|
||||
value: '8760h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.updateMarket.maxEnact',
|
||||
value: '8760h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.updateMarket.minClose',
|
||||
value: '1h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.updateMarket.minEnact',
|
||||
value: '2h0m0s',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.updateMarket.minProposerBalance',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'spam.protection.proposal.min.tokens',
|
||||
value: '1000000000000000000',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const marketQueryMock: MockedResponse<ProposalMarketsQuery> = {
|
||||
request: {
|
||||
query: MARKETS_QUERY,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsConnection: {
|
||||
__typename: 'MarketConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'MarketEdge',
|
||||
node: {
|
||||
__typename: 'Market',
|
||||
id: 'd2319c91104a523032cf04ac4e20962b87ee1f187d1e411a2cac12554dd38f29',
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'ETHUSD (September Market)',
|
||||
code: 'ETHiUSDT',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'MarketEdge',
|
||||
node: {
|
||||
__typename: 'Market',
|
||||
id: 'bb941f84e25b3a06068e33917d01215d56a51e9abff6ff9b9a3f2cf49b495e37',
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'Las Vegas nuggets',
|
||||
code: 'Nuggets2',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'MarketEdge',
|
||||
node: {
|
||||
__typename: 'Market',
|
||||
id: '976f71b8d6f99f3685297156a11245f21e49c99d3be17f2577a4bf693b5f37d1',
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'Nuggets',
|
||||
code: 'Nuggets',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'MarketEdge',
|
||||
node: {
|
||||
__typename: 'Market',
|
||||
id: '1cb2e1755208914b6f258a28babd19ae8dfbaf4084d8867d8a120c50dca1e17f',
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'CELUSD (June 2022)',
|
||||
code: 'CELUSD',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<MockedProvider
|
||||
mocks={[updateMarketNetworkParamsQueryMock, marketQueryMock]}
|
||||
addTypename={false}
|
||||
>
|
||||
<Router>
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposeUpdateMarket />
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
</Router>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
// Note: form submission is tested in propose-raw.spec.tsx. Reusable form
|
||||
// components are tested in their own directory.
|
||||
|
||||
describe('Propose Update Market', () => {
|
||||
it('should render successfully', async () => {
|
||||
const { baseElement } = renderComponent();
|
||||
await expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the title', async () => {
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Update market proposal')).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the form components', async () => {
|
||||
renderComponent();
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('update-market-proposal-form')).toBeTruthy()
|
||||
);
|
||||
expect(screen.getByTestId('min-proposal-requirements')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-title')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-description')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-terms')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-vote-deadline')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-enactment-deadline')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-submit')).toBeTruthy();
|
||||
expect(screen.getByTestId('proposal-transaction-dialog')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the select element with no initial value', async () => {
|
||||
renderComponent();
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Update market proposal')).toBeInTheDocument()
|
||||
);
|
||||
expect(screen.getByTestId('proposal-market-select')).toHaveValue('');
|
||||
});
|
||||
|
||||
it('should render the correct market details when the market select is used', async () => {
|
||||
renderComponent();
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Update market proposal')).toBeInTheDocument()
|
||||
);
|
||||
fireEvent.change(screen.getByTestId('proposal-market-select'), {
|
||||
target: {
|
||||
value:
|
||||
'd2319c91104a523032cf04ac4e20962b87ee1f187d1e411a2cac12554dd38f29',
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('proposal-market-select')).toHaveValue(
|
||||
'd2319c91104a523032cf04ac4e20962b87ee1f187d1e411a2cac12554dd38f29'
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('update-market-details')).toHaveTextContent(
|
||||
'ETHUSD (September Market)'
|
||||
);
|
||||
expect(screen.getByTestId('update-market-details')).toHaveTextContent(
|
||||
'ETHiUSDT'
|
||||
);
|
||||
expect(screen.getByTestId('update-market-details')).toHaveTextContent(
|
||||
'd2319c91104a523032cf04ac4e20962b87ee1f187d1e411a2cac12554dd38f29'
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,311 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
useProposalSubmit,
|
||||
getClosingTimestamp,
|
||||
getEnactmentTimestamp,
|
||||
} from '@vegaprotocol/governance';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import {
|
||||
ProposalFormSubheader,
|
||||
ProposalFormMinRequirements,
|
||||
ProposalFormTitle,
|
||||
ProposalFormDescription,
|
||||
ProposalFormTerms,
|
||||
ProposalFormSubmit,
|
||||
ProposalFormTransactionDialog,
|
||||
ProposalFormVoteAndEnactmentDeadline,
|
||||
} from '../../components/propose';
|
||||
import {
|
||||
AsyncRenderer,
|
||||
FormGroup,
|
||||
InputError,
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
Link,
|
||||
Select,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Heading } from '../../../../components/heading';
|
||||
import { VegaWalletContainer } from '../../../../components/vega-wallet-container';
|
||||
import type { ProposalMarketsQuery } from './__generated__/ProposalMarketsQuery';
|
||||
import { NetworkParams, useNetworkParams } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export const MARKETS_QUERY = gql`
|
||||
query ProposalMarketsQuery {
|
||||
marketsConnection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export interface UpdateMarketProposalFormFields {
|
||||
proposalVoteDeadline: string;
|
||||
proposalEnactmentDeadline: string;
|
||||
proposalTitle: string;
|
||||
proposalDescription: string;
|
||||
proposalMarketId: string;
|
||||
proposalTerms: string;
|
||||
proposalReference: string;
|
||||
}
|
||||
|
||||
const docsLink = '/update-market-proposal';
|
||||
|
||||
export const ProposeUpdateMarket = () => {
|
||||
const {
|
||||
params,
|
||||
loading: networkParamsLoading,
|
||||
error: networkParamsError,
|
||||
} = useNetworkParams([
|
||||
NetworkParams.governance_proposal_updateMarket_maxClose,
|
||||
NetworkParams.governance_proposal_updateMarket_minClose,
|
||||
NetworkParams.governance_proposal_updateMarket_maxEnact,
|
||||
NetworkParams.governance_proposal_updateMarket_minEnact,
|
||||
NetworkParams.governance_proposal_updateMarket_minProposerBalance,
|
||||
NetworkParams.spam_protection_proposal_min_tokens,
|
||||
]);
|
||||
|
||||
const {
|
||||
data: marketsData,
|
||||
loading: marketsLoading,
|
||||
error: marketsError,
|
||||
} = useQuery<ProposalMarketsQuery>(MARKETS_QUERY);
|
||||
const sortedMarkets = useMemo(() => {
|
||||
if (!marketsData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return marketsData.marketsConnection.edges
|
||||
.map((edge) => edge.node)
|
||||
.sort((a, b) => {
|
||||
const aName = a.tradableInstrument.instrument.name;
|
||||
const bName = b.tradableInstrument.instrument.name;
|
||||
|
||||
if (aName < bName) {
|
||||
return -1;
|
||||
}
|
||||
if (aName > bName) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}, [marketsData]);
|
||||
|
||||
const [selectedMarket, setSelectedMarket] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useForm<UpdateMarketProposalFormFields>();
|
||||
const { finalizedProposal, submit, Dialog } = useProposalSubmit();
|
||||
|
||||
const onSubmit = async (fields: UpdateMarketProposalFormFields) => {
|
||||
await submit({
|
||||
rationale: {
|
||||
title: fields.proposalTitle,
|
||||
description: fields.proposalDescription,
|
||||
},
|
||||
terms: {
|
||||
updateMarket: {
|
||||
marketId: fields.proposalMarketId,
|
||||
changes: {
|
||||
...JSON.parse(fields.proposalTerms),
|
||||
},
|
||||
},
|
||||
closingTimestamp: getClosingTimestamp(fields.proposalVoteDeadline),
|
||||
enactmentTimestamp: getEnactmentTimestamp(
|
||||
fields.proposalVoteDeadline,
|
||||
fields.proposalEnactmentDeadline
|
||||
),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
loading={networkParamsLoading && marketsLoading}
|
||||
error={networkParamsError && marketsError}
|
||||
data={params && marketsData}
|
||||
>
|
||||
<Heading title={t('UpdateMarketProposal')} />
|
||||
<VegaWalletContainer>
|
||||
{() => (
|
||||
<>
|
||||
<ProposalFormMinRequirements
|
||||
minProposerBalance={
|
||||
params.governance_proposal_updateMarket_minProposerBalance
|
||||
}
|
||||
spamProtectionMin={params.spam_protection_proposal_min_tokens}
|
||||
/>
|
||||
|
||||
{VEGA_DOCS_URL && (
|
||||
<p className="text-sm" data-testid="proposal-docs-link">
|
||||
<span className="mr-1">{t('ProposalTermsText')}</span>
|
||||
<Link
|
||||
href={`${VEGA_DOCS_URL}/tutorials/proposals${docsLink}`}
|
||||
target="_blank"
|
||||
>{`${VEGA_DOCS_URL}/tutorials/proposals${docsLink}`}</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{VEGA_EXPLORER_URL && (
|
||||
<p className="text-sm">
|
||||
{t('MoreMarketsInfo')}{' '}
|
||||
<Link
|
||||
href={`${VEGA_EXPLORER_URL}/markets`}
|
||||
target="_blank"
|
||||
>{`${VEGA_EXPLORER_URL}/markets`}</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div data-testid="update-market-proposal-form">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<ProposalFormSubheader>
|
||||
{t('ProposalRationale')}
|
||||
</ProposalFormSubheader>
|
||||
|
||||
<ProposalFormTitle
|
||||
registerField={register('proposalTitle', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
errorMessage={errors?.proposalTitle?.message}
|
||||
/>
|
||||
|
||||
<ProposalFormDescription
|
||||
registerField={register('proposalDescription', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
errorMessage={errors?.proposalDescription?.message}
|
||||
/>
|
||||
|
||||
<ProposalFormSubheader>
|
||||
{t('SelectAMarketToChange')}
|
||||
</ProposalFormSubheader>
|
||||
|
||||
<FormGroup
|
||||
label={t('SelectAMarketToChange')}
|
||||
labelFor="proposal-market"
|
||||
hideLabel={true}
|
||||
>
|
||||
<Select
|
||||
data-testid="proposal-market-select"
|
||||
id="proposal-market"
|
||||
{...register('proposalMarketId', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
onChange={(e) => setSelectedMarket(e.target.value)}
|
||||
>
|
||||
<option value="">{t('SelectMarket')}</option>
|
||||
{sortedMarkets.map((market) => (
|
||||
<option value={market.id} key={market.id}>
|
||||
{market.tradableInstrument.instrument.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
{errors?.proposalMarketId?.message && (
|
||||
<InputError intent="danger">
|
||||
{errors?.proposalMarketId?.message}
|
||||
</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
|
||||
{selectedMarket && (
|
||||
<div className="mt-[-20px] mb-6">
|
||||
<KeyValueTable data-testid="update-market-details">
|
||||
<KeyValueTableRow>
|
||||
{t('MarketName')}
|
||||
{
|
||||
marketsData?.marketsConnection?.edges?.find(
|
||||
({ node: market }) => market.id === selectedMarket
|
||||
)?.node.tradableInstrument.instrument.name
|
||||
}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('MarketCode')}
|
||||
{
|
||||
marketsData?.marketsConnection?.edges?.find(
|
||||
({ node: market }) => market.id === selectedMarket
|
||||
)?.node.tradableInstrument.instrument.code
|
||||
}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('MarketId')}
|
||||
{selectedMarket}
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ProposalFormTerms
|
||||
registerField={register('proposalTerms', {
|
||||
required: t('Required'),
|
||||
validate: {
|
||||
validateJson: (value) => {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return t('Must be valid JSON');
|
||||
}
|
||||
},
|
||||
},
|
||||
})}
|
||||
labelOverride={t('ProposeUpdateMarketTerms')}
|
||||
errorMessage={errors?.proposalTerms?.message}
|
||||
customDocLink={docsLink}
|
||||
/>
|
||||
|
||||
<ProposalFormVoteAndEnactmentDeadline
|
||||
voteRegister={register('proposalVoteDeadline', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
voteErrorMessage={errors?.proposalVoteDeadline?.message}
|
||||
voteMinClose={
|
||||
params.governance_proposal_updateMarket_minClose
|
||||
}
|
||||
voteMaxClose={
|
||||
params.governance_proposal_updateMarket_maxClose
|
||||
}
|
||||
enactmentRegister={register('proposalEnactmentDeadline', {
|
||||
required: t('Required'),
|
||||
})}
|
||||
enactmentErrorMessage={
|
||||
errors?.proposalEnactmentDeadline?.message
|
||||
}
|
||||
enactmentMinClose={
|
||||
params.governance_proposal_updateMarket_minEnact
|
||||
}
|
||||
enactmentMaxClose={
|
||||
params.governance_proposal_updateMarket_maxEnact
|
||||
}
|
||||
/>
|
||||
|
||||
<ProposalFormSubmit isSubmitting={isSubmitting} />
|
||||
<ProposalFormTransactionDialog
|
||||
finalizedProposal={finalizedProposal}
|
||||
TransactionDialog={Dialog}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</VegaWalletContainer>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
@ -3,7 +3,7 @@ import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import React from 'react';
|
||||
import type { WithTranslation } from 'react-i18next';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { useRoutes } from 'react-router-dom';
|
||||
|
||||
import { SplashLoader } from '../components/splash-loader';
|
||||
import routerConfig from './router-config';
|
||||
@ -45,6 +45,8 @@ class RouteErrorBoundary extends React.Component<
|
||||
const BoundaryWithTranslation = withTranslation()(RouteErrorBoundary);
|
||||
|
||||
export const AppRouter = () => {
|
||||
const routes = useRoutes(routerConfig);
|
||||
|
||||
const splashLoading = (
|
||||
<Splash>
|
||||
<SplashLoader />
|
||||
@ -52,26 +54,8 @@ export const AppRouter = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
// @ts-ignore withTranslation HOC types not working
|
||||
<BoundaryWithTranslation>
|
||||
<React.Suspense fallback={splashLoading}>
|
||||
<Routes>
|
||||
{routerConfig.map(
|
||||
({ path, component: Component, name, children }) => (
|
||||
<Route key={name} path={path} element={<Component name={name} />}>
|
||||
{children && children.length
|
||||
? children.map((child) => (
|
||||
<Route
|
||||
key={`${name}-${child.path ? child.path : 'index'}`}
|
||||
{...child}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
</Route>
|
||||
)
|
||||
)}
|
||||
</Routes>
|
||||
</React.Suspense>
|
||||
<React.Suspense fallback={splashLoading}>{routes}</React.Suspense>
|
||||
</BoundaryWithTranslation>
|
||||
);
|
||||
};
|
||||
|
@ -10,7 +10,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { EpochCountdown } from '../../../components/epoch-countdown';
|
||||
import { Heading } from '../../../components/heading';
|
||||
import { SplashLoader } from '../../../components/splash-loader';
|
||||
import { NetworkParams } from '../../../config';
|
||||
import {
|
||||
AppStateActionType,
|
||||
useAppState,
|
||||
@ -18,7 +17,7 @@ import {
|
||||
import type { Rewards } from './__generated__/Rewards';
|
||||
import { RewardInfo } from './reward-info';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useNetworkParams } from '@vegaprotocol/react-helpers';
|
||||
import { useNetworkParams, NetworkParams } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export const REWARDS_QUERY = gql`
|
||||
query Rewards($partyId: ID!) {
|
||||
@ -73,33 +72,30 @@ export const RewardsIndex = () => {
|
||||
variables: { partyId: keypair?.pub },
|
||||
skip: !keypair?.pub,
|
||||
});
|
||||
const {
|
||||
data: rewardAssetData,
|
||||
loading: rewardAssetLoading,
|
||||
error: rewardAssetError,
|
||||
} = useNetworkParams([
|
||||
NetworkParams.REWARD_ASSET,
|
||||
NetworkParams.REWARD_PAYOUT_DURATION,
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.reward_asset,
|
||||
NetworkParams.reward_staking_delegation_payoutDelay,
|
||||
]);
|
||||
|
||||
const payoutDuration = React.useMemo(() => {
|
||||
if (!rewardAssetData || !rewardAssetData[1]) {
|
||||
if (!params) {
|
||||
return 0;
|
||||
}
|
||||
return new Duration(rewardAssetData[1]).milliseconds();
|
||||
}, [rewardAssetData]);
|
||||
return new Duration(
|
||||
params.reward_staking_delegation_payoutDelay
|
||||
).milliseconds();
|
||||
}, [params]);
|
||||
|
||||
if (error || rewardAssetError) {
|
||||
if (error) {
|
||||
return (
|
||||
<section>
|
||||
<p>{t('Something went wrong')}</p>
|
||||
{error && <pre>{error.message}</pre>}
|
||||
{rewardAssetError && <pre>{rewardAssetError.message}</pre>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading || rewardAssetLoading || !rewardAssetData?.length) {
|
||||
if (loading || !params) {
|
||||
return (
|
||||
<Splash>
|
||||
<SplashLoader />
|
||||
@ -145,7 +141,7 @@ export const RewardsIndex = () => {
|
||||
<RewardInfo
|
||||
currVegaKey={keypair}
|
||||
data={data}
|
||||
rewardAssetId={rewardAssetData[0]}
|
||||
rewardAssetId={params.reward_asset}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import Home from './home';
|
||||
import NotFound from './not-found';
|
||||
import NotPermitted from './not-permitted';
|
||||
@ -129,6 +130,48 @@ const LazyGovernancePropose = React.lazy(
|
||||
)
|
||||
);
|
||||
|
||||
const LazyGovernanceProposeNetworkParameter = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "route-governance-propose-network-parameter", webpackPrefetch: true */ './governance/propose/network-parameter'
|
||||
)
|
||||
);
|
||||
|
||||
const LazyGovernanceProposeNewMarket = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "route-governance-propose-new-market", webpackPrefetch: true */ './governance/propose/new-market'
|
||||
)
|
||||
);
|
||||
|
||||
const LazyGovernanceProposeUpdateMarket = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "route-governance-propose-update-market", webpackPrefetch: true */ './governance/propose/update-market'
|
||||
)
|
||||
);
|
||||
|
||||
const LazyGovernanceProposeNewAsset = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "route-governance-propose-new-asset", webpackPrefetch: true */ './governance/propose/new-asset'
|
||||
)
|
||||
);
|
||||
|
||||
const LazyGovernanceProposeFreeform = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "route-governance-propose-freeform", webpackPrefetch: true */ './governance/propose/freeform'
|
||||
)
|
||||
);
|
||||
|
||||
const LazyGovernanceProposeRaw = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "route-governance-propose-raw", webpackPrefetch: true */ './governance/propose/raw'
|
||||
)
|
||||
);
|
||||
|
||||
const LazyRewards = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
@ -153,14 +196,12 @@ const LazyWithdrawals = React.lazy(
|
||||
const routerConfig = [
|
||||
{
|
||||
path: Routes.HOME,
|
||||
name: 'Home',
|
||||
// Not lazy as loaded when a user first hits the site
|
||||
component: Home,
|
||||
element: <Home name="Home" />,
|
||||
},
|
||||
{
|
||||
path: Routes.TRANCHES,
|
||||
name: 'Tranches',
|
||||
component: LazyTranches,
|
||||
element: <LazyTranches name="Tranches" />,
|
||||
children: [
|
||||
{ index: true, element: <LazyTranchesTranches /> },
|
||||
{ path: ':trancheId', element: <LazyTranchesTranche /> },
|
||||
@ -168,13 +209,11 @@ const routerConfig = [
|
||||
},
|
||||
{
|
||||
path: Routes.CLAIM,
|
||||
name: 'Claim',
|
||||
component: LazyClaim,
|
||||
element: <LazyClaim name="Claim" />,
|
||||
},
|
||||
{
|
||||
path: Routes.STAKING,
|
||||
name: 'Staking',
|
||||
component: LazyStaking,
|
||||
element: <LazyStaking name="Staking" />,
|
||||
children: [
|
||||
{ path: 'associate', element: <LazyStakingAssociate /> },
|
||||
{ path: 'disassociate', element: <LazyStakingDisassociate /> },
|
||||
@ -191,18 +230,15 @@ const routerConfig = [
|
||||
},
|
||||
{
|
||||
path: Routes.REWARDS,
|
||||
name: 'Rewards',
|
||||
component: LazyRewards,
|
||||
element: <LazyRewards name="Rewards" />,
|
||||
},
|
||||
{
|
||||
path: Routes.WITHDRAWALS,
|
||||
name: 'Withdrawals',
|
||||
component: LazyWithdrawals,
|
||||
element: <LazyWithdrawals name="Withdrawals" />,
|
||||
},
|
||||
{
|
||||
path: Routes.VESTING,
|
||||
name: 'Vesting',
|
||||
component: LazyRedemption,
|
||||
element: <LazyRedemption name="Vesting" />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
@ -216,31 +252,48 @@ const routerConfig = [
|
||||
},
|
||||
{
|
||||
path: Routes.GOVERNANCE,
|
||||
name: 'Governance',
|
||||
component: LazyGovernance,
|
||||
element: <LazyGovernance name="Governance" />,
|
||||
children: [
|
||||
{ path: ':proposalId', element: <LazyGovernanceProposal /> },
|
||||
{ path: 'propose', element: <LazyGovernancePropose /> },
|
||||
{ path: 'rejected', element: <LazyRejectedGovernanceProposals /> },
|
||||
{ index: true, element: <LazyGovernanceProposals /> },
|
||||
{
|
||||
path: 'propose',
|
||||
element: <Outlet />,
|
||||
children: [
|
||||
{ index: true, element: <LazyGovernancePropose /> },
|
||||
{
|
||||
path: 'network-parameter',
|
||||
element: <LazyGovernanceProposeNetworkParameter />,
|
||||
},
|
||||
{
|
||||
path: 'new-market',
|
||||
element: <LazyGovernanceProposeNewMarket />,
|
||||
},
|
||||
{
|
||||
path: 'update-market',
|
||||
element: <LazyGovernanceProposeUpdateMarket />,
|
||||
},
|
||||
{ path: 'new-asset', element: <LazyGovernanceProposeNewAsset /> },
|
||||
{ path: 'freeform', element: <LazyGovernanceProposeFreeform /> },
|
||||
{ path: 'raw', element: <LazyGovernanceProposeRaw /> },
|
||||
],
|
||||
},
|
||||
{ path: ':proposalId', element: <LazyGovernanceProposal /> },
|
||||
{ path: 'rejected', element: <LazyRejectedGovernanceProposals /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.NOT_PERMITTED,
|
||||
name: 'Not permitted',
|
||||
// Not lazy as loaded when a user first hits the site
|
||||
component: NotPermitted,
|
||||
element: <NotPermitted name="Not permitted" />,
|
||||
},
|
||||
{
|
||||
path: Routes.CONTRACTS,
|
||||
name: 'Contracts',
|
||||
component: LazyContracts,
|
||||
element: <LazyContracts name="Contracts" />,
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
name: 'NotFound',
|
||||
// Not lazy as loaded when a user first hits the site
|
||||
component: NotFound,
|
||||
element: <NotFound name="NotFound" />,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -6,6 +6,7 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
import { addDecimal } from '@vegaprotocol/react-helpers';
|
||||
import type { Nodes_nodes } from './__generated__/Nodes';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import { ValidatorStatus } from '@vegaprotocol/types';
|
||||
|
||||
jest.mock('../../components/epoch-countdown', () => ({
|
||||
EpochCountdown: () => <div data-testid="epoch-info"></div>,
|
||||
@ -55,7 +56,7 @@ const MOCK_NODES = {
|
||||
stakeScore: '0.2300971220240714',
|
||||
performanceScore: '1',
|
||||
votingPower: '2408',
|
||||
status: 'tendermint',
|
||||
status: ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
||||
__typename: 'RankingScore',
|
||||
},
|
||||
}),
|
||||
@ -72,7 +73,7 @@ const MOCK_NODES = {
|
||||
stakeScore: '0.0966762995515676',
|
||||
performanceScore: '0.999629748500531',
|
||||
votingPower: '1163',
|
||||
status: 'tendermint',
|
||||
status: ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
||||
__typename: 'RankingScore',
|
||||
},
|
||||
}),
|
||||
@ -161,7 +162,7 @@ describe('Nodes list', () => {
|
||||
stakeScore: '0.2300971220240714',
|
||||
performanceScore: '1',
|
||||
votingPower: '2408',
|
||||
status: 'tendermint',
|
||||
status: ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
||||
__typename: 'RankingScore',
|
||||
},
|
||||
}),
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { useEffect, useMemo, useRef, forwardRef } from 'react';
|
||||
import { forwardRef, useEffect, useMemo, useRef } from 'react';
|
||||
import {
|
||||
AgGridDynamic as AgGrid,
|
||||
AsyncRenderer,
|
||||
@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { EpochCountdown } from '../../components/epoch-countdown';
|
||||
import { BigNumber } from '../../lib/bignumber';
|
||||
import { formatNumber } from '@vegaprotocol/react-helpers';
|
||||
import { ValidatorStatus } from '@vegaprotocol/types';
|
||||
import type { Nodes } from './__generated__/Nodes';
|
||||
import type { Staking_epoch } from './__generated__/Staking';
|
||||
import type { ColDef } from 'ag-grid-community';
|
||||
@ -136,7 +137,16 @@ export const NodeList = ({ epoch }: NodeListProps) => {
|
||||
? '-'
|
||||
: stakedOnNode.dividedBy(stakedTotal).times(100).dp(2).toString() +
|
||||
'%';
|
||||
const statusTranslated = t(`status-${status}`);
|
||||
const statusTranslated = t(
|
||||
`${
|
||||
(status === ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ &&
|
||||
'Ersatz') ||
|
||||
(status === ValidatorStatus.VALIDATOR_NODE_STATUS_PENDING &&
|
||||
'Pending') ||
|
||||
(status === ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT &&
|
||||
'Consensus')
|
||||
}`
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
|
@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { TokenInput } from '../../components/token-input';
|
||||
import { NetworkParams } from '../../config';
|
||||
import { useAppState } from '../../contexts/app-state/app-state-context';
|
||||
import { useSearchParams } from '../../hooks/use-search-params';
|
||||
import { BigNumber } from '../../lib/bignumber';
|
||||
@ -30,7 +29,7 @@ import type {
|
||||
UndelegateSubmissionBody,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useNetworkParam } from '@vegaprotocol/react-helpers';
|
||||
import { useNetworkParam, NetworkParams } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export const PARTY_DELEGATIONS_QUERY = gql`
|
||||
query PartyDelegations($partyId: ID!) {
|
||||
@ -103,14 +102,14 @@ export const StakingForm = ({
|
||||
setAmount('');
|
||||
}, [action, setAmount]);
|
||||
|
||||
const { data } = useNetworkParam(
|
||||
NetworkParams.VALIDATOR_DELEGATION_MIN_AMOUNT
|
||||
const { param: minAmount } = useNetworkParam(
|
||||
NetworkParams.validators_delegation_minAmount
|
||||
);
|
||||
|
||||
const minTokensWithDecimals = React.useMemo(() => {
|
||||
const minTokens = new BigNumber(data && data.length === 1 ? data[0] : '');
|
||||
const minTokens = new BigNumber(minAmount !== null ? minAmount : '');
|
||||
return addDecimal(minTokens, appState.decimals);
|
||||
}, [appState.decimals, data]);
|
||||
}, [appState.decimals, minAmount]);
|
||||
|
||||
const maxDelegation = React.useMemo(() => {
|
||||
if (action === Actions.Add) {
|
||||
|
@ -10,8 +10,11 @@ import {
|
||||
WithdrawalsTable,
|
||||
} from '@vegaprotocol/withdraws';
|
||||
import { useState } from 'react';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import type { RouteChildProps } from '../index';
|
||||
|
||||
const Withdrawals = () => {
|
||||
const Withdrawals = ({ name }: RouteChildProps) => {
|
||||
useDocumentTitle(name);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
|
@ -72,6 +72,8 @@ const getBundledEnvironmentValue = (key: EnvKey) => {
|
||||
return process.env['NX_VEGA_WALLET_URL'];
|
||||
case 'VEGA_TOKEN_URL':
|
||||
return process.env['NX_VEGA_TOKEN_URL'];
|
||||
case 'VEGA_DOCS_URL':
|
||||
return process.env['NX_VEGA_DOCS_URL'];
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -22,6 +22,7 @@ const schemaObject = {
|
||||
VEGA_ENV: z.nativeEnum(Networks),
|
||||
VEGA_EXPLORER_URL: z.optional(z.string()),
|
||||
VEGA_TOKEN_URL: z.optional(z.string()),
|
||||
VEGA_DOCS_URL: z.optional(z.string()),
|
||||
VEGA_NETWORKS: z
|
||||
.object(
|
||||
Object.keys(Networks).reduce(
|
||||
|
@ -1,3 +1,2 @@
|
||||
export * from './proposals-hooks';
|
||||
export * from './proposal-form';
|
||||
export * from './proposals-queries';
|
||||
|
@ -1,84 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
InputError,
|
||||
TextArea,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useProposalSubmit } from './proposals-hooks';
|
||||
import {
|
||||
getProposalDialogIcon,
|
||||
getProposalDialogIntent,
|
||||
getProposalDialogTitle,
|
||||
} from '../utils';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export interface FormFields {
|
||||
proposalData: string;
|
||||
}
|
||||
|
||||
export const ProposalForm = () => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useForm<FormFields>();
|
||||
const { finalizedProposal, submit, Dialog } = useProposalSubmit();
|
||||
|
||||
const hasError = Boolean(errors.proposalData?.message);
|
||||
|
||||
const onSubmit = async (fields: FormFields) => {
|
||||
await submit(JSON.parse(fields.proposalData));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormGroup
|
||||
label="Make a proposal by submitting JSON"
|
||||
labelFor="proposal-data"
|
||||
>
|
||||
<TextArea
|
||||
id="proposal-data"
|
||||
className="min-h-[200px]"
|
||||
hasError={hasError}
|
||||
data-testid="proposal-data"
|
||||
{...register('proposalData', {
|
||||
required: t('Required'),
|
||||
validate: {
|
||||
validateJson: (value) => {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return t('Must be valid JSON');
|
||||
}
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.proposalData?.message && (
|
||||
<InputError intent="danger">
|
||||
{errors.proposalData?.message}
|
||||
</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
data-testid="proposal-submit"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? t('Submitting') : t('Submit')} {t('Proposal')}
|
||||
</Button>
|
||||
<Dialog
|
||||
title={getProposalDialogTitle(finalizedProposal?.state)}
|
||||
intent={getProposalDialogIntent(finalizedProposal?.state)}
|
||||
icon={getProposalDialogIcon(finalizedProposal?.state)}
|
||||
>
|
||||
{finalizedProposal?.rejectionReason ? (
|
||||
<p>{finalizedProposal.rejectionReason}</p>
|
||||
) : undefined}
|
||||
</Dialog>
|
||||
</form>
|
||||
);
|
||||
};
|
33
libs/governance/src/utils/get-closing-timestamp.spec.ts
Normal file
33
libs/governance/src/utils/get-closing-timestamp.spec.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { getClosingTimestamp } from './get-closing-timestamp';
|
||||
import { addHours, addMinutes, getTime } from 'date-fns';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('getClosingTimestamp', () => {
|
||||
it('should return the correct timestamp if the proposalVoteDeadline is 1 (when 2 mins are added)', () => {
|
||||
const proposalVoteDeadline = '1';
|
||||
const expected = Math.floor(
|
||||
getTime(
|
||||
addHours(addMinutes(new Date(), 2), Number(proposalVoteDeadline))
|
||||
) / 1000
|
||||
);
|
||||
const actual = getClosingTimestamp(proposalVoteDeadline);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return the correct timestamp if the proposalVoteDeadline is 2 (when no extra mins are added)', () => {
|
||||
const proposalVoteDeadline = '2';
|
||||
const expected = Math.floor(
|
||||
getTime(addHours(new Date(), Number(proposalVoteDeadline))) / 1000
|
||||
);
|
||||
const actual = getClosingTimestamp(proposalVoteDeadline);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
14
libs/governance/src/utils/get-closing-timestamp.ts
Normal file
14
libs/governance/src/utils/get-closing-timestamp.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { addHours, addMinutes, getTime } from 'date-fns';
|
||||
|
||||
// If proposaVoteDeadline is at its minimum of 1 hour, then we add
|
||||
// 2 extra minutes to the closing timestamp to ensure that there's time
|
||||
// to confirm in the wallet.
|
||||
|
||||
export const getClosingTimestamp = (proposalVoteDeadline: string) =>
|
||||
Math.floor(
|
||||
getTime(
|
||||
proposalVoteDeadline === '1'
|
||||
? addHours(addMinutes(new Date(), 2), Number(proposalVoteDeadline))
|
||||
: addHours(new Date(), Number(proposalVoteDeadline))
|
||||
) / 1000
|
||||
);
|
31
libs/governance/src/utils/get-enactment-timestamp.spec.ts
Normal file
31
libs/governance/src/utils/get-enactment-timestamp.spec.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { getEnactmentTimestamp } from './get-enactment-timestamp';
|
||||
import { addHours, getTime } from 'date-fns';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('getEnactmentTimestamp', () => {
|
||||
it('should return the correct timestamp', () => {
|
||||
const proposalVoteDeadline = '2';
|
||||
const enactmentDeadline = '1';
|
||||
const expected = Math.floor(
|
||||
getTime(
|
||||
addHours(
|
||||
new Date(),
|
||||
Number(proposalVoteDeadline) + Number(enactmentDeadline)
|
||||
)
|
||||
) / 1000
|
||||
);
|
||||
const actual = getEnactmentTimestamp(
|
||||
proposalVoteDeadline,
|
||||
enactmentDeadline
|
||||
);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
15
libs/governance/src/utils/get-enactment-timestamp.ts
Normal file
15
libs/governance/src/utils/get-enactment-timestamp.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { addHours, fromUnixTime, getTime } from 'date-fns';
|
||||
import { getClosingTimestamp } from './get-closing-timestamp';
|
||||
|
||||
export const getEnactmentTimestamp = (
|
||||
proposalVoteDeadline: string,
|
||||
enactmentDeadline: string
|
||||
) =>
|
||||
Math.floor(
|
||||
getTime(
|
||||
addHours(
|
||||
new Date(fromUnixTime(getClosingTimestamp(proposalVoteDeadline))),
|
||||
Number(enactmentDeadline)
|
||||
)
|
||||
) / 1000
|
||||
);
|
33
libs/governance/src/utils/get-validation-timestamp.spec.ts
Normal file
33
libs/governance/src/utils/get-validation-timestamp.spec.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { addHours, addMinutes, getTime } from 'date-fns';
|
||||
import { getValidationTimestamp } from './get-validation-timestamp';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('getValidationTimestamp', () => {
|
||||
it('should return the correct timestamp if the proposalValidationDeadline is 0 (when 2 mins are added)', () => {
|
||||
const proposalValidationDeadline = '0';
|
||||
const expected = Math.floor(
|
||||
getTime(
|
||||
addHours(addMinutes(new Date(), 2), Number(proposalValidationDeadline))
|
||||
) / 1000
|
||||
);
|
||||
const actual = getValidationTimestamp(proposalValidationDeadline);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return the correct timestamp if the proposalValidationDeadline is 1 (when no extra mins are added)', () => {
|
||||
const proposalValidationDeadline = '1';
|
||||
const expected = Math.floor(
|
||||
getTime(addHours(new Date(), Number(proposalValidationDeadline))) / 1000
|
||||
);
|
||||
const actual = getValidationTimestamp(proposalValidationDeadline);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
17
libs/governance/src/utils/get-validation-timestamp.ts
Normal file
17
libs/governance/src/utils/get-validation-timestamp.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { addHours, addMinutes, getTime } from 'date-fns';
|
||||
|
||||
// If proposalValidationDeadline is at its minimum of 0 hours, then we add
|
||||
// 2 extra minutes to the validation timestamp to ensure that there's time
|
||||
// to confirm in the wallet.
|
||||
|
||||
export const getValidationTimestamp = (proposalValidationDeadline: string) =>
|
||||
Math.floor(
|
||||
getTime(
|
||||
proposalValidationDeadline === '0'
|
||||
? addHours(
|
||||
addMinutes(new Date(), 2),
|
||||
Number(proposalValidationDeadline)
|
||||
)
|
||||
: addHours(new Date(), Number(proposalValidationDeadline))
|
||||
) / 1000
|
||||
);
|
@ -1 +1,4 @@
|
||||
export * from './proposal-dialog-helpers';
|
||||
export * from './get-closing-timestamp';
|
||||
export * from './get-enactment-timestamp';
|
||||
export * from './get-validation-timestamp';
|
||||
|
@ -1,15 +1,13 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import type { LiquidityProvisionStatus } from '@vegaprotocol/types';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import { useNetworkParam } from '@vegaprotocol/react-helpers';
|
||||
import { useNetworkParam, NetworkParams } from '@vegaprotocol/react-helpers';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type {
|
||||
MarketLiquidity,
|
||||
MarketLiquidity_market_data_liquidityProviderFeeShare,
|
||||
} from './__generated__';
|
||||
|
||||
const SISKA_NETWORK_PARAMETER = 'market.liquidity.stakeToCcySiskas';
|
||||
|
||||
const MARKET_LIQUIDITY_QUERY = gql`
|
||||
query MarketLiquidity($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
@ -106,7 +104,9 @@ export const useLiquidityProvision = ({
|
||||
partyId?: string;
|
||||
marketId?: string;
|
||||
}) => {
|
||||
const { data: stakeToCcySiskas } = useNetworkParam(SISKA_NETWORK_PARAMETER);
|
||||
const { param: stakeToCcySiskas } = useNetworkParam(
|
||||
NetworkParams.market_liquidity_stakeToCcySiskas
|
||||
);
|
||||
const stakeToCcySiska = stakeToCcySiskas && stakeToCcySiskas[0];
|
||||
const { data, loading, error } = useQuery<MarketLiquidity>(
|
||||
MARKET_LIQUIDITY_QUERY,
|
||||
|
115
libs/react-helpers/src/hooks/use-network-params.spec.tsx
Normal file
115
libs/react-helpers/src/hooks/use-network-params.spec.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { NetworkParamsKey } from './use-network-params';
|
||||
import {
|
||||
NetworkParams,
|
||||
NETWORK_PARAMETERS_QUERY,
|
||||
NETWORK_PARAMETER_QUERY,
|
||||
useNetworkParam,
|
||||
useNetworkParams,
|
||||
} from './use-network-params';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { NetworkParams as NetworkParamsResponse } from './__generated__';
|
||||
|
||||
describe('useNetworkParam', () => {
|
||||
const setup = (arg: NetworkParamsKey) => {
|
||||
const mock: MockedResponse = {
|
||||
request: {
|
||||
query: NETWORK_PARAMETER_QUERY,
|
||||
variables: {
|
||||
key: arg,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
networkParameter: {
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'reward.staking.delegation.payoutDelay',
|
||||
value: '200',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={[mock]}>{children}</MockedProvider>
|
||||
);
|
||||
|
||||
return renderHook(() => useNetworkParam(arg), { wrapper });
|
||||
};
|
||||
|
||||
it('returns a single param value', async () => {
|
||||
const { result } = setup(
|
||||
NetworkParams.reward_staking_delegation_payoutDelay
|
||||
);
|
||||
expect(result.current.param).toBe(null);
|
||||
await waitFor(() => {
|
||||
expect(result.current.param).toEqual('200');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useNetworkParams', () => {
|
||||
const setup = (args?: NetworkParamsKey[]) => {
|
||||
const mock: MockedResponse<NetworkParamsResponse> = {
|
||||
request: {
|
||||
query: NETWORK_PARAMETERS_QUERY,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
networkParameters: [
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'spam.protection.proposal.min.tokens',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'governance.proposal.updateMarket.minProposerBalance',
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'reward.staking.delegation.payoutDelay',
|
||||
value: '200',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={[mock]}>{children}</MockedProvider>
|
||||
);
|
||||
|
||||
return renderHook(() => useNetworkParams(args), { wrapper });
|
||||
};
|
||||
it('returns an object with only desired params', async () => {
|
||||
const { result } = setup([
|
||||
NetworkParams.spam_protection_proposal_min_tokens,
|
||||
NetworkParams.governance_proposal_updateMarket_minProposerBalance,
|
||||
]);
|
||||
|
||||
expect(result.current.params).toBe(null);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.params).toEqual({
|
||||
spam_protection_proposal_min_tokens: '1',
|
||||
governance_proposal_updateMarket_minProposerBalance: '2',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('returns all params', async () => {
|
||||
const { result } = setup();
|
||||
|
||||
expect(result.current.params).toBe(null);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.params).toEqual({
|
||||
spam_protection_proposal_min_tokens: '1',
|
||||
governance_proposal_updateMarket_minProposerBalance: '2',
|
||||
reward_staking_delegation_payoutDelay: '200',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,5 +1,6 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import type { NetworkParams } from './__generated__/NetworkParams';
|
||||
import { useMemo } from 'react';
|
||||
import type { NetworkParams as NetworkParamsResponse } from './__generated__';
|
||||
|
||||
export const NETWORK_PARAMETERS_QUERY = gql`
|
||||
query NetworkParams {
|
||||
@ -10,27 +11,127 @@ export const NETWORK_PARAMETERS_QUERY = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const useNetworkParam = (param: string) => {
|
||||
const { data, loading, error } = useQuery<NetworkParams, never>(
|
||||
export const NETWORK_PARAMETER_QUERY = gql`
|
||||
query NetworkParam($key: String!) {
|
||||
networkParameter(key: $key) {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const NetworkParams = {
|
||||
blockchains_ethereumConfig: 'blockchains_ethereumConfig',
|
||||
reward_asset: 'reward_asset',
|
||||
reward_staking_delegation_payoutDelay:
|
||||
'reward_staking_delegation_payoutDelay',
|
||||
governance_proposal_updateMarket_requiredMajority:
|
||||
'governance_proposal_updateMarket_requiredMajority',
|
||||
governance_proposal_market_minClose: 'governance_proposal_market_minClose',
|
||||
governance_proposal_market_maxClose: 'governance_proposal_market_maxClose',
|
||||
governance_proposal_market_minEnact: 'governance_proposal_market_minEnact',
|
||||
governance_proposal_market_maxEnact: 'governance_proposal_market_maxEnact',
|
||||
governance_proposal_updateMarket_minClose:
|
||||
'governance_proposal_updateMarket_minClose',
|
||||
governance_proposal_updateMarket_maxClose:
|
||||
'governance_proposal_updateMarket_maxClose',
|
||||
governance_proposal_updateMarket_minEnact:
|
||||
'governance_proposal_updateMarket_minEnact',
|
||||
governance_proposal_updateMarket_maxEnact:
|
||||
'governance_proposal_updateMarket_maxEnact',
|
||||
governance_proposal_asset_minClose: 'governance_proposal_asset_minClose',
|
||||
governance_proposal_asset_maxClose: 'governance_proposal_asset_maxClose',
|
||||
governance_proposal_asset_minEnact: 'governance_proposal_asset_minEnact',
|
||||
governance_proposal_asset_maxEnact: 'governance_proposal_asset_maxEnact',
|
||||
governance_proposal_updateNetParam_minClose:
|
||||
'governance_proposal_updateNetParam_minClose',
|
||||
governance_proposal_updateNetParam_maxClose:
|
||||
'governance_proposal_updateNetParam_maxClose',
|
||||
governance_proposal_updateNetParam_minEnact:
|
||||
'governance_proposal_updateNetParam_minEnact',
|
||||
governance_proposal_updateNetParam_maxEnact:
|
||||
'governance_proposal_updateNetParam_maxEnact',
|
||||
governance_proposal_freeform_minClose:
|
||||
'governance_proposal_freeform_minClose',
|
||||
governance_proposal_freeform_maxClose:
|
||||
'governance_proposal_freeform_maxClose',
|
||||
governance_proposal_updateMarket_requiredParticipation:
|
||||
'governance_proposal_updateMarket_requiredParticipation',
|
||||
governance_proposal_updateMarket_minProposerBalance:
|
||||
'governance_proposal_updateMarket_minProposerBalance',
|
||||
governance_proposal_market_requiredMajority:
|
||||
'governance_proposal_market_requiredMajority',
|
||||
governance_proposal_market_requiredParticipation:
|
||||
'governance_proposal_market_requiredParticipation',
|
||||
governance_proposal_market_minProposerBalance:
|
||||
'governance_proposal_market_minProposerBalance',
|
||||
governance_proposal_asset_requiredMajority:
|
||||
'governance_proposal_asset_requiredMajority',
|
||||
governance_proposal_asset_requiredParticipation:
|
||||
'governance_proposal_asset_requiredParticipation',
|
||||
governance_proposal_asset_minProposerBalance:
|
||||
'governance_proposal_asset_minProposerBalance',
|
||||
governance_proposal_updateNetParam_requiredMajority:
|
||||
'governance_proposal_updateNetParam_requiredMajority',
|
||||
governance_proposal_updateNetParam_requiredParticipation:
|
||||
'governance_proposal_updateNetParam_requiredParticipation',
|
||||
governance_proposal_updateNetParam_minProposerBalance:
|
||||
'governance_proposal_updateNetParam_minProposerBalance',
|
||||
governance_proposal_freeform_requiredParticipation:
|
||||
'governance_proposal_freeform_requiredParticipation',
|
||||
governance_proposal_freeform_requiredMajority:
|
||||
'governance_proposal_freeform_requiredMajority',
|
||||
governance_proposal_freeform_minProposerBalance:
|
||||
'governance_proposal_freeform_minProposerBalance',
|
||||
validators_delegation_minAmount: 'validators_delegation_minAmount',
|
||||
spam_protection_proposal_min_tokens: 'spam_protection_proposal_min_tokens',
|
||||
market_liquidity_stakeToCcySiskas: 'market_liquidity_stakeToCcySiskas',
|
||||
} as const;
|
||||
|
||||
type Params = typeof NetworkParams;
|
||||
export type NetworkParamsKey = Params[keyof Params];
|
||||
type Result = {
|
||||
[key in keyof Params]: string;
|
||||
};
|
||||
|
||||
export const useNetworkParams = <T extends NetworkParamsKey[]>(params?: T) => {
|
||||
const { data, loading, error } = useQuery<NetworkParamsResponse, never>(
|
||||
NETWORK_PARAMETERS_QUERY
|
||||
);
|
||||
const foundParams = data?.networkParameters?.filter((p) => param === p.key);
|
||||
|
||||
const paramsObj = useMemo(() => {
|
||||
if (!data?.networkParameters) return null;
|
||||
return data.networkParameters
|
||||
.map((p) => ({
|
||||
...p,
|
||||
key: p.key.split('.').join('_'),
|
||||
}))
|
||||
.filter((p) => {
|
||||
if (params === undefined || params.length === 0) return true;
|
||||
if (params.includes(p.key as NetworkParamsKey)) return true;
|
||||
return false;
|
||||
})
|
||||
.reduce((obj, p) => {
|
||||
obj[p.key] = p.value;
|
||||
return obj;
|
||||
}, {} as { [key: string]: string });
|
||||
}, [data, params]);
|
||||
|
||||
return {
|
||||
data: foundParams ? foundParams.map((f) => f.value) : null,
|
||||
params: paramsObj as Pick<Result, T[number]>,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export const useNetworkParams = (params: string[]) => {
|
||||
const { data, loading, error } = useQuery<NetworkParams, never>(
|
||||
NETWORK_PARAMETERS_QUERY
|
||||
);
|
||||
const foundParams = data?.networkParameters
|
||||
?.filter((p) => params.includes(p.key))
|
||||
.sort((a, b) => params.indexOf(a.key) - params.indexOf(b.key));
|
||||
export const useNetworkParam = (param: NetworkParamsKey) => {
|
||||
const { data, loading, error } = useQuery(NETWORK_PARAMETER_QUERY, {
|
||||
variables: {
|
||||
key: param,
|
||||
},
|
||||
});
|
||||
return {
|
||||
data: foundParams ? foundParams.map((f) => f.value) : null,
|
||||
param: data?.networkParameter ? data.networkParameter.value : null,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
|
@ -36,3 +36,12 @@ export const maxSafe = (max: BigNumber) => (value: string) => {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const suitableForSyntaxHighlighter = (str: string) => {
|
||||
try {
|
||||
const test = JSON.parse(str);
|
||||
return test && Object.keys(test).length > 0;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
@ -34,6 +34,9 @@ const vegaCustomClasses = plugin(function ({ addUtilities }) {
|
||||
'.clip-path-rounded': {
|
||||
clipPath: 'circle(50%)',
|
||||
},
|
||||
'.color-scheme-dark': {
|
||||
colorScheme: 'dark',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -4,9 +4,10 @@ import type { ReactNode } from 'react';
|
||||
export interface FormGroupProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
label: string; // For accessibility reasons this must always be set for screen readers. If you want it to not show, then add labelClassName="sr-only"
|
||||
label: string; // For accessibility reasons this must always be set for screen readers. If you want it to not show, then use the hideLabel prop"
|
||||
labelFor: string; // Same as above
|
||||
hideLabel?: boolean;
|
||||
labelDescription?: string;
|
||||
labelAlign?: 'left' | 'right';
|
||||
}
|
||||
|
||||
@ -15,6 +16,7 @@ export const FormGroup = ({
|
||||
className,
|
||||
label,
|
||||
labelFor,
|
||||
labelDescription,
|
||||
labelAlign = 'left',
|
||||
hideLabel = false,
|
||||
}: FormGroupProps) => {
|
||||
@ -28,6 +30,9 @@ export const FormGroup = ({
|
||||
{label && (
|
||||
<label htmlFor={labelFor} className={labelClasses}>
|
||||
{label}
|
||||
{labelDescription && (
|
||||
<div className="font-light mt-1">{labelDescription}</div>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
{children}
|
||||
|
@ -36,9 +36,9 @@ Default.args = {
|
||||
labelFor: 'labelFor',
|
||||
};
|
||||
|
||||
export const Error = Template.bind({});
|
||||
Error.args = {
|
||||
export const WithLabelDescription = Template.bind({});
|
||||
WithLabelDescription.args = {
|
||||
label: 'Label',
|
||||
labelFor: 'labelFor',
|
||||
hasError: true,
|
||||
labelDescription: 'Description text',
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ export default {
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = (args) => (
|
||||
<FormGroup labelClassName="sr-only" label="Hello" labelFor={args.id}>
|
||||
<FormGroup label="Hello" labelFor={args.id}>
|
||||
<Input value="I type words" {...args} />
|
||||
</FormGroup>
|
||||
);
|
||||
@ -51,6 +51,8 @@ export const TypeDateTime = Template.bind({});
|
||||
TypeDateTime.args = {
|
||||
type: 'datetime-local',
|
||||
id: 'input-datetime-local',
|
||||
min: '2022-09-05T11:29:17',
|
||||
max: '2023-09-05T10:29:49',
|
||||
};
|
||||
|
||||
export const IconPrepend = Template.bind({});
|
||||
|
@ -130,10 +130,14 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
const hasPrepended = !!(prependIconName || prependElement);
|
||||
const hasAppended = !!(appendIconName || appendElement);
|
||||
|
||||
const inputClassName = classNames('appearance-none', className, {
|
||||
'pl-10': hasPrepended,
|
||||
'pr-10': hasAppended,
|
||||
});
|
||||
const inputClassName = classNames(
|
||||
'appearance-none dark:color-scheme-dark',
|
||||
className,
|
||||
{
|
||||
'pl-10': hasPrepended,
|
||||
'pr-10': hasAppended,
|
||||
}
|
||||
);
|
||||
|
||||
const input = (
|
||||
<input
|
||||
|
@ -79,7 +79,7 @@ export interface WithdrawSubmissionBody extends BaseTransaction {
|
||||
};
|
||||
}
|
||||
|
||||
interface ProposalNewMarketTerms {
|
||||
export interface ProposalNewMarketTerms {
|
||||
newMarket: {
|
||||
changes: {
|
||||
decimalPlaces: string;
|
||||
@ -119,7 +119,7 @@ interface ProposalNewMarketTerms {
|
||||
enactmentTimestamp: number;
|
||||
}
|
||||
|
||||
interface ProposalUpdateMarketTerms {
|
||||
export interface ProposalUpdateMarketTerms {
|
||||
updateMarket: {
|
||||
marketId: string;
|
||||
changes: {
|
||||
@ -141,7 +141,7 @@ interface ProposalUpdateMarketTerms {
|
||||
enactmentTimestamp: number;
|
||||
}
|
||||
|
||||
interface ProposalNetworkParameterTerms {
|
||||
export interface ProposalNetworkParameterTerms {
|
||||
updateNetworkParameter: {
|
||||
changes: {
|
||||
key: string;
|
||||
@ -152,12 +152,12 @@ interface ProposalNetworkParameterTerms {
|
||||
enactmentTimestamp: number;
|
||||
}
|
||||
|
||||
interface ProposalFreeformTerms {
|
||||
export interface ProposalFreeformTerms {
|
||||
newFreeform: Record<string, never>;
|
||||
closingTimestamp: number;
|
||||
}
|
||||
|
||||
interface ProposalNewAssetTerms {
|
||||
export interface ProposalNewAssetTerms {
|
||||
newAsset: {
|
||||
changes: {
|
||||
name: string;
|
||||
|
@ -55,6 +55,7 @@
|
||||
"i18next": "^20.3.5",
|
||||
"i18next-browser-languagedetector": "^6.1.2",
|
||||
"immer": "^9.0.12",
|
||||
"iso8601-duration": "^2.1.1",
|
||||
"js-sha3": "^0.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "12.2.3",
|
||||
|
@ -14443,6 +14443,11 @@ isexe@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
||||
|
||||
iso8601-duration@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/iso8601-duration/-/iso8601-duration-2.1.1.tgz#88d9e481525b50e57840bc93fb8a1727a7d849d2"
|
||||
integrity sha512-VGGpW30/R57FpG1J7RqqKBAaK7lIiudlZkQ5tRoO9hNlKYQNnhs60DQpXlPFBmp6I+kJ61PHkI3f/T7cR4wfbw==
|
||||
|
||||
isobject@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
|
||||
|
Loading…
Reference in New Issue
Block a user