Initiate voting from bounty list - accept / reject bounty (#4320)

* Add initiate voting functionality

* Use bounty proposals from bounty derive

* Apply review suggestions

* Apply review suggestions

* Use useRef for approveBounty and closeBounty extrinsics
This commit is contained in:
Ewa Kowalska 2021-01-06 14:24:16 +01:00 committed by GitHub
parent adf3f3b160
commit 967d10b6cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 130 additions and 4 deletions

View File

@ -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.",
"Approve": "Approve",
"Bond": "Bond",
"Bond is estimated based on bountyDepositBase and dataDepositPerByte constants.": "Bond is estimated based on bountyDepositBase and dataDepositPerByte constants.",
"Bounties": "Bounties",
@ -13,14 +14,19 @@
"Curators fee": "Curators fee",
"Description of the Bounty (to be stored on-chain)": "Description of the Bounty (to be stored on-chain)",
"How much should be paid out for completed Bounty. Upon funding, the amount will be reserved in treasury.": "How much should be paid out for completed Bounty. Upon funding, the amount will be reserved in treasury.",
"Initiate Voting": "Initiate Voting",
"No open bounties": "No open bounties",
"Proposer": "Proposer",
"Proposer bond depends on bounty title length.": "Proposer bond depends on bounty title length.",
"Reject": "Reject",
"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.",
"The council member that will create a motion, submission equates to an \"aye\" vote for chosen option.": "The council member that will create a motion, submission equates to an \"aye\" vote for chosen option.",
"The description of this bounty": "The description of this bounty",
"The total payment amount of this bounty, curators fee included.": "The total payment amount of this bounty, curators fee included.",
"This account will propose the bounty. Bond amount will be reserved on its balance.": "This account will propose the bounty. Bond amount will be reserved on its balance.",
"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",
"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.",
@ -36,5 +42,6 @@
"title": "title",
"update due": "update due",
"value": "value",
"vote with account": "vote with account",
"{{blocks}} blocks": "{{blocks}} blocks"
}

View File

@ -12,6 +12,7 @@
"No open tips": "No open tips",
"No pending proposals": "No pending proposals",
"Of the beneficiary amount, at least {{bondPercentage}} would need to be put up as collateral. The maximum of this and the minimum bond will be used to secure the proposal, refundable if it passes.": "Of the beneficiary amount, at least {{bondPercentage}} would need to be put up as collateral. The maximum of this and the minimum bond will be used to secure the proposal, refundable if it passes.",
"Proposal can either be to approve or reject this spend. Once approved, the change is applied by either removing the proposal or scheduling payout.": "Proposal can either be to approve or reject this spend. Once approved, the change is applied by either removing the proposal or scheduling payout.",
"Proposal can either be to approve or reject this spend. One approved, the change is applied by either removing the proposal or scheduling payout.": "Proposal can either be to approve or reject this spend. One approved, the change is applied by either removing the proposal or scheduling payout.",
"Proposals": "Proposals",
"Propose tip": "Propose tip",

View File

@ -41,13 +41,14 @@ function Bounties (): React.ReactElement {
empty={deriveBounties && t<string>('No open bounties')}
header={headerRef.current}
>
{deriveBounties && bestNumber && deriveBounties.map(({ bounty, description, index }) => (
{deriveBounties && bestNumber && deriveBounties.map(({ bounty, description, index, proposals }) => (
<Bounty
bestNumber={bestNumber}
bounty={bounty}
description={description}
index={index}
key={index.toNumber()}
proposals={proposals}
/>
))}
</Table>

View File

@ -1,6 +1,7 @@
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import type { BlockNumber, Bounty as BountyType } from '@polkadot/types/interfaces';
import BN from 'bn.js';
@ -21,6 +22,7 @@ interface Props {
className?: string;
description: string;
index: number;
proposals?: DeriveCollectiveProposal[];
}
interface DueProps {
@ -29,7 +31,7 @@ interface DueProps {
const EMPTY_CELL = '-';
function Bounty ({ bestNumber, bounty, className = '', description, index }: Props): React.ReactElement<Props> {
function Bounty ({ bestNumber, bounty, className = '', description, index, proposals }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [isExpanded, setIsExpanded] = useState(false);
@ -61,6 +63,7 @@ function Bounty ({ bestNumber, bounty, className = '', description, index }: Pro
<BountyActions
bestNumber={bestNumber}
index={index}
proposals={proposals}
status={status}
/>
</td>

View File

@ -1,21 +1,24 @@
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import type { BlockNumber, BountyStatus } from '@polkadot/types/interfaces';
import React, { useCallback, useMemo } from 'react';
import BountyClaimAction from './BountyClaimAction';
import BountyCuratorProposedActions from './BountyCuratorProposedActions';
import BountyInitiateVoting from './BountyInitiateVoting';
import { getBountyStatus } from './helpers';
interface Props {
bestNumber: BlockNumber;
index: number;
proposals?: DeriveCollectiveProposal[];
status: BountyStatus;
}
export function BountyActions ({ bestNumber, index, status }: Props): JSX.Element {
export function BountyActions ({ bestNumber, index, proposals, status }: Props): JSX.Element {
const updateStatus = useCallback(() => getBountyStatus(status), [status]);
const { beneficiary, curator, unlockAt } = updateStatus();
@ -24,6 +27,11 @@ export function BountyActions ({ bestNumber, index, status }: Props): JSX.Elemen
return (
<>
{status.isProposed &&
<BountyInitiateVoting
index={index}
proposals={proposals}
/>}
{status.isCuratorProposed && curator &&
<BountyCuratorProposedActions
curatorId={curator}

View File

@ -0,0 +1,106 @@
// Copyright 2017-2020 @polkadot/app-treasury authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import BN from 'bn.js';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { getTreasuryProposalThreshold } from '@polkadot/apps-config';
import { Button, InputAddress, Modal, TxButton } from '@polkadot/react-components';
import { useApi, useMembers, useToggle } from '@polkadot/react-hooks';
import { useTranslation } from './translate';
interface Props {
index: number;
proposals?: DeriveCollectiveProposal[];
}
const BOUNTY_METHODS = ['approveBounty', 'closeBounty'];
function BountyInitiateVoting ({ index, proposals }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const { isMember, members } = useMembers();
const [isOpen, toggleOpen] = useToggle();
const [accountId, setAccountId] = useState<string | null>(null);
const [threshold, setThreshold] = useState<BN>();
useEffect((): void => {
members && setThreshold(
new BN(Math.ceil(members.length * getTreasuryProposalThreshold(api)))
);
}, [api, members]);
const approveBountyProposal = useRef((api.tx.bounties || api.tx.treasury).approveBounty(index));
const closeBountyProposal = useRef((api.tx.bounties || api.tx.treasury).closeBounty(index));
const isVotingInitiated = useMemo(() => proposals?.filter(({ proposal }) => BOUNTY_METHODS.includes(proposal.method)).length !== 0, [proposals]);
return isMember && !isVotingInitiated
? (
<>
<Button
icon='step-forward'
isDisabled={false}
label={t<string>('Initiate Voting')}
onClick={toggleOpen}
/>
{isOpen && (
<Modal
header={t<string>('Initiate Voting')}
size='large'
>
<Modal.Content>
<Modal.Columns>
<Modal.Column>
<p>{t<string>('This action will create a Council motion to either approve or reject the Bounty.')}</p>
</Modal.Column>
<Modal.Column>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<InputAddress
filter={members}
help={t<string>('Select the council account you wish to use to create a motion for the Bounty.')}
label={t<string>('vote with account')}
onChange={setAccountId}
type='account'
withLabel
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('The council member that will create a motion, submission equates to an "aye" vote for chosen option.')}</p>
</Modal.Column>
</Modal.Columns>
</Modal.Content>
<Modal.Actions onCancel={toggleOpen}>
<TxButton
accountId={accountId}
icon='check'
isDisabled={false}
label={t<string>('Approve')}
onStart={toggleOpen}
params={[threshold, approveBountyProposal.current, approveBountyProposal.current.length]}
tx={api.tx.council.propose}
/>
<TxButton
accountId={accountId}
icon='ban'
isDisabled={false}
label={t<string>('Reject')}
onStart={toggleOpen}
params={[threshold, closeBountyProposal.current, closeBountyProposal.current.length]}
tx={api.tx.council.propose}
/>
</Modal.Actions>
</Modal>
)}
</>
)
: null;
}
export default React.memo(BountyInitiateVoting);

View File

@ -80,7 +80,7 @@ function Council ({ id, isDisabled, members }: Props): React.ReactElement<Props>
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('Proposal can either be to approve or reject this spend. One approved, the change is applied by either removing the proposal or scheduling payout.')}</p>
<p>{t<string>('Proposal can either be to approve or reject this spend. Once approved, the change is applied by either removing the proposal or scheduling payout.')}</p>
</Modal.Column>
</Modal.Columns>
</Modal.Content>