From ba15fa8fed4718ce53b21436d1e9753e3fc17fdc Mon Sep 17 00:00:00 2001 From: MiZiet <55240109+MiZiet@users.noreply.github.com> Date: Mon, 18 Jan 2021 20:58:26 +0100 Subject: [PATCH] Add extend bounty action (#4445) * [WIP] Add ExtendBountyExpiry action * Add Extend bounty expiry action * use renderOneBounty function in curator tests * [WIP] Add happy path test * [WIP] Happy path test * Add happy path test * Finish test * Update locales * Improve extend bounty expiry test * Cleanup bounty indexes * Change locales * Remove AccountId from ExtendBountyExpiryAction * Remove unused properties in bounty tests * Apply review suggestions --- .../apps/public/locales/en/app-bounties.json | 13 ++- packages/page-bounties/src/Bounties.spec.tsx | 62 ++++++++---- packages/page-bounties/src/Bounty.tsx | 4 +- packages/page-bounties/src/BountyActions.tsx | 12 ++- .../page-bounties/src/BountyClaimAction.tsx | 4 +- .../src/BountyCuratorProposedActions.tsx | 4 +- .../src/BountyInitiateVoting.tsx | 3 +- .../src/ExtendBountyExpiryAction.tsx | 99 +++++++++++++++++++ .../src/ProposeCuratorAction.tsx | 4 +- .../src/helpers/truncateTitle.ts | 2 +- .../page-bounties/src/hooks/useBounties.tsx | 3 + 11 files changed, 174 insertions(+), 36 deletions(-) create mode 100644 packages/page-bounties/src/ExtendBountyExpiryAction.tsx diff --git a/packages/apps/public/locales/en/app-bounties.json b/packages/apps/public/locales/en/app-bounties.json index 0e1ca86b3d..fef17f62ca 100644 --- a/packages/apps/public/locales/en/app-bounties.json +++ b/packages/apps/public/locales/en/app-bounties.json @@ -9,7 +9,6 @@ "Assign curator": "Assign curator", "Bond": "Bond", "Bounties": "Bounties", - "Calculated bond for bounty": "Calculated bond for bounty", "Choose a curator whose background and expertise is such that they are capable of determining when the task is complete.": "Choose a curator whose background and expertise is such that they are capable of determining when the task is complete.", "Claim": "Claim", "Claimable": "Claimable", @@ -17,37 +16,41 @@ "Curator's deposit": "Curator's deposit", "Curator's fee": "Curator's fee", "Curator's fee can't be higher than bounty value.": "Curator's fee can't be higher than bounty value.", - "Curator's fee.": "Curator's fee.", "Description of the Bounty (to be stored on-chain)": "Description of the Bounty (to be stored on-chain)", + "Extend Expiry": "Extend Expiry", "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", + "Only curator can extend the bounty time.": "Only curator can extend the bounty time.", "Part of the bounty value that will go to the Curator as a reward for their work": "Part of the bounty value that will go to the Curator as a reward for their work", "Propose Curator": "Propose Curator", "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 Curator.": "Select Curator.", "Select an account which (after a successful vote) will act as a curator.": "Select an account which (after a successful vote) will act as a curator.", "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 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 the motion.": "The council member that will create the motion.", "The description of this bounty": "The description of this bounty", + "The note on the extension.": "The note on the extension.", + "The note that will be added to the transaction. It won't be stored on chain": "The note that will be added to the transaction. It won't be stored on chain", "The total payment amount of this bounty, curator's fee included.": "The total payment amount of this bounty, curator's fee included.", + "This account will be used to create an extend bounty expire transaction.": "This account will be used to create an extend bounty expire transaction.", "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 assign a Curator.": "This action will create a Council motion to assign a Curator.", "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 action will extend expiry time of the selected bounty.": "This action will extend expiry time of the selected 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", "beneficiary": "beneficiary", "bounties": "bounties", "bounty bond": "bounty bond", + "bounty remark": "bounty remark", "bounty requested allocation": "bounty requested allocation", "bounty title": "bounty title", "curator": "curator", @@ -67,4 +70,4 @@ "value": "value", "vote with account": "vote with account", "{{blocks}} blocks": "{{blocks}} blocks" -} \ No newline at end of file +} diff --git a/packages/page-bounties/src/Bounties.spec.tsx b/packages/page-bounties/src/Bounties.spec.tsx index 820f561a55..b4b0a00d5e 100644 --- a/packages/page-bounties/src/Bounties.spec.tsx +++ b/packages/page-bounties/src/Bounties.spec.tsx @@ -48,6 +48,8 @@ function aGenesisHash () { return new TypeRegistry().createType('Hash', POLKADOT_GENESIS); } +const extendBountyExpiry = jest.fn().mockReturnValue('mockProposeExtrinsic'); + let mockBountyApi: BountyApi = { approveBounty: jest.fn(), bestNumber: new BN(1) as BlockNumber, @@ -56,6 +58,7 @@ let mockBountyApi: BountyApi = { bountyValueMinimum: new BN(1), closeBounty: jest.fn(), dataDepositPerByte: new BN(1), + extendBountyExpiry, maximumReasonLength: 100, proposeBounty: jest.fn(), proposeCurator: jest.fn() @@ -64,7 +67,7 @@ let mockBountyApi: BountyApi = { let mockBalance = balanceOf(1); let apiWithAugmentations: ApiPromise; const mockMembers = { isMember: true }; - +const mockAccounts = { allAccounts: ['5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM'] }; const mockTreasury = { burn: new BN(1), spendPeriod: new BN(0), @@ -121,6 +124,12 @@ jest.mock('@polkadot/react-hooks/useMembers', () => { }; }); +jest.mock('@polkadot/react-hooks/useAccounts', () => { + return { + useAccounts: () => mockAccounts + }; +}); + const propose = jest.fn().mockReturnValue('mockProposeExtrinsic'); let queueExtrinsic: QueueTxExtrinsicAdd; @@ -276,12 +285,9 @@ describe('Bounties', () => { describe('propose curator modal', () => { it('shows an error if fee is greater than bounty value', async () => { - const { findByTestId, findByText } = renderBounties({ bounties: [ - { bounty: aBounty({ status: bountyStatus('Funded'), value: balanceOf(5) }), - description: 'kusama comic book', - index: anIndex(), - proposals: [] } - ] }); + const bounty = { status: bountyStatus('Funded'), value: balanceOf(5) }; + const { findByTestId, findByText } = renderOneBounty(aBounty(bounty)); + const proposeCuratorButton = await findByText('Propose Curator'); fireEvent.click(proposeCuratorButton); @@ -295,12 +301,9 @@ describe('Bounties', () => { }); it('disables Assign Curator button if validation fails', async () => { - const { findByTestId, findByText } = renderBounties({ bounties: [ - { bounty: aBounty({ status: bountyStatus('Funded'), value: balanceOf(5) }), - description: 'kusama comic book', - index: anIndex(), - proposals: [] } - ] }); + const bounty = { status: bountyStatus('Funded'), value: balanceOf(5) }; + const { findByTestId, findByText } = renderOneBounty(aBounty(bounty)); + const proposeCuratorButton = await findByText('Propose Curator'); fireEvent.click(proposeCuratorButton); @@ -316,12 +319,9 @@ describe('Bounties', () => { }); it('queues propose extrinsic on submit', async () => { - const { findByTestId, findByText, getAllByRole } = renderBounties({ bounties: [ - { bounty: aBounty({ status: bountyStatus('Funded'), value: balanceOf(5) }), - description: 'kusama comic book', - index: anIndex(), - proposals: [] } - ] }); + const bounty = { status: bountyStatus('Funded') }; + const { findByTestId, findByText, getAllByRole } = renderOneBounty(aBounty(bounty)); + const proposeCuratorButton = await findByText('Propose Curator'); fireEvent.click(proposeCuratorButton); @@ -347,6 +347,30 @@ describe('Bounties', () => { }); }); + describe('extend bounty expiry action modal', () => { + it('queues extend bounty expiry extrinsic on submit', async () => { + const bounty = bountyInStatus('Active'); + + const { findByTestId, findByText } = renderOneBounty(bounty); + + const extendBountyExpiryButton = await findByText('Extend Expiry'); + + fireEvent.click(extendBountyExpiryButton); + + expect(await findByText('This action will extend expiry time of the selected bounty.')).toBeTruthy(); + + const remarkInput = await findByTestId('bounty remark'); + + fireEvent.change(remarkInput, { target: { value: 'The bounty extend expiry remark' } }); + + const acceptButton = await findByText('Accept'); + + fireEvent.click(acceptButton); + expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: '5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM', extrinsic: 'mockProposeExtrinsic' })); + expect(extendBountyExpiry).toHaveBeenCalledWith(anIndex(0), 'The bounty extend expiry remark'); + }); + }); + describe('status is extended when voting', () => { it('on proposed curator', async () => { const bounty = bountyInStatus('Funded'); diff --git a/packages/page-bounties/src/Bounty.tsx b/packages/page-bounties/src/Bounty.tsx index bb4c4c9543..737f5a51c1 100644 --- a/packages/page-bounties/src/Bounty.tsx +++ b/packages/page-bounties/src/Bounty.tsx @@ -2,7 +2,7 @@ // 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 type { BlockNumber, Bounty as BountyType, BountyIndex } from '@polkadot/types/interfaces'; import BN from 'bn.js'; import React, { useCallback, useMemo, useState } from 'react'; @@ -22,7 +22,7 @@ interface Props { bounty: BountyType; className?: string; description: string; - index: number; + index: BountyIndex; proposals?: DeriveCollectiveProposal[]; } diff --git a/packages/page-bounties/src/BountyActions.tsx b/packages/page-bounties/src/BountyActions.tsx index e34b1ab044..0bcfd9508c 100644 --- a/packages/page-bounties/src/BountyActions.tsx +++ b/packages/page-bounties/src/BountyActions.tsx @@ -2,20 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types'; -import type { Balance, BlockNumber, BountyStatus } from '@polkadot/types/interfaces'; +import type { Balance, BlockNumber, BountyIndex, BountyStatus } from '@polkadot/types/interfaces'; import React, { useCallback, useMemo } from 'react'; import BountyClaimAction from './BountyClaimAction'; import BountyCuratorProposedActions from './BountyCuratorProposedActions'; import BountyInitiateVoting from './BountyInitiateVoting'; +import ExtendBountyExpiryAction from './ExtendBountyExpiryAction'; import { getBountyStatus } from './helpers'; import ProposeCuratorAction from './ProposeCuratorAction'; interface Props { bestNumber: BlockNumber; description: string; - index: number; + index: BountyIndex; proposals?: DeriveCollectiveProposal[]; status: BountyStatus; value: Balance; @@ -48,6 +49,13 @@ export function BountyActions ({ bestNumber, description, index, proposals, stat curatorId={curator} index={index} />} + {status.isActive && curator && + + } {status.isPendingPayout && beneficiary && blocksUntilPayout && | null { + const { t } = useTranslation(); + const { extendBountyExpiry } = useBounties(); + const [isOpen, toggleOpen] = useToggle(); + const { allAccounts } = useAccounts(); + const [remark, setRemark] = useState(''); + const isCurator = useMemo(() => allAccounts.includes(curatorId.toString()), [allAccounts, curatorId]); + + const onRemarkChange = useCallback((value: string) => { + setRemark(value); + }, []); + + return isCurator + ? ( + <> +