Compare commits
50
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb603ce6ea | ||
|
|
cc0e4987e5 | ||
|
|
a96bfeec8f | ||
|
|
1c5f44bb6c | ||
|
|
9b9cd22b63 | ||
|
|
35eb7543a7 | ||
|
|
1ef017e14c | ||
|
|
a989d3a0b2 | ||
|
|
4f8a585956 | ||
|
|
e67f322400 | ||
|
|
2a700ddd84 | ||
|
|
238e019708 | ||
|
|
f701124f0f | ||
|
|
c5613f0107 | ||
|
|
a6ae5dd526 | ||
|
|
6d5aaa29ec | ||
|
|
9ff908cf4d | ||
|
|
bccfa05c89 | ||
|
|
01cb4fe74e | ||
|
|
0ccb70b68b | ||
|
|
55318f18bc | ||
|
|
22c2e49a98 | ||
|
|
48110180f2 | ||
|
|
bd3c27b9dc | ||
|
|
5a1b5b0efe | ||
|
|
c5fb26bd4c | ||
|
|
cb455e2399 | ||
|
|
81f11fbb6b | ||
|
|
aaecccc386 | ||
|
|
e3a0b9a3da | ||
|
|
7841c06121 | ||
|
|
15dfbee0d3 | ||
|
|
d2637a9645 | ||
|
|
7695ea567a | ||
|
|
a8a3753237 | ||
|
|
8cc499880c | ||
|
|
aedc4ba223 | ||
|
|
1579439730 | ||
|
|
8416d9a15e | ||
|
|
aa34dc6751 | ||
|
|
c39c111a1c | ||
|
|
2cdfb63050 | ||
|
|
52b1c24070 | ||
|
|
3a0a527805 | ||
|
|
b174d275c7 | ||
|
|
dc32655e02 | ||
|
|
7d4ac4648a | ||
|
|
d314b20a08 | ||
|
|
5925dc006f | ||
|
|
b05da4dceb |
@@ -10,6 +10,7 @@ import {
|
||||
formatNumber,
|
||||
t,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { isJsonObject } from '@vegaprotocol/react-helpers';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import type {
|
||||
NetworkParametersQuery,
|
||||
@@ -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 {
|
||||
|
||||
@@ -8,3 +8,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
|
||||
|
||||
@@ -8,3 +8,4 @@ NX_ETHERSCAN_URL=https://etherscan.io
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_VEGA_REST=https://api.token.vega.xyz/
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/docs/mainnet
|
||||
|
||||
@@ -8,6 +8,7 @@ NX_VEGA_REST=https://api.n01.stagnet3.vega.xyz
|
||||
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/mainnet
|
||||
|
||||
# App flags
|
||||
NX_EXPLORER_ASSETS=1
|
||||
|
||||
@@ -8,3 +8,4 @@ NX_ETHERSCAN_URL=https://ropsten.etherscan.io
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_VEGA_REST=https://api.n11.testnet.vega.xyz
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/docs/testnet
|
||||
|
||||
@@ -52,7 +52,9 @@ export const ENV = {
|
||||
branch: windowOrDefault('NX_BRANCH'),
|
||||
vegaUrl: windowOrDefault('NX_VEGA_URL'),
|
||||
restUrl: windowOrDefault('NX_VEGA_REST'),
|
||||
explorerUrl: windowOrDefault('NX_VEGA_EXPLORER'),
|
||||
urlConnect: TRUTHY.includes(windowOrDefault('NX_ETH_URL_CONNECT')),
|
||||
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,60 @@
|
||||
"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",
|
||||
"MinProposalRequirements1": "You must have at least",
|
||||
"MinProposalRequirements2": "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 the",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ENV } from '../config/env';
|
||||
import { ENV } from '../config';
|
||||
|
||||
export function getDataNodeUrl() {
|
||||
const base = ENV.vegaUrl;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export * from './proposal-docs-link';
|
||||
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,16 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalDocsLink } from './proposal-docs-link';
|
||||
|
||||
describe('Proposal Docs Link', () => {
|
||||
it('should render successfully', () => {
|
||||
render(<ProposalDocsLink urlPart1="https://test.com" />);
|
||||
expect(screen.getByTestId('link')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display link text', () => {
|
||||
render(<ProposalDocsLink urlPart1="https://test.com" />);
|
||||
expect(
|
||||
screen.getByText('https://test.com/tutorials/proposals')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import classnames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
interface ProposalDocsLinkProps {
|
||||
urlPart1: string;
|
||||
urlPart2?: string;
|
||||
}
|
||||
|
||||
export const ProposalDocsLink = ({
|
||||
urlPart1,
|
||||
urlPart2,
|
||||
}: ProposalDocsLinkProps) => {
|
||||
const { t } = useTranslation();
|
||||
const docsLink = `${urlPart1}${urlPart2 ? urlPart2 : '/tutorials/proposals'}`;
|
||||
const linkStyles = classnames('underline mb-2');
|
||||
|
||||
return (
|
||||
<p>
|
||||
{t('ProposalDocsPrefix')}{' '}
|
||||
<Link href={docsLink} target="_blank" className={linkStyles}>
|
||||
{docsLink}
|
||||
</Link>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalFormDescription } from './proposal-form-description';
|
||||
|
||||
describe('Proposal Form Description', () => {
|
||||
it('should render successfully', () => {
|
||||
const register = jest.fn();
|
||||
render(
|
||||
<ProposalFormDescription
|
||||
registerField={register('proposalDescription')}
|
||||
errorMessage={undefined}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('proposal-description')).toBeTruthy();
|
||||
});
|
||||
|
||||
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 = function ({
|
||||
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>
|
||||
);
|
||||
};
|
||||
+32
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 && Number(addDecimal(minProposerBalance, 18));
|
||||
const spamProtectionMinFormatted =
|
||||
spamProtectionMin && Number(addDecimal(spamProtectionMin, 18));
|
||||
|
||||
const larger =
|
||||
Number(minProposerBalanceFormatted) > (spamProtectionMinFormatted || 0)
|
||||
? minProposerBalanceFormatted
|
||||
: spamProtectionMinFormatted;
|
||||
|
||||
return (
|
||||
<p className="mb-4">{`${t('MinProposalRequirements1')} ${larger} ${t(
|
||||
'MinProposalRequirements2'
|
||||
)}`}</p>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalFormSubheader } from './proposal-form-subheader';
|
||||
|
||||
describe('Proposal Form Subheader', () => {
|
||||
it('should display subheader text', () => {
|
||||
render(<ProposalFormSubheader>Test</ProposalFormSubheader>);
|
||||
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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,19 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalFormSubmit } from './proposal-form-submit';
|
||||
|
||||
describe('Proposal Form Submit', () => {
|
||||
it('should render successfully', () => {
|
||||
render(<ProposalFormSubmit isSubmitting={false} />);
|
||||
expect(screen.getByTestId('proposal-submit')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display submitting text', () => {
|
||||
render(<ProposalFormSubmit isSubmitting={true} />);
|
||||
expect(screen.getByText('Submitting Proposal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display submit text', () => {
|
||||
render(<ProposalFormSubmit isSubmitting={false} />);
|
||||
expect(screen.getByText('Submit Proposal')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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,26 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalFormTerms } from './proposal-form-terms';
|
||||
|
||||
describe('Proposal Form Terms', () => {
|
||||
it('should render successfully', () => {
|
||||
const register = jest.fn();
|
||||
render(
|
||||
<ProposalFormTerms
|
||||
registerField={register('proposalTerms')}
|
||||
errorMessage={undefined}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('proposal-terms')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display error text', () => {
|
||||
const register = jest.fn();
|
||||
render(
|
||||
<ProposalFormTerms
|
||||
registerField={register('proposalTerms')}
|
||||
errorMessage="Error text"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Error text')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FormGroup, InputError, TextArea } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { ProposalDocsLink } from './proposal-docs-link';
|
||||
import type { UseFormRegisterReturn } from 'react-hook-form';
|
||||
|
||||
interface ProposalFormTermsProps {
|
||||
registerField: UseFormRegisterReturn<'proposalTerms'>;
|
||||
errorMessage: string | undefined;
|
||||
labelOverride?: string;
|
||||
}
|
||||
|
||||
export const ProposalFormTerms = function ({
|
||||
registerField: register,
|
||||
errorMessage,
|
||||
labelOverride,
|
||||
}: ProposalFormTermsProps) {
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<FormGroup
|
||||
label={labelOverride || t('ProposalTerms')}
|
||||
labelFor="proposal-terms"
|
||||
>
|
||||
<div className="mt-[-4px] mb-2 text-sm font-light">
|
||||
{VEGA_DOCS_URL && <ProposalDocsLink urlPart1={VEGA_DOCS_URL} />}
|
||||
</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,26 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalFormTitle } from './proposal-form-title';
|
||||
|
||||
describe('Proposal Form Title', () => {
|
||||
it('should render successfully', () => {
|
||||
const register = jest.fn();
|
||||
render(
|
||||
<ProposalFormTitle
|
||||
registerField={register('proposalTitle')}
|
||||
errorMessage={undefined}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('proposal-title')).toBeTruthy();
|
||||
});
|
||||
|
||||
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 = function ({
|
||||
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>
|
||||
);
|
||||
};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalFormTransactionDialog } from './proposal-form-transaction-dialog';
|
||||
|
||||
const mockTransactionDialog = jest
|
||||
.fn()
|
||||
.mockImplementation(() => <div>Mock Transaction Dialog</div>);
|
||||
|
||||
describe('Proposal Form Transaction Dialog', () => {
|
||||
it('should render', () => {
|
||||
render(
|
||||
<ProposalFormTransactionDialog
|
||||
finalizedProposal={null}
|
||||
TransactionDialog={mockTransactionDialog}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Mock Transaction Dialog')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
getProposalDialogIcon,
|
||||
getProposalDialogIntent,
|
||||
getProposalDialogTitle,
|
||||
} from '@vegaprotocol/governance';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
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">
|
||||
{finalizedProposal?.rejectionReason ? (
|
||||
<TransactionDialog
|
||||
title={t('Proposal rejected')}
|
||||
intent={getProposalDialogIntent(ProposalState.STATE_REJECTED)}
|
||||
icon={getProposalDialogIcon(ProposalState.STATE_REJECTED)}
|
||||
>
|
||||
<p>{finalizedProposal.rejectionReason}</p>
|
||||
</TransactionDialog>
|
||||
) : (
|
||||
<TransactionDialog
|
||||
title={getProposalDialogTitle(finalizedProposal?.state)}
|
||||
intent={getProposalDialogIntent(finalizedProposal?.state)}
|
||||
icon={getProposalDialogIcon(finalizedProposal?.state)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
+194
@@ -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(0);
|
||||
});
|
||||
|
||||
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/1970, 2:02:00 AM'
|
||||
);
|
||||
expect(screen.getByTestId('validation-date')).toHaveTextContent(
|
||||
'1/1/1970, 1:02:00 AM'
|
||||
);
|
||||
expect(screen.getByTestId('enactment-date')).toHaveTextContent(
|
||||
'1/1/1970, 3: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/1970, 2:02:30 AM'
|
||||
);
|
||||
expect(screen.getByTestId('validation-date')).toHaveTextContent(
|
||||
'1/1/1970, 1:02:30 AM'
|
||||
);
|
||||
expect(screen.getByTestId('enactment-date')).toHaveTextContent(
|
||||
'1/1/1970, 3: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/1970, 3:00:00 AM'
|
||||
);
|
||||
expect(screen.getByTestId('enactment-date')).toHaveTextContent(
|
||||
'1/1/1970, 4: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/1970, 6:00:00 AM'
|
||||
);
|
||||
expect(validationDeadlineInput).toHaveValue(5);
|
||||
fireEvent.click(voteDeadlineMinButton);
|
||||
expect(screen.getByTestId('validation-date')).toHaveTextContent(
|
||||
'1/1/1970, 2:00:00 AM'
|
||||
);
|
||||
expect(validationDeadlineInput).toHaveValue(1);
|
||||
});
|
||||
});
|
||||
+380
@@ -0,0 +1,380 @@
|
||||
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';
|
||||
|
||||
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 = function ({
|
||||
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<{
|
||||
vote: number;
|
||||
enactment: number;
|
||||
validation: number;
|
||||
}>({
|
||||
vote: minVoteHours,
|
||||
enactment: minEnactmentHours,
|
||||
validation: 0,
|
||||
});
|
||||
|
||||
const [deadlineDates, setDeadlineDates] = useState<{
|
||||
vote: Date;
|
||||
enactment: Date;
|
||||
validation: Date;
|
||||
}>({
|
||||
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 && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
{enactmentMinClose && enactmentMaxClose && maxEnactmentHours && (
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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,77 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposeFreeform } from './propose-freeform';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import {
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
} 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';
|
||||
|
||||
jest.mock('../../../../hooks/use-network-param.tsx', () => ({
|
||||
useNetworkParamWithKeys: () => ({
|
||||
data: [
|
||||
{
|
||||
__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.freeform.min.tokens',
|
||||
value: '1000000000000000000',
|
||||
},
|
||||
],
|
||||
loading: false,
|
||||
error: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<Router>
|
||||
<MockedProvider mocks={[networkParamsQueryMock]}>
|
||||
<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', () => {
|
||||
const { baseElement } = renderComponent();
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the title', () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText('New freeform proposal')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
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,
|
||||
ProposalDocsLink,
|
||||
} from '../../components/propose';
|
||||
import { AsyncRenderer, Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { Heading } from '../../../../components/heading';
|
||||
import { VegaWalletContainer } from '../../../../components/vega-wallet-container';
|
||||
import type { ProposalFreeformTerms } from '@vegaprotocol/wallet';
|
||||
import { useNetworkParams, NetworkParams } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export interface FreeformProposalFormFields {
|
||||
proposalVoteDeadline: string;
|
||||
proposalTitle: string;
|
||||
proposalDescription: string;
|
||||
proposalTerms: string;
|
||||
proposalReference: string;
|
||||
}
|
||||
|
||||
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),
|
||||
} as ProposalFreeformTerms,
|
||||
});
|
||||
};
|
||||
|
||||
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 && (
|
||||
<div className="text-sm">
|
||||
<ProposalDocsLink
|
||||
urlPart1={VEGA_DOCS_URL}
|
||||
urlPart2={'/tutorials/proposals/freeform-proposal'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{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';
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { ProposeNetworkParameter } from './propose-network-parameter';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import {
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
} 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';
|
||||
|
||||
jest.mock('../../../../hooks/use-network-param.tsx', () => ({
|
||||
useNetworkParamWithKeys: () => ({
|
||||
data: [
|
||||
{
|
||||
__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',
|
||||
},
|
||||
{
|
||||
key: 'limits.assets.proposeEnabledFrom',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
key: 'limits.markets.proposeEnabledFrom',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
key: 'market.auction.maximumDuration',
|
||||
value: '168h0m0s',
|
||||
},
|
||||
],
|
||||
loading: false,
|
||||
error: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<Router>
|
||||
<MockedProvider mocks={[networkParamsQueryMock]}>
|
||||
<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', () => {
|
||||
const { baseElement } = renderComponent();
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the correct title', () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText('Update network parameter proposal')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the network param select element with no initial value', () => {
|
||||
renderComponent();
|
||||
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', () => {
|
||||
renderComponent();
|
||||
const select = screen.getByTestId('proposal-parameter-select');
|
||||
expect(select).toHaveValue('');
|
||||
|
||||
fireEvent.change(screen.getByTestId('proposal-parameter-select'), {
|
||||
target: {
|
||||
value: 'spam.protection.proposal.min.tokens',
|
||||
},
|
||||
});
|
||||
|
||||
expect(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();
|
||||
});
|
||||
});
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { isJsonObject, 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,
|
||||
ProposalDocsLink,
|
||||
} 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>
|
||||
|
||||
{isJsonObject(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;
|
||||
}
|
||||
|
||||
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 && (
|
||||
<div className="text-sm">
|
||||
<ProposalDocsLink
|
||||
urlPart1={VEGA_DOCS_URL}
|
||||
urlPart2={'/tutorials/proposals/network-parameter-proposal'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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,77 @@
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import {
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
} from '../../test-helpers/mocks';
|
||||
import { ProposeNewAsset } from './propose-new-asset';
|
||||
|
||||
jest.mock('../../../../hooks/use-network-param.tsx', () => ({
|
||||
useNetworkParamWithKeys: () => ({
|
||||
data: [
|
||||
{
|
||||
__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',
|
||||
},
|
||||
],
|
||||
loading: false,
|
||||
error: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<Router>
|
||||
<MockedProvider mocks={[networkParamsQueryMock]}>
|
||||
<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', () => {
|
||||
const { baseElement } = renderComponent();
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the title', () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText('New asset proposal')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
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,
|
||||
ProposalDocsLink,
|
||||
} from '../../components/propose';
|
||||
import { AsyncRenderer, Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { Heading } from '../../../../components/heading';
|
||||
import { VegaWalletContainer } from '../../../../components/vega-wallet-container';
|
||||
import type { ProposalNewAssetTerms } from '@vegaprotocol/wallet';
|
||||
import { NetworkParams, useNetworkParams } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export interface NewAssetProposalFormFields {
|
||||
proposalVoteDeadline: string;
|
||||
proposalEnactmentDeadline: string;
|
||||
proposalValidationDeadline: string;
|
||||
proposalTitle: string;
|
||||
proposalDescription: string;
|
||||
proposalTerms: string;
|
||||
proposalReference: string;
|
||||
}
|
||||
|
||||
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
|
||||
),
|
||||
} as ProposalNewAssetTerms,
|
||||
});
|
||||
};
|
||||
|
||||
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 && (
|
||||
<div className="text-sm">
|
||||
<ProposalDocsLink
|
||||
urlPart1={VEGA_DOCS_URL}
|
||||
urlPart2={'/tutorials/proposals/new-asset-proposal'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{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}
|
||||
/>
|
||||
|
||||
<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,33 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { ProposeNewMarket } from './propose-new-market';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import {
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
} 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';
|
||||
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<Router>
|
||||
<MockedProvider mocks={[networkParamsQueryMock]}>
|
||||
<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('ProposeNewMarket', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = renderComponent();
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
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,
|
||||
ProposalDocsLink,
|
||||
} from '../../components/propose';
|
||||
import { AsyncRenderer, Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { Heading } from '../../../../components/heading';
|
||||
import { VegaWalletContainer } from '../../../../components/vega-wallet-container';
|
||||
import type { ProposalNewMarketTerms } from '@vegaprotocol/wallet';
|
||||
import { NetworkParams, useNetworkParams } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export interface NewMarketProposalFormFields {
|
||||
proposalVoteDeadline: string;
|
||||
proposalEnactmentDeadline: string;
|
||||
proposalTitle: string;
|
||||
proposalDescription: string;
|
||||
proposalTerms: string;
|
||||
proposalReference: string;
|
||||
}
|
||||
|
||||
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
|
||||
),
|
||||
} as ProposalNewMarketTerms,
|
||||
});
|
||||
};
|
||||
|
||||
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 && (
|
||||
<div className="text-sm">
|
||||
<ProposalDocsLink
|
||||
urlPart1={VEGA_DOCS_URL}
|
||||
urlPart2={'/tutorials/proposals/new-market-proposal'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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}
|
||||
/>
|
||||
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -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,85 @@
|
||||
import Routes from '../../routes';
|
||||
import classnames from 'classnames';
|
||||
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';
|
||||
import { ProposalDocsLink } from '../components/propose';
|
||||
|
||||
export const Propose = () => {
|
||||
const { VEGA_DOCS_URL, VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const linkStyles = classnames('block underline mb-2');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading title={t('NewProposal')} />
|
||||
<VegaWalletContainer>
|
||||
{() => (
|
||||
<>
|
||||
<p>{t('MinProposalRequirements')}</p>
|
||||
<ProposalForm />
|
||||
</>
|
||||
<section className="pb-6">
|
||||
<Heading title={t('NewProposal')} />
|
||||
{VEGA_DOCS_URL && <ProposalDocsLink urlPart1={VEGA_DOCS_URL} />}
|
||||
{VEGA_EXPLORER_URL && (
|
||||
<p>
|
||||
{t('MoreProposalsInfo')}{' '}
|
||||
<Link
|
||||
href={`${VEGA_EXPLORER_URL}/governance`}
|
||||
target="_blank"
|
||||
>{`${VEGA_EXPLORER_URL}/governance`}</Link>
|
||||
</p>
|
||||
)}
|
||||
</VegaWalletContainer>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-h5">{t('ProposalTypeQuestion')}</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<Link
|
||||
href={`${Routes.GOVERNANCE}/propose/network-parameter`}
|
||||
className={linkStyles}
|
||||
>
|
||||
{t('NetworkParameter')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href={`${Routes.GOVERNANCE}/propose/new-market`}
|
||||
className={linkStyles}
|
||||
>
|
||||
{t('NewMarket')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href={`${Routes.GOVERNANCE}/propose/update-market`}
|
||||
className={linkStyles}
|
||||
>
|
||||
{t('UpdateMarket')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href={`${Routes.GOVERNANCE}/propose/new-asset`}
|
||||
className={linkStyles}
|
||||
>
|
||||
{t('NewAsset')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href={`${Routes.GOVERNANCE}/propose/freeform`}
|
||||
className={linkStyles}
|
||||
>
|
||||
{t('Freeform')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href={`${Routes.GOVERNANCE}/propose/raw`}
|
||||
className={linkStyles}
|
||||
>
|
||||
{t('RawProposal')}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { ProposeRaw, ProposeRaw as default } from './propose-raw';
|
||||
+19
-16
@@ -1,5 +1,6 @@
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
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 +9,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 +45,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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
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 {
|
||||
ProposalDocsLink,
|
||||
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 && (
|
||||
<div className="text-sm">
|
||||
<ProposalDocsLink urlPart1={VEGA_DOCS_URL} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Generated
+57
@@ -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';
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import {
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
} from '../../test-helpers/mocks';
|
||||
import { ProposeUpdateMarket } from './propose-update-market';
|
||||
|
||||
jest.mock('../../../../hooks/use-network-param.tsx', () => ({
|
||||
useNetworkParamWithKeys: () => ({
|
||||
data: [
|
||||
{
|
||||
__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',
|
||||
},
|
||||
],
|
||||
loading: false,
|
||||
error: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@apollo/client', () => ({
|
||||
...jest.requireActual('@apollo/client'),
|
||||
useQuery: jest.fn(() => ({
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
error: undefined,
|
||||
})),
|
||||
}));
|
||||
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<Router>
|
||||
<MockedProvider mocks={[networkParamsQueryMock]}>
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposeUpdateMarket />
|
||||
</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 Update Market', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = renderComponent();
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the title', () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText('Update market proposal')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the select element with no initial value', () => {
|
||||
renderComponent();
|
||||
expect(screen.getByTestId('proposal-market-select')).toHaveValue('');
|
||||
});
|
||||
|
||||
it('should render the correct market details when the market select is used', () => {
|
||||
renderComponent();
|
||||
|
||||
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,300 @@
|
||||
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 { ProposalUpdateMarketTerms } from '@vegaprotocol/wallet';
|
||||
import type { ProposalMarketsQuery } from './__generated__/ProposalMarketsQuery';
|
||||
import { NetworkParams, useNetworkParams } from '@vegaprotocol/react-helpers';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 } = 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
|
||||
),
|
||||
} as ProposalUpdateMarketTerms,
|
||||
});
|
||||
};
|
||||
|
||||
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_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}
|
||||
/>
|
||||
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -129,6 +129,48 @@ const LazyGovernancePropose = React.lazy(
|
||||
)
|
||||
);
|
||||
|
||||
const LazyGovernanceProposeNetworkParameter = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "route-governance-propose", webpackPrefetch: true */ './governance/propose/network-parameter'
|
||||
)
|
||||
);
|
||||
|
||||
const LazyGovernanceProposeNewMarket = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "route-governance-propose", webpackPrefetch: true */ './governance/propose/new-market'
|
||||
)
|
||||
);
|
||||
|
||||
const LazyGovernanceProposeUpdateMarket = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "route-governance-propose", webpackPrefetch: true */ './governance/propose/update-market'
|
||||
)
|
||||
);
|
||||
|
||||
const LazyGovernanceProposeNewAsset = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "route-governance-propose", webpackPrefetch: true */ './governance/propose/new-asset'
|
||||
)
|
||||
);
|
||||
|
||||
const LazyGovernanceProposeFreeform = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "route-governance-propose", webpackPrefetch: true */ './governance/propose/freeform'
|
||||
)
|
||||
);
|
||||
|
||||
const LazyGovernanceProposeRaw = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "route-governance-propose", webpackPrefetch: true */ './governance/propose/raw'
|
||||
)
|
||||
);
|
||||
|
||||
const LazyRewards = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
@@ -220,6 +262,21 @@ const routerConfig = [
|
||||
component: LazyGovernance,
|
||||
children: [
|
||||
{ path: ':proposalId', element: <LazyGovernanceProposal /> },
|
||||
{
|
||||
path: 'propose/network-parameter',
|
||||
element: <LazyGovernanceProposeNetworkParameter />,
|
||||
},
|
||||
{
|
||||
path: 'propose/new-market',
|
||||
element: <LazyGovernanceProposeNewMarket />,
|
||||
},
|
||||
{
|
||||
path: 'propose/update-market',
|
||||
element: <LazyGovernanceProposeUpdateMarket />,
|
||||
},
|
||||
{ path: 'propose/new-asset', element: <LazyGovernanceProposeNewAsset /> },
|
||||
{ path: 'propose/freeform', element: <LazyGovernanceProposeFreeform /> },
|
||||
{ path: 'propose/raw', element: <LazyGovernanceProposeRaw /> },
|
||||
{ path: 'propose', element: <LazyGovernancePropose /> },
|
||||
{ path: 'rejected', element: <LazyRejectedGovernanceProposals /> },
|
||||
{ index: true, element: <LazyGovernanceProposals /> },
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { getClosingTimestamp } from './get-closing-timestamp';
|
||||
import { addHours, addMinutes, getTime } from 'date-fns';
|
||||
|
||||
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(Date.now()), 2), 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(Date.now()), proposalVoteDeadline)) / 1000
|
||||
);
|
||||
const actual = getClosingTimestamp(proposalVoteDeadline);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
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(Date.now()), 2),
|
||||
Number(proposalVoteDeadline)
|
||||
)
|
||||
: addHours(new Date(Date.now()), Number(proposalVoteDeadline))
|
||||
) / 1000
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
import { getEnactmentTimestamp } from './get-enactment-timestamp';
|
||||
import { addHours, getTime } from 'date-fns';
|
||||
|
||||
describe('getEnactmentTimestamp', () => {
|
||||
it('should return the correct timestamp', () => {
|
||||
const proposalVoteDeadline = 1;
|
||||
const enactmentDeadline = 1;
|
||||
const expected = Math.floor(
|
||||
getTime(
|
||||
addHours(new Date(Date.now()), proposalVoteDeadline + enactmentDeadline)
|
||||
) / 1000
|
||||
);
|
||||
const actual = getEnactmentTimestamp(
|
||||
proposalVoteDeadline,
|
||||
enactmentDeadline
|
||||
);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
// test getValidationTimestamp
|
||||
|
||||
import { addHours, addMinutes, getTime } from 'date-fns';
|
||||
import { getValidationTimestamp } from './get-validation-timestamp';
|
||||
|
||||
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(Date.now()), 2),
|
||||
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(Date.now()), proposalValidationDeadline)) / 1000
|
||||
);
|
||||
const actual = getValidationTimestamp(proposalValidationDeadline);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -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(Date.now()), 2),
|
||||
Number(proposalValidationDeadline)
|
||||
)
|
||||
: addHours(new Date(Date.now()), 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!, $partyId: String) {
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
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__';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
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,11 @@ export const maxSafe = (max: BigNumber) => (value: string) => {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isJsonObject = (str: string) => {
|
||||
try {
|
||||
return JSON.parse(str) && Object.keys(JSON.parse(str)).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,13 @@ interface ProposalNetworkParameterTerms {
|
||||
enactmentTimestamp: number;
|
||||
}
|
||||
|
||||
interface ProposalFreeformTerms {
|
||||
export interface ProposalFreeformTerms {
|
||||
newFreeform: Record<string, never>;
|
||||
closingTimestamp: number;
|
||||
validationTimestamp: 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",
|
||||
|
||||
@@ -14834,6 +14834,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"
|
||||
|
||||
Reference in New Issue
Block a user