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
+ ? (
+ <>
+