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
This commit is contained in:
parent
be14d3a8d6
commit
ba15fa8fed
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
|
||||
@ -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 &&
|
||||
<ExtendBountyExpiryAction
|
||||
curatorId={curator}
|
||||
description={description}
|
||||
index={index}
|
||||
/>
|
||||
}
|
||||
{status.isPendingPayout && beneficiary && blocksUntilPayout &&
|
||||
<BountyClaimAction
|
||||
beneficiaryId={beneficiary}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId } from '@polkadot/types/interfaces';
|
||||
import type { AccountId, BountyIndex } from '@polkadot/types/interfaces';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useMemo } from 'react';
|
||||
@ -14,7 +14,7 @@ import { useTranslation } from './translate';
|
||||
|
||||
interface Props {
|
||||
beneficiaryId: AccountId;
|
||||
index: number;
|
||||
index: BountyIndex;
|
||||
payoutDue: BN;
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId } from '@polkadot/types/interfaces';
|
||||
import type { AccountId, BountyIndex } from '@polkadot/types/interfaces';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
@ -12,7 +12,7 @@ import { useTranslation } from './translate';
|
||||
|
||||
interface Props {
|
||||
curatorId: AccountId;
|
||||
index: number;
|
||||
index: BountyIndex;
|
||||
}
|
||||
|
||||
function BountyCuratorProposedActions ({ curatorId, index }: Props) {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
import type { BountyIndex } from '@polkadot/types/interfaces';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
@ -14,7 +15,7 @@ import { useBounties } from './hooks';
|
||||
import { useTranslation } from './translate';
|
||||
|
||||
interface Props {
|
||||
index: number;
|
||||
index: BountyIndex;
|
||||
proposals?: DeriveCollectiveProposal[];
|
||||
}
|
||||
|
||||
|
||||
99
packages/page-bounties/src/ExtendBountyExpiryAction.tsx
Normal file
99
packages/page-bounties/src/ExtendBountyExpiryAction.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
// Copyright 2017-2021 @polkadot/app-treasury authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId, BountyIndex } from '@polkadot/types/interfaces';
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { Button, Input, InputAddress, Modal, TxButton } from '@polkadot/react-components';
|
||||
import { useAccounts, useToggle } from '@polkadot/react-hooks';
|
||||
|
||||
import { truncateTitle } from './helpers';
|
||||
import { useBounties } from './hooks';
|
||||
import { useTranslation } from './translate';
|
||||
|
||||
interface Props {
|
||||
description: string
|
||||
index: BountyIndex;
|
||||
curatorId: AccountId;
|
||||
}
|
||||
|
||||
function ExtendBountyExpiryAction ({ curatorId, description, index }: Props): React.ReactElement<Props> | 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
|
||||
? (
|
||||
<>
|
||||
<Button
|
||||
icon='calendar-plus'
|
||||
isDisabled={false}
|
||||
label={t<string>('Extend Expiry')}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
header={t<string>(`Extend Expiry of "${truncateTitle(description, 30)}"`)}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('This action will extend expiry time of the selected bounty.')}</p>
|
||||
</Modal.Column>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
help={t<string>('This account will be used to create an extend bounty expire transaction.')}
|
||||
isDisabled
|
||||
label={t<string>('curator account')}
|
||||
type='account'
|
||||
value={curatorId.toString()}
|
||||
withLabel
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('Only curator can extend the bounty time.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<Input
|
||||
autoFocus
|
||||
defaultValue={''}
|
||||
help={t<string>('The note on the extension.')}
|
||||
label={t<string>('bounty remark')}
|
||||
onChange={onRemarkChange}
|
||||
value={remark}
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t<string>("The note that will be added to the transaction. It won't be stored on chain")}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={toggleOpen}>
|
||||
<TxButton
|
||||
accountId={curatorId}
|
||||
icon='check'
|
||||
label={t<string>('Accept')}
|
||||
onStart={toggleOpen}
|
||||
params={[index, remark]}
|
||||
tx={extendBountyExpiry}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
export default React.memo(ExtendBountyExpiryAction);
|
||||
@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
import type { Balance } from '@polkadot/types/interfaces';
|
||||
import type { Balance, BountyIndex } from '@polkadot/types/interfaces';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
@ -18,7 +18,7 @@ import { useTranslation } from './translate';
|
||||
|
||||
interface Props {
|
||||
description: string
|
||||
index: number;
|
||||
index: BountyIndex;
|
||||
proposals?: DeriveCollectiveProposal[];
|
||||
value: Balance;
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export function truncateTitle (str: string, maxLength: number) {
|
||||
export function truncateTitle (str: string, maxLength: number): string {
|
||||
const ellipsis = String.fromCharCode(8230);
|
||||
|
||||
return (str.length > maxLength) ? str.substr(0, maxLength - 1) + ellipsis : str;
|
||||
|
||||
@ -18,6 +18,7 @@ export type BountyApi = {
|
||||
bountyValueMinimum: BN,
|
||||
closeBounty: ((...args: any[]) => SubmittableExtrinsic<'promise'>);
|
||||
dataDepositPerByte: BN,
|
||||
extendBountyExpiry: ((...args: any[]) => SubmittableExtrinsic<'promise'>);
|
||||
maximumReasonLength: number,
|
||||
proposeBounty: ((...args: any[]) => SubmittableExtrinsic<'promise'>);
|
||||
proposeCurator: ((...args: any[]) => SubmittableExtrinsic<'promise'>);
|
||||
@ -37,6 +38,7 @@ export function useBounties (): BountyApi {
|
||||
const proposeCurator = (api.tx.bounties || api.tx.treasury).proposeCurator;
|
||||
const approveBounty = (api.tx.bounties || api.tx.treasury).approveBounty;
|
||||
const closeBounty = (api.tx.bounties || api.tx.treasury).closeBounty;
|
||||
const extendBountyExpiry = (api.tx.bounties || api.tx.treasury).extendBountyExpiry;
|
||||
|
||||
return {
|
||||
approveBounty,
|
||||
@ -47,6 +49,7 @@ export function useBounties (): BountyApi {
|
||||
bountyValueMinimum,
|
||||
closeBounty,
|
||||
dataDepositPerByte,
|
||||
extendBountyExpiry,
|
||||
maximumReasonLength,
|
||||
proposeBounty,
|
||||
proposeCurator
|
||||
|
||||
Loading…
Reference in New Issue
Block a user