Render extended statuses when voting on bounty (#4396)
This commit is contained in:
parent
c7a5b605d6
commit
4e1fe4b38d
@ -3,6 +3,7 @@
|
||||
"Account does not have enough funds.": "Account does not have enough funds.",
|
||||
"Add Bounty": "Add Bounty",
|
||||
"Allocation value is smaller than the minimum bounty value.": "Allocation value is smaller than the minimum bounty value.",
|
||||
"Approval under voting": "Approval under voting",
|
||||
"Approve": "Approve",
|
||||
"Bond": "Bond",
|
||||
"Bond is estimated based on bountyDepositBase and dataDepositPerByte constants.": "Bond is estimated based on bountyDepositBase and dataDepositPerByte constants.",
|
||||
@ -10,6 +11,7 @@
|
||||
"Calculated bond for bounty": "Calculated bond for bounty",
|
||||
"Claim": "Claim",
|
||||
"Claimable": "Claimable",
|
||||
"Curator under voting": "Curator under voting",
|
||||
"Curators deposit": "Curators deposit",
|
||||
"Curators fee": "Curators fee",
|
||||
"Description of the Bounty (to be stored on-chain)": "Description of the Bounty (to be stored on-chain)",
|
||||
@ -19,6 +21,7 @@
|
||||
"Proposer": "Proposer",
|
||||
"Proposer bond depends on bounty title length.": "Proposer bond depends on bounty title length.",
|
||||
"Reject": "Reject",
|
||||
"Rejection under voting": "Rejection under voting",
|
||||
"Select the account you wish to propose the bounty from.": "Select the account you wish to propose the bounty from.",
|
||||
"Select the council account you wish to use to create a motion for the Bounty.": "Select the council account you wish to use to create a motion for the Bounty.",
|
||||
"The council member that is will create a motion, submission equates to an \"aye\" vote for chosen option.": "The council member that is will create a motion, submission equates to an \"aye\" vote for chosen option.",
|
||||
@ -29,6 +32,7 @@
|
||||
"This action will create a Council motion to either approve or reject the Bounty.": "This action will create a Council motion to either approve or reject the Bounty.",
|
||||
"This amount will be reserved from origin account and returned on approval or slashed upon rejection.": "This amount will be reserved from origin account and returned on approval or slashed upon rejection.",
|
||||
"Title too long": "Title too long",
|
||||
"Unassign curator under voting": "Unassign curator under voting",
|
||||
"Use this account to propose bounty from. This can be a normal or council account.": "Use this account to propose bounty from. This can be a normal or council account.",
|
||||
"Value": "Value",
|
||||
"active": "active",
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
"All voters may alter their votes any number of times prior to the close of the poll.": "",
|
||||
"An account that is to receive the rewards": "",
|
||||
"Any combination of the four options may be approved of by the voter. There is no need to select only one option!": "",
|
||||
"Approval under voting": "",
|
||||
"Approving of all or none of the options is equivalent and will not affect the outcome of the poll.": "",
|
||||
"Asset ID": "",
|
||||
"Authorize transaction": "",
|
||||
@ -29,6 +30,7 @@
|
||||
"Contracts": "",
|
||||
"Controller account": "",
|
||||
"Create a backup file for this account": "",
|
||||
"Curator under voting": "",
|
||||
"Curators deposit": "",
|
||||
"Curators fee": "",
|
||||
"Dark theme (experimental, work-in-progress)": "",
|
||||
@ -75,6 +77,7 @@
|
||||
"Please read these terms and conditions carefully. By submitting this statement, you are deemed to have accepted these Terms and Conditions. If you do not agree to these terms, please refrain from accessing or proceeding. You can also find them at:": "",
|
||||
"Produced blocks": "",
|
||||
"Proposer": "",
|
||||
"Rejection under voting": "",
|
||||
"Retrieving data": "",
|
||||
"Retrieving nominators": "",
|
||||
"Retrieving validators": "",
|
||||
@ -126,6 +129,7 @@
|
||||
"This vote does not affect any economics of the Polkadot platform. Staking rewards, inflation, effective market capitalisation and the underlying balances of every account remain completely unchanged. It is \"merely\" about what units we use to denominate the balances into \"DOT\" for the purpose of display.": "",
|
||||
"Unable to find on-chain WASM code for the supplied codeHash": "",
|
||||
"Unable to retrieve the specified block details. {{error}}": "",
|
||||
"Unassign curator under voting": "",
|
||||
"Undelegate": "",
|
||||
"Unknown": "",
|
||||
"Unless you are a developer with insight into what the specific script does to your environment (based on reading the code being executed) generally the advice would be to not use this environment.": "",
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveBounties } from '@polkadot/api-derive/types';
|
||||
import type { SubmittableExtrinsic } from '@polkadot/api/types';
|
||||
import type { DeriveBounties, DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
import type { BlockNumber, Bounty, BountyIndex } from '@polkadot/types/interfaces';
|
||||
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
@ -10,8 +11,11 @@ import React, { Suspense } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
import { ApiPromise, WsProvider } from '@polkadot/api';
|
||||
import { lightTheme } from '@polkadot/apps/themes';
|
||||
import { POLKADOT_GENESIS } from '@polkadot/apps-config';
|
||||
import { Metadata } from '@polkadot/metadata';
|
||||
import metaStatic from '@polkadot/metadata/static';
|
||||
import { ApiContext } from '@polkadot/react-api';
|
||||
import { ApiProps } from '@polkadot/react-api/types';
|
||||
import i18next from '@polkadot/react-components/i18n';
|
||||
@ -24,8 +28,8 @@ function aBounty (): Bounty {
|
||||
return new TypeRegistry().createType('Bounty');
|
||||
}
|
||||
|
||||
function anIndex (): BountyIndex {
|
||||
return new TypeRegistry().createType('BountyIndex');
|
||||
function anIndex (index = 0): BountyIndex {
|
||||
return new TypeRegistry().createType('BountyIndex', index);
|
||||
}
|
||||
|
||||
function balanceOf (number: number) {
|
||||
@ -49,6 +53,7 @@ let mockBountyApi: BountyApi = {
|
||||
};
|
||||
|
||||
let mockBalance = balanceOf(1);
|
||||
let apiWithAugmentations: ApiPromise;
|
||||
|
||||
const mockTreasury = {
|
||||
burn: new BN(1),
|
||||
@ -69,9 +74,41 @@ jest.mock('@polkadot/react-hooks/useTreasury', () => {
|
||||
};
|
||||
});
|
||||
|
||||
export function createApiWithAugmentations (): ApiPromise {
|
||||
const registry = new TypeRegistry();
|
||||
const metadata = new Metadata(registry, metaStatic);
|
||||
|
||||
registry.setMetadata(metadata);
|
||||
|
||||
const api = new ApiPromise({ provider: new WsProvider('ws://', false), registry });
|
||||
|
||||
api.injectMetadata(metadata, true);
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
function bountyInStatus (status: string) {
|
||||
return new TypeRegistry().createType('Bounty', { status, value: new BN(151) });
|
||||
}
|
||||
|
||||
function aProposal (extrinsic: SubmittableExtrinsic<'promise'>) {
|
||||
return {
|
||||
hash: new TypeRegistry().createType('Hash'),
|
||||
proposal: apiWithAugmentations
|
||||
.registry.createType('Proposal', extrinsic),
|
||||
votes: apiWithAugmentations.registry.createType('Votes', {
|
||||
ayes: ['5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'],
|
||||
index: 0,
|
||||
nays: ['5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'],
|
||||
threshold: 4
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
describe('Bounties', () => {
|
||||
beforeAll(async () => {
|
||||
await i18next.changeLanguage('en');
|
||||
apiWithAugmentations = createApiWithAugmentations();
|
||||
});
|
||||
|
||||
const renderBounties = (bountyApi: Partial<BountyApi> = {}, { balance = 1 } = {}) => {
|
||||
@ -100,6 +137,42 @@ describe('Bounties', () => {
|
||||
);
|
||||
};
|
||||
|
||||
function renderOneBounty (bounty: Bounty, proposals: DeriveCollectiveProposal[] = [], description = '', index = anIndex()) {
|
||||
return renderBounties({ bounties: [{ bounty, description, index, proposals }] });
|
||||
}
|
||||
|
||||
it('creates correct bounty with proposal', () => {
|
||||
const bounty = apiWithAugmentations.registry.createType('Bounty', {
|
||||
curatorDeposit: new BN(1),
|
||||
status: 'Funded',
|
||||
value: new BN(10)
|
||||
});
|
||||
|
||||
expect(bounty.curatorDeposit.eq(new BN(1))).toBeTruthy();
|
||||
expect(bounty.status.isFunded).toBeTruthy();
|
||||
expect(bounty.value.eq(new BN(10))).toBeTruthy();
|
||||
});
|
||||
|
||||
it('creates correct proposal', () => {
|
||||
const proposal = apiWithAugmentations.registry.createType('Proposal', apiWithAugmentations.tx.bounties.proposeCurator(0, '5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z', 1));
|
||||
|
||||
expect(proposal.args[0].eq(new BN(0))).toBeTruthy();
|
||||
expect(proposal.args[1].toString()).toEqual('5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z');
|
||||
expect(proposal.args[2].eq(new BN(1))).toBeTruthy();
|
||||
expect(proposal.method).toEqual('proposeCurator');
|
||||
});
|
||||
|
||||
it('creates correct votes', () => {
|
||||
const votes = apiWithAugmentations.registry.createType('Votes', { ayes: ['5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'], index: 0, nays: ['5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'], threshold: 4 });
|
||||
|
||||
expect(votes.ayes.length).toEqual(1);
|
||||
expect(votes.ayes[0].toString()).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
|
||||
expect(votes.nays.length).toEqual(1);
|
||||
expect(votes.nays[0].toString()).toEqual('5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty');
|
||||
expect(votes.index.toNumber()).toEqual(0);
|
||||
expect(votes.threshold.toNumber()).toEqual(4);
|
||||
});
|
||||
|
||||
it('shows empty list when no bounties', async () => {
|
||||
const { findByText } = renderBounties();
|
||||
|
||||
@ -107,7 +180,7 @@ describe('Bounties', () => {
|
||||
});
|
||||
|
||||
it('renders a bounty', async () => {
|
||||
const { findByText, queryAllByText } = renderBounties({ bounties: [{ bounty: aBounty(), description: 'kusama comic book', index: anIndex(), proposals: [] }] });
|
||||
const { findByText, queryAllByText } = renderOneBounty(aBounty(), [], 'kusama comic book');
|
||||
|
||||
expect(await findByText('kusama comic book')).toBeTruthy();
|
||||
expect(queryAllByText('No open bounties')).toHaveLength(0);
|
||||
@ -148,4 +221,63 @@ describe('Bounties', () => {
|
||||
expect(await findByText('Account does not have enough funds.')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('status is extended when voting', () => {
|
||||
it('on proposed curator', async () => {
|
||||
const bounty = bountyInStatus('Funded');
|
||||
const proposals = [aProposal(apiWithAugmentations.tx.bounties.proposeCurator(0, '5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z', 1))];
|
||||
|
||||
const { findByText } = renderOneBounty(bounty, proposals);
|
||||
|
||||
expect(await findByText('Curator under voting')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('on bounty approval in proposed status', async () => {
|
||||
const bounty = bountyInStatus('Proposed');
|
||||
const proposals = [aProposal(apiWithAugmentations.tx.bounties.approveBounty(0))];
|
||||
|
||||
const { findByText } = renderOneBounty(bounty, proposals);
|
||||
|
||||
expect(await findByText('Approval under voting')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('on parallel bounty approval and bounty close in proposed status', async () => {
|
||||
const bounty = bountyInStatus('Proposed');
|
||||
const proposals = [
|
||||
aProposal(apiWithAugmentations.tx.bounties.closeBounty(0)),
|
||||
aProposal(apiWithAugmentations.tx.bounties.approveBounty(0))
|
||||
];
|
||||
|
||||
const { findByText } = renderOneBounty(bounty, proposals);
|
||||
|
||||
expect(await findByText('Approval under voting')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('on close bounty in active status', async () => {
|
||||
const bounty = bountyInStatus('Active');
|
||||
const proposals = [aProposal(apiWithAugmentations.tx.bounties.closeBounty(0))];
|
||||
|
||||
const { findByText } = renderOneBounty(bounty, proposals);
|
||||
|
||||
expect(await findByText('Rejection under voting')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('on unassign curator in active state', async () => {
|
||||
const bounty = bountyInStatus('Active');
|
||||
const proposals = [aProposal(apiWithAugmentations.tx.bounties.unassignCurator(0))];
|
||||
|
||||
const { findByText } = renderOneBounty(bounty, proposals);
|
||||
|
||||
expect(await findByText('Unassign curator under voting')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('on bounty approval in active state', async () => {
|
||||
const bounty = bountyInStatus('Active');
|
||||
const proposals = [aProposal(apiWithAugmentations.tx.bounties.approveBounty(0))];
|
||||
|
||||
const { findByTestId } = renderOneBounty(bounty, proposals);
|
||||
|
||||
await expect(findByTestId('extendedStatus')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -15,6 +15,7 @@ import { formatNumber } from '@polkadot/util';
|
||||
import { BountyActions } from './BountyActions';
|
||||
import { getBountyStatus } from './helpers';
|
||||
import { useTranslation } from './translate';
|
||||
import VotingDescription from './VotingDescription';
|
||||
|
||||
interface Props {
|
||||
bestNumber: BlockNumber;
|
||||
@ -52,7 +53,19 @@ function Bounty ({ bestNumber, bounty, className = '', description, index, propo
|
||||
return (
|
||||
<>
|
||||
<tr className={className}>
|
||||
<td>{bountyStatus}</td>
|
||||
<td>
|
||||
<div>
|
||||
<div>{bountyStatus}</div>
|
||||
{proposals && (
|
||||
<div>
|
||||
<VotingDescription
|
||||
proposals={proposals}
|
||||
status={status}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>{description}</td>
|
||||
<td><FormatBalance value={value} /></td>
|
||||
<td>{curator ? <AddressSmall value={curator} /> : EMPTY_CELL}</td>
|
||||
|
||||
47
packages/page-bounties/src/VotingDescription.tsx
Normal file
47
packages/page-bounties/src/VotingDescription.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
import type { BountyStatus } from '@polkadot/types/interfaces';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { bestValidProposalName } from './helpers/extendedStatuses';
|
||||
import { useTranslation } from './translate';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
proposals: DeriveCollectiveProposal[];
|
||||
status: BountyStatus;
|
||||
}
|
||||
|
||||
function VotingDescription ({ className = '', proposals, status }: Props): React.ReactElement<Props> {
|
||||
const bestProposalName = bestValidProposalName(proposals, status);
|
||||
const { t } = useTranslation();
|
||||
const descriptions: Record<string, string> = useMemo(() => ({
|
||||
approveBounty: t('Approval under voting'),
|
||||
closeBounty: t('Rejection under voting'),
|
||||
proposeCurator: t('Curator under voting'),
|
||||
unassignCurator: t('Unassign curator under voting')
|
||||
}), [t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{bestProposalName && (
|
||||
<div
|
||||
className={className}
|
||||
data-testid='extendedStatus'
|
||||
>
|
||||
<>{descriptions[bestProposalName] || null}</>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(VotingDescription)`
|
||||
font-size: 0.7rem;
|
||||
color: #8B8B8B;
|
||||
margin-top: 10px;
|
||||
`);
|
||||
37
packages/page-bounties/src/helpers/extendedStatuses.ts
Normal file
37
packages/page-bounties/src/helpers/extendedStatuses.ts
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
import type { BountyStatus } from '@polkadot/types/interfaces';
|
||||
|
||||
type BountyVotingStatus = 'Active' | 'Approved' | 'CuratorProposed' | 'Funded' | 'PendingPayout' | 'Proposed';
|
||||
type BountyVotingStatuses = { [status in BountyVotingStatus]: string[] };
|
||||
|
||||
const validProposalNames: BountyVotingStatuses = {
|
||||
Active: ['closeBounty', 'unassignCurator'],
|
||||
Approved: [],
|
||||
CuratorProposed: ['unassignCurator', 'closeBounty'],
|
||||
Funded: ['proposeCurator', 'closeBounty'],
|
||||
PendingPayout: ['unassignCurator'],
|
||||
Proposed: ['approveBounty', 'closeBounty']
|
||||
};
|
||||
|
||||
function validMethods (status: BountyStatus): string[] {
|
||||
return validProposalNames[status.type as BountyVotingStatus];
|
||||
}
|
||||
|
||||
function getProposalByMethod (bountyProposals: DeriveCollectiveProposal[], method: string | undefined): DeriveCollectiveProposal | undefined {
|
||||
return bountyProposals.find(({ proposal }) => proposal.method === method);
|
||||
}
|
||||
|
||||
export function bestValidProposalName (bountyProposals: DeriveCollectiveProposal[], status: BountyStatus): string | undefined {
|
||||
const methods = bountyProposals.map(({ proposal }) => proposal.method);
|
||||
|
||||
return validMethods(status).find((method) => methods.includes(method));
|
||||
}
|
||||
|
||||
export function getProposalToDisplay (bountyProposals: DeriveCollectiveProposal[], status: BountyStatus): DeriveCollectiveProposal | null {
|
||||
const method = bestValidProposalName(bountyProposals, status);
|
||||
|
||||
return getProposalByMethod(bountyProposals, method) ?? null;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user