Bounties - Hide bounty unhappy path actions (#4500)
This commit is contained in:
parent
2a26df1419
commit
ccf44f650b
@ -40,18 +40,21 @@
|
||||
"Nay: {{count}}": "Nay: {{count}}",
|
||||
"No open bounties": "No open bounties",
|
||||
"Only curator can extend the bounty time.": "Only curator can extend the bounty time.",
|
||||
"Only the account proposed as curator by the council can create the unassign curator transaction ": "Only the account proposed as curator by the council can create the unassign curator transaction ",
|
||||
"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",
|
||||
"Proposed Curator": "Proposed Curator",
|
||||
"Proposer": "Proposer",
|
||||
"Proposer bond depends on bounty title length.": "Proposer bond depends on bounty title length.",
|
||||
"Reject": "Reject",
|
||||
"Reject Curator": "Reject Curator",
|
||||
"Rejection under voting": "Rejection under voting",
|
||||
"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.",
|
||||
"Select the council account you wish to use to create a proposal for closing bounty.": "Select the council account you wish to use to create a proposal for closing bounty.",
|
||||
"Slash Curator": "Slash Curator",
|
||||
"Slash Curator (Council)": "Slash Curator (Council)",
|
||||
"Slash curator": "Slash curator",
|
||||
"The Curator account that will be used to send this transaction. Any applicable fees will be paid by this account.": "The Curator account that will be used to send this transaction. Any applicable fees will be paid by this account.",
|
||||
"The Curator account that will give up on it's role": "The Curator account that will give up on it's role",
|
||||
@ -68,6 +71,7 @@
|
||||
"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 be used to create the unassign curator transaction.": "This account will be used to create the unassign curator 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 Slash the Curator.": "This action will Slash the Curator.",
|
||||
"This action will award the Beneficiary and close the bounty after a delay.": "This action will award the Beneficiary and close the bounty after a delay.",
|
||||
@ -77,6 +81,7 @@
|
||||
"This action will create a Council motion to unassign the Curator.": "This action will create a Council motion to unassign the Curator.",
|
||||
"This action will create a Council proposal to close the Bounty.": "This action will create a Council proposal to close the Bounty.",
|
||||
"This action will extend expiry time of the selected bounty.": "This action will extend expiry time of the selected bounty.",
|
||||
"This action will reject your candidacy for the curator of the bounty.": "This action will reject your candidacy for the curator of the bounty.",
|
||||
"This action will unassign you from a curator role.": "This action will unassign you from a curator role.",
|
||||
"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.",
|
||||
"This bounty has received approval and is currently being implemented.": "This bounty has received approval and is currently being implemented.",
|
||||
@ -87,8 +92,11 @@
|
||||
"Unassign Curator": "Unassign Curator",
|
||||
"Unassign curator": "Unassign curator",
|
||||
"Unassign curator under voting": "Unassign curator under voting",
|
||||
"Value": "Value",
|
||||
"You are giving up your curator role, the bounty will return to the Funded state. You will get your deposit back.": "You are giving up your curator role, the bounty will return to the Funded state. You will get your deposit back.",
|
||||
"active": "active",
|
||||
"beneficiary": "beneficiary",
|
||||
"bounties": "bounties",
|
||||
"award bounty": "award bounty",
|
||||
"award with account": "award with account",
|
||||
"beneficiary account": "beneficiary account",
|
||||
@ -105,14 +113,17 @@
|
||||
"next bounty funding in": "next bounty funding in",
|
||||
"next burn": "next burn",
|
||||
"past": "past",
|
||||
"payout due": "payout due",
|
||||
"propose with account": "propose with account",
|
||||
"proposing account": "proposing account",
|
||||
"reject curator": "reject curator",
|
||||
"select curator": "select curator",
|
||||
"spend period": "spend period",
|
||||
"status": "status",
|
||||
"submit with account": "submit with account",
|
||||
"title": "title",
|
||||
"treasury": "treasury",
|
||||
"update due": "update due",
|
||||
"value": "value",
|
||||
"vote with account": "vote with account",
|
||||
"{{blocks}} blocks": "{{blocks}} blocks"
|
||||
|
||||
@ -95,6 +95,7 @@
|
||||
"Settings": "",
|
||||
"Show address on hardware device": "",
|
||||
"Slash Curator": "",
|
||||
"Slash Curator (Council)": "",
|
||||
"Slash curator": "",
|
||||
"Slashed": "",
|
||||
"Specified payment account": "",
|
||||
|
||||
@ -27,7 +27,16 @@ import { TypeRegistry } from '@polkadot/types/create';
|
||||
import { keyring } from '@polkadot/ui-keyring';
|
||||
import { extractTime } from '@polkadot/util';
|
||||
|
||||
import { alice, bob, defaultBalance, defaultBountyApi, defaultMembers, defaultTreasury, ferdie } from '../test/hooks/defaults';
|
||||
import { alice,
|
||||
bob,
|
||||
defaultBalance,
|
||||
defaultBountyApi,
|
||||
defaultMembers,
|
||||
defaultTreasury,
|
||||
ferdie } from '../test/hooks/defaults';
|
||||
import { clickButtonWithName } from '../test/utils/clickButtonWithName';
|
||||
import { clickElementWithTestId } from '../test/utils/clickElementWithTestId';
|
||||
import { clickElementWithText } from '../test/utils/clickElementWithText';
|
||||
import Bounties from './Bounties';
|
||||
import { BountyApi } from './hooks';
|
||||
|
||||
@ -35,10 +44,14 @@ const mockMembers = defaultMembers;
|
||||
const mockTreasury = defaultTreasury;
|
||||
let mockBountyApi = defaultBountyApi;
|
||||
let mockBalance = defaultBalance;
|
||||
|
||||
const mockBlockTime = [50, '', extractTime(1)];
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
useBalance: () => mockBalance,
|
||||
jest.mock('./hooks/useBalance', () => ({
|
||||
useBalance: () => mockBalance
|
||||
}));
|
||||
|
||||
jest.mock('./hooks/useBounties', () => ({
|
||||
useBounties: () => mockBountyApi
|
||||
}));
|
||||
|
||||
@ -80,7 +93,7 @@ let aBountyStatus: (status: string) => BountyStatus;
|
||||
let aBountyIndex: (index?:number) => BountyIndex;
|
||||
let aBounty: ({ status, value }?: Partial<Bounty>) => Bounty;
|
||||
let bountyWith: ({ status, value }: { status?: string, value?: number }) => Bounty;
|
||||
let bountyStatusWith: ({ curator, status }: { curator?: string, status?: string, }) => BountyStatus;
|
||||
let bountyStatusWith: ({ curator, status, updateDue }: { curator?: string, status?: string, updateDue?: number}) => BountyStatus;
|
||||
|
||||
describe('Bounties', () => {
|
||||
beforeAll(async () => {
|
||||
@ -215,14 +228,13 @@ describe('Bounties', () => {
|
||||
});
|
||||
|
||||
it('validates balance is enough for bond', async () => {
|
||||
const { findByTestId, findByText, queryByText } = renderBounties(
|
||||
const { findByRole, findByTestId, findByText, queryByText } = renderBounties(
|
||||
{ bountyDepositBase: new BN(10), dataDepositPerByte: new BN(1) },
|
||||
{ balance: 10 }
|
||||
);
|
||||
|
||||
const addBountyButton = await findByText('Add Bounty');
|
||||
await clickButtonWithName('Add Bounty', findByRole);
|
||||
|
||||
fireEvent.click(addBountyButton);
|
||||
expect(await findByText('Description of the Bounty (to be stored on-chain)')).toBeTruthy(); // wait for load
|
||||
|
||||
expect(queryByText('Account does not have enough funds.')).toBeFalsy();
|
||||
@ -301,11 +313,12 @@ describe('Bounties', () => {
|
||||
describe('close bounty modal', () => {
|
||||
it('creates closeBounty proposal', async () => {
|
||||
const bounty = bountyWith({ status: 'Funded' });
|
||||
const { findByText, getByRole } = renderOneBounty(bounty);
|
||||
const { findByTestId, findByText, getByRole } = renderOneBounty(bounty);
|
||||
|
||||
const closeButton = await findByText('Close');
|
||||
await clickElementWithTestId('extra-actions', findByTestId);
|
||||
|
||||
await clickElementWithText('Close', findByText);
|
||||
|
||||
fireEvent.click(closeButton);
|
||||
expect(await findByText('This action will create a Council proposal to close the Bounty.')).toBeTruthy();
|
||||
|
||||
const combobox = getByRole('combobox');
|
||||
@ -323,9 +336,36 @@ describe('Bounties', () => {
|
||||
const bounty = bountyWith({ status: 'Funded' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.closeBounty(0))];
|
||||
|
||||
const { findByText } = renderOneBounty(bounty, proposals);
|
||||
const { findByTestId } = renderOneBounty(bounty, proposals);
|
||||
|
||||
await expect(findByText('Close')).rejects.toThrow();
|
||||
await expect(findByTestId('extra-actions')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reject curator modal', () => {
|
||||
it('creates extrinsic', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({ curator: bob, status: 'CuratorProposed' }) });
|
||||
|
||||
const { findByRole, findByTestId, findByText } = renderOneBounty(bounty);
|
||||
|
||||
await clickElementWithTestId('extra-actions', findByTestId);
|
||||
|
||||
await clickElementWithText('Reject Curator', findByText);
|
||||
|
||||
await clickButtonWithName('Reject', findByRole);
|
||||
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: bob }));
|
||||
});
|
||||
|
||||
it('shows options for all roles', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({ curator: bob, status: 'Active' }) });
|
||||
|
||||
const { findByTestId, findByText } = renderOneBounty(bounty);
|
||||
|
||||
await clickElementWithTestId('extra-actions', findByTestId);
|
||||
|
||||
expect(await findByText('Give Up')).toBeTruthy();
|
||||
expect(await findByText('Slash Curator (Council)')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@ -333,11 +373,11 @@ describe('Bounties', () => {
|
||||
it('queues extend bounty expiry extrinsic on submit', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({ curator: alice }) });
|
||||
|
||||
const { findByTestId, findByText } = renderOneBounty(bounty);
|
||||
const { findByRole, findByTestId, findByText } = renderOneBounty(bounty);
|
||||
|
||||
const extendBountyExpiryButton = await findByText('Extend Expiry');
|
||||
await clickElementWithTestId('extra-actions', findByTestId);
|
||||
|
||||
fireEvent.click(extendBountyExpiryButton);
|
||||
await clickElementWithText('Extend Expiry', findByText);
|
||||
|
||||
expect(await findByText('This action will extend expiry time of the selected bounty.')).toBeTruthy();
|
||||
|
||||
@ -345,9 +385,8 @@ describe('Bounties', () => {
|
||||
|
||||
fireEvent.change(remarkInput, { target: { value: 'The bounty extend expiry remark' } });
|
||||
|
||||
const acceptButton = await findByText('Accept');
|
||||
await clickButtonWithName('Accept', findByRole);
|
||||
|
||||
fireEvent.click(acceptButton);
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: alice, extrinsic: 'mockExtendExtrinsic' }));
|
||||
expect(mockBountyApi.extendBountyExpiry).toHaveBeenCalledWith(aBountyIndex(0), 'The bounty extend expiry remark');
|
||||
});
|
||||
@ -356,11 +395,11 @@ describe('Bounties', () => {
|
||||
describe('slash curator action modal', () => {
|
||||
it('give up on a Curator function in Active state bounty', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({ curator: alice }) });
|
||||
const { findByText, getAllByRole } = renderOneBounty(bounty);
|
||||
const { findByRole, findByTestId, findByText, getAllByRole } = renderOneBounty(bounty);
|
||||
|
||||
const slashCuratorButton = await findByText('Give Up');
|
||||
await clickElementWithTestId('extra-actions', findByTestId);
|
||||
|
||||
fireEvent.click(slashCuratorButton);
|
||||
await clickElementWithText('Give Up', findByText);
|
||||
|
||||
expect(await findByText('This action will unassign you from a curator role.')).toBeTruthy();
|
||||
|
||||
@ -370,9 +409,7 @@ describe('Bounties', () => {
|
||||
|
||||
fireEvent.change(proposingAccountInput, { target: { value: alice } });
|
||||
|
||||
const acceptButton = await findByText('Approve');
|
||||
|
||||
fireEvent.click(acceptButton);
|
||||
await clickButtonWithName('Approve', findByRole);
|
||||
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: alice, extrinsic: 'mockUnassignExtrinsic' }));
|
||||
expect(mockBountyApi.unassignCurator).toHaveBeenCalledWith(aBountyIndex(0));
|
||||
@ -380,11 +417,12 @@ describe('Bounties', () => {
|
||||
|
||||
it('for bounty in PendingPayout state', async () => {
|
||||
const bounty = bountyWith({ status: 'PendingPayout' });
|
||||
const { findByText, getAllByRole } = renderOneBounty(bounty);
|
||||
|
||||
const slashCuratorButton = await findByText('Slash Curator');
|
||||
const { findByRole, findByTestId, findByText, getAllByRole } = renderOneBounty(bounty);
|
||||
|
||||
fireEvent.click(slashCuratorButton);
|
||||
await clickElementWithTestId('extra-actions', findByTestId);
|
||||
|
||||
await clickElementWithText('Slash Curator (Council)', findByText);
|
||||
|
||||
expect(await findByText('This action will create a Council motion to slash the Curator.')).toBeTruthy();
|
||||
|
||||
@ -393,10 +431,9 @@ describe('Bounties', () => {
|
||||
const proposingAccountInput = comboboxes[0].children[0];
|
||||
|
||||
fireEvent.change(proposingAccountInput, { target: { value: alice } });
|
||||
fireEvent.keyDown(proposingAccountInput, { code: 'Enter', key: 'Enter' });
|
||||
|
||||
const acceptButton = await findByText('Approve');
|
||||
|
||||
fireEvent.click(acceptButton);
|
||||
await clickButtonWithName('Approve', findByRole);
|
||||
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: alice, extrinsic: 'mockProposeExtrinsic' }));
|
||||
});
|
||||
@ -581,7 +618,7 @@ describe('Bounties', () => {
|
||||
describe('Beneficiary description', () => {
|
||||
it('PendingPayout status', async () => {
|
||||
const bounty = bountyWith({ status: 'PendingPayout' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.awardBounty(0, '5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z'))];
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.awardBounty(0, alice))];
|
||||
|
||||
const { findByText } = renderOneBounty(bounty, proposals);
|
||||
|
||||
|
||||
@ -16,10 +16,11 @@ import { formatNumber } from '@polkadot/util';
|
||||
import { getProposalToDisplay } from './helpers/extendedStatuses';
|
||||
import VotingResultsColumn from './Voting/VotersColumn';
|
||||
import { BountyActions } from './BountyActions';
|
||||
import BountyExtraActions from './BountyExtraActions';
|
||||
import BountyInfos from './BountyInfos';
|
||||
import BountyStatusView from './BountyStatusView';
|
||||
import Curator from './Curator';
|
||||
import { getBountyStatus } from './helpers';
|
||||
import { useBountyStatus } from './hooks';
|
||||
import { useTranslation } from './translate';
|
||||
|
||||
interface Props {
|
||||
@ -43,10 +44,7 @@ function Bounty ({ bestNumber, bounty, className = '', description, index, propo
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const { bond, curatorDeposit, fee, proposer, status, value } = bounty;
|
||||
|
||||
const updateStatus = useCallback(() => getBountyStatus(status), [status]);
|
||||
|
||||
const { beneficiary, bountyStatus, curator, unlockAt, updateDue } = updateStatus();
|
||||
const { beneficiary, bountyStatus, curator, unlockAt, updateDue } = useBountyStatus(status);
|
||||
|
||||
const blocksUntilUpdate = useMemo(() => updateDue?.sub(bestNumber), [bestNumber, updateDue]);
|
||||
const blocksUntilPayout = useMemo(() => unlockAt?.sub(bestNumber), [bestNumber, unlockAt]);
|
||||
@ -137,6 +135,13 @@ function Bounty ({ bestNumber, bounty, className = '', description, index, propo
|
||||
isLogo
|
||||
type='bounty'
|
||||
/>
|
||||
<BountyExtraActions
|
||||
bestNumber={bestNumber}
|
||||
description={description}
|
||||
index={index}
|
||||
proposals={proposals}
|
||||
status={status}
|
||||
/>
|
||||
<div className='table-column-icon'
|
||||
onClick={handleOnIconClick}>
|
||||
<Icon icon={
|
||||
@ -249,28 +254,6 @@ export default React.memo(styled(Bounty)(({ theme }: ThemeProps) => `
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
svg {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: #fff;
|
||||
border: 1px solid #616161;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .inline-balance {
|
||||
|
||||
@ -8,24 +8,24 @@ import React, { useMemo } from 'react';
|
||||
import { TxButton } from '@polkadot/react-components';
|
||||
import { useAccounts } from '@polkadot/react-hooks';
|
||||
|
||||
import { useBounties } from '../hooks';
|
||||
import { useTranslation } from '../translate';
|
||||
import { useBounties } from './hooks';
|
||||
import { useTranslation } from './translate';
|
||||
|
||||
interface Props {
|
||||
curatorId: AccountId;
|
||||
index: BountyIndex;
|
||||
}
|
||||
|
||||
function BountyCuratorProposedActions ({ curatorId, index }: Props) {
|
||||
function BountyAcceptCurator ({ curatorId, index }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { acceptCurator, unassignCurator } = useBounties();
|
||||
const { acceptCurator } = useBounties();
|
||||
const { allAccounts } = useAccounts();
|
||||
|
||||
const isCurator = useMemo(() => allAccounts.includes(curatorId.toString()), [allAccounts, curatorId]);
|
||||
|
||||
return isCurator
|
||||
? (
|
||||
<>
|
||||
return (
|
||||
<>
|
||||
{isCurator &&
|
||||
<TxButton
|
||||
accountId={curatorId}
|
||||
icon='check'
|
||||
@ -33,16 +33,9 @@ function BountyCuratorProposedActions ({ curatorId, index }: Props) {
|
||||
params={[index]}
|
||||
tx={acceptCurator}
|
||||
/>
|
||||
<TxButton
|
||||
accountId={curatorId}
|
||||
icon='times'
|
||||
label={t<string>('Reject')}
|
||||
params={[index]}
|
||||
tx={unassignCurator}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
: null;
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(BountyCuratorProposedActions);
|
||||
export default React.memo(BountyAcceptCurator);
|
||||
@ -1,30 +1,28 @@
|
||||
// Copyright 2017-2021 @polkadot/app-treasury authors & contributors
|
||||
// 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';
|
||||
import React, { useEffect, 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 { InputAddress, Modal, TxButton } from '@polkadot/react-components';
|
||||
import { useApi, useMembers } from '@polkadot/react-hooks';
|
||||
|
||||
import { useBounties } from '../hooks';
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
index: BountyIndex;
|
||||
proposals?: DeriveCollectiveProposal[];
|
||||
toggleOpen: () => void;
|
||||
}
|
||||
|
||||
function CloseBounty ({ index, proposals }: Props): React.ReactElement<Props> | null {
|
||||
function CloseBounty ({ index, toggleOpen }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { isMember, members } = useMembers();
|
||||
const { members } = useMembers();
|
||||
const { closeBounty } = useBounties();
|
||||
const [isOpen, toggleOpen] = useToggle();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [threshold, setThreshold] = useState<BN>();
|
||||
|
||||
@ -36,58 +34,44 @@ function CloseBounty ({ index, proposals }: Props): React.ReactElement<Props> |
|
||||
|
||||
const closeBountyProposal = useRef(closeBounty(index));
|
||||
|
||||
const existingCloseBountyProposal = useMemo(() => proposals?.find(({ proposal }) => proposal.method === 'closeBounty'), [proposals]);
|
||||
|
||||
return isMember && !existingCloseBountyProposal
|
||||
? (
|
||||
<>
|
||||
<Button
|
||||
icon='step-forward'
|
||||
return (
|
||||
<Modal
|
||||
header={t<string>('close bounty')}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('This action will create a Council proposal to close the Bounty.')}</p>
|
||||
</Modal.Column>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
filter={members}
|
||||
help={t<string>('Select the council account you wish to use to create a proposal for closing bounty.')}
|
||||
label={t<string>('propose with account')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
withLabel
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('The council member that will create the close bounty proposal, submission equates to an "aye" vote.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={toggleOpen}>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='ban'
|
||||
isDisabled={false}
|
||||
label={t<string>('Close')}
|
||||
onClick={toggleOpen}
|
||||
label={t<string>('Close Bounty')}
|
||||
onStart={toggleOpen}
|
||||
params={[threshold, closeBountyProposal.current, closeBountyProposal.current.length]}
|
||||
tx={api.tx.council.propose}
|
||||
/>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
header={t<string>('close bounty')}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('This action will create a Council proposal to close the Bounty.')}</p>
|
||||
</Modal.Column>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
filter={members}
|
||||
help={t<string>('Select the council account you wish to use to create a proposal for closing bounty.')}
|
||||
label={t<string>('propose with account')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
withLabel
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('The council member that will create the close bounty proposal, submission equates to an "aye" vote.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={toggleOpen}>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='ban'
|
||||
isDisabled={false}
|
||||
label={t<string>('Close Bounty')}
|
||||
onStart={toggleOpen}
|
||||
params={[threshold, closeBountyProposal.current, closeBountyProposal.current.length]}
|
||||
tx={api.tx.council.propose}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
: null;
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(CloseBounty);
|
||||
|
||||
@ -3,97 +3,84 @@
|
||||
|
||||
import type { AccountId, BountyIndex } from '@polkadot/types/interfaces';
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { Button, Input, InputAddress, Modal, TxButton } from '@polkadot/react-components';
|
||||
import { useAccounts, useToggle } from '@polkadot/react-hooks';
|
||||
import { Input, InputAddress, Modal, TxButton } from '@polkadot/react-components';
|
||||
|
||||
import { truncateTitle } from '../helpers';
|
||||
import { useBounties } from '../hooks';
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
curatorId: AccountId;
|
||||
description: string
|
||||
index: BountyIndex;
|
||||
curatorId: AccountId;
|
||||
toggleOpen: () => void;
|
||||
}
|
||||
|
||||
function ExtendBountyExpiryAction ({ curatorId, description, index }: Props): React.ReactElement<Props> | null {
|
||||
function ExtendBountyExpiryAction ({ curatorId, description, index, toggleOpen }: 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
|
||||
defaultValue={curatorId.toString()}
|
||||
help={t<string>('This account will be used to create an extend bounty expire transaction.')}
|
||||
isDisabled
|
||||
label={t<string>('curator account')}
|
||||
type='account'
|
||||
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}
|
||||
return (
|
||||
<>
|
||||
<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.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
: null;
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ExtendBountyExpiryAction);
|
||||
|
||||
@ -1,34 +1,30 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
import type { AccountId, BountyIndex, BountyStatus } from '@polkadot/types/interfaces';
|
||||
import type { AccountId, BountyIndex } from '@polkadot/types/interfaces';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { SubmittableExtrinsic } from '@polkadot/api/types';
|
||||
import { getTreasuryProposalThreshold } from '@polkadot/apps-config';
|
||||
import { Button, InputAddress, Modal, TxButton } from '@polkadot/react-components';
|
||||
import { useAccounts, useApi, useMembers, useToggle } from '@polkadot/react-hooks';
|
||||
import { InputAddress, Modal, TxButton } from '@polkadot/react-components';
|
||||
import { useAccounts, useApi, useMembers } from '@polkadot/react-hooks';
|
||||
|
||||
import { determineUnassignCuratorAction, truncateTitle } from '../helpers';
|
||||
import { truncateTitle } from '../helpers';
|
||||
import { useBounties } from '../hooks';
|
||||
import { useUserRole } from '../hooks/useUserRole';
|
||||
import { useTranslation } from '../translate';
|
||||
import { ValidUnassignCuratorAction } from '../types';
|
||||
|
||||
interface Props {
|
||||
blocksUntilUpdate?: BN;
|
||||
action: ValidUnassignCuratorAction;
|
||||
curatorId: AccountId;
|
||||
description: string;
|
||||
index: BountyIndex;
|
||||
proposals?: DeriveCollectiveProposal[];
|
||||
status: BountyStatus;
|
||||
toggleOpen: () => void;
|
||||
}
|
||||
|
||||
interface ActionProperties {
|
||||
buttonName: string;
|
||||
filter: string[];
|
||||
header: string;
|
||||
helpMessage: string;
|
||||
@ -39,14 +35,11 @@ interface ActionProperties {
|
||||
tx: ((...args: any[]) => SubmittableExtrinsic<'promise'>);
|
||||
}
|
||||
|
||||
const BOUNTY_METHODS = ['unassignCurator'];
|
||||
|
||||
function SlashCurator ({ blocksUntilUpdate, curatorId, description, index, proposals, status }: Props): React.ReactElement<Props> | null {
|
||||
function SlashCurator ({ action, curatorId, description, index, toggleOpen }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { members } = useMembers();
|
||||
const { unassignCurator } = useBounties();
|
||||
const [isOpen, toggleOpen] = useToggle();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [threshold, setThreshold] = useState<BN>();
|
||||
const { allAccounts } = useAccounts();
|
||||
@ -61,7 +54,6 @@ function SlashCurator ({ blocksUntilUpdate, curatorId, description, index, propo
|
||||
|
||||
const actionProperties = useMemo<Record<ValidUnassignCuratorAction, ActionProperties>>(() => ({
|
||||
GiveUp: {
|
||||
buttonName: t('Give Up'),
|
||||
filter: [curatorId.toString()],
|
||||
header: t('This action will unassign you from a curator role.'),
|
||||
helpMessage: t("The Curator account that will give up on it's role"),
|
||||
@ -72,7 +64,6 @@ function SlashCurator ({ blocksUntilUpdate, curatorId, description, index, propo
|
||||
tx: unassignCurator
|
||||
},
|
||||
SlashCuratorAction: {
|
||||
buttonName: t('Slash Curator'),
|
||||
filter: allAccounts,
|
||||
header: t('This action will Slash the Curator.'),
|
||||
helpMessage: t('The Curator that will be slashed.'),
|
||||
@ -83,7 +74,6 @@ function SlashCurator ({ blocksUntilUpdate, curatorId, description, index, propo
|
||||
tx: unassignCurator
|
||||
},
|
||||
SlashCuratorMotion: {
|
||||
buttonName: t('Slash Curator'),
|
||||
filter: members,
|
||||
header: t('This action will create a Council motion to slash the Curator.'),
|
||||
helpMessage: t('The Curator that will be slashed.'),
|
||||
@ -94,7 +84,6 @@ function SlashCurator ({ blocksUntilUpdate, curatorId, description, index, propo
|
||||
tx: api.tx.council.propose
|
||||
},
|
||||
UnassignCurator: {
|
||||
buttonName: t('Unassign Curator'),
|
||||
filter: members,
|
||||
header: t('This action will create a Council motion to unassign the Curator.'),
|
||||
helpMessage: t('The Curator that will be unassigned'),
|
||||
@ -106,81 +95,60 @@ function SlashCurator ({ blocksUntilUpdate, curatorId, description, index, propo
|
||||
}
|
||||
}), [t, curatorId, index, unassignCurator, api.tx.council.propose, allAccounts, members, threshold, unassignCuratorProposal]);
|
||||
|
||||
const isVotingInitiated = useMemo(() => proposals?.filter(({ proposal }) => BOUNTY_METHODS.includes(proposal.method)).length !== 0, [proposals]);
|
||||
const { filter, header, helpMessage, params, proposingAccountTip, tip, title, tx } = actionProperties[action];
|
||||
|
||||
const userRole = useUserRole(curatorId);
|
||||
|
||||
const action = determineUnassignCuratorAction(userRole, status, blocksUntilUpdate);
|
||||
|
||||
if (action === 'None') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { buttonName, filter, header, helpMessage, params, proposingAccountTip, tip, title, tx } = actionProperties[action];
|
||||
|
||||
return !isVotingInitiated
|
||||
? (
|
||||
<>
|
||||
<Button
|
||||
icon='user-slash'
|
||||
label={buttonName}
|
||||
onClick={toggleOpen}
|
||||
return (
|
||||
<Modal
|
||||
header={`${title} - "${truncateTitle(description, 30)}"`}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Column>
|
||||
<p>{header}</p>
|
||||
</Modal.Column>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
filter={filter}
|
||||
help={t<string>('The account that will sign the transaction.')}
|
||||
label={t<string>('proposing account')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
withLabel
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{proposingAccountTip}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
defaultValue={curatorId}
|
||||
help={helpMessage}
|
||||
isDisabled
|
||||
label={t<string>('current curator')}
|
||||
type='account'
|
||||
withLabel
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{tip}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={toggleOpen}>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='check'
|
||||
label='Approve'
|
||||
onStart={toggleOpen}
|
||||
params={params}
|
||||
tx={tx}
|
||||
/>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
header={`${title} - "${truncateTitle(description, 30)}"`}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Column>
|
||||
<p>{header}</p>
|
||||
</Modal.Column>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
filter={filter}
|
||||
help={t<string>('The account that will sign the transaction.')}
|
||||
label={t<string>('proposing account')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
withLabel
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{proposingAccountTip}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
defaultValue={curatorId}
|
||||
help={helpMessage}
|
||||
isDisabled
|
||||
label={t<string>('current curator')}
|
||||
type='account'
|
||||
withLabel
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{tip}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={toggleOpen}>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='check'
|
||||
label='Approve'
|
||||
onStart={toggleOpen}
|
||||
params={params}
|
||||
tx={tx}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
: null;
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(SlashCurator);
|
||||
|
||||
@ -4,17 +4,14 @@
|
||||
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
import type { Balance, BlockNumber, BountyIndex, BountyStatus } from '@polkadot/types/interfaces';
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { getBountyStatus } from '../helpers';
|
||||
import BountyAcceptCurator from '../BountyAcceptCurator';
|
||||
import { useBountyStatus } from '../hooks';
|
||||
import AwardBounty from './AwardBounty';
|
||||
import BountyClaimAction from './BountyClaimAction';
|
||||
import BountyCuratorProposedActions from './BountyCuratorProposedActions';
|
||||
import BountyInitiateVoting from './BountyInitiateVoting';
|
||||
import CloseBounty from './CloseBounty';
|
||||
import ExtendBountyExpiryAction from './ExtendBountyExpiryAction';
|
||||
import ProposeCuratorAction from './ProposeCuratorAction';
|
||||
import SlashCurator from './SlashCurator';
|
||||
|
||||
interface Props {
|
||||
bestNumber: BlockNumber;
|
||||
@ -26,11 +23,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export function BountyActions ({ bestNumber, description, index, proposals, status, value }: Props): JSX.Element {
|
||||
const updateStatus = useCallback(() => getBountyStatus(status), [status]);
|
||||
const { beneficiary, curator, unlockAt } = useBountyStatus(status);
|
||||
|
||||
const { beneficiary, curator, unlockAt, updateDue } = updateStatus();
|
||||
|
||||
const blocksUntilUpdate = useMemo(() => updateDue?.sub(bestNumber), [bestNumber, updateDue]);
|
||||
const blocksUntilPayout = useMemo(() => unlockAt?.sub(bestNumber), [bestNumber, unlockAt]);
|
||||
|
||||
return (
|
||||
@ -50,24 +44,11 @@ export function BountyActions ({ bestNumber, description, index, proposals, stat
|
||||
/>
|
||||
}
|
||||
{status.isCuratorProposed && curator &&
|
||||
<BountyCuratorProposedActions
|
||||
<BountyAcceptCurator
|
||||
curatorId={curator}
|
||||
index={index}
|
||||
/>
|
||||
}
|
||||
{status.isActive && curator &&
|
||||
<ExtendBountyExpiryAction
|
||||
curatorId={curator}
|
||||
description={description}
|
||||
index={index}
|
||||
/>
|
||||
}
|
||||
{(status.isFunded || status.isActive || status.isCuratorProposed) &&
|
||||
<CloseBounty
|
||||
index={index}
|
||||
proposals={proposals}
|
||||
/>
|
||||
}
|
||||
{status.isPendingPayout && beneficiary && blocksUntilPayout &&
|
||||
<BountyClaimAction
|
||||
beneficiaryId={beneficiary}
|
||||
@ -75,21 +56,11 @@ export function BountyActions ({ bestNumber, description, index, proposals, stat
|
||||
payoutDue={blocksUntilPayout}
|
||||
/>
|
||||
}
|
||||
{(status.isCuratorProposed || status.isActive || status.isPendingPayout) && curator && (
|
||||
<SlashCurator
|
||||
blocksUntilUpdate={blocksUntilUpdate}
|
||||
curatorId={curator}
|
||||
description={description}
|
||||
index={index}
|
||||
proposals={proposals}
|
||||
status={status}
|
||||
/>
|
||||
)}
|
||||
{status.isActive && curator &&
|
||||
<AwardBounty
|
||||
curatorId={curator}
|
||||
index={index}
|
||||
/>
|
||||
<AwardBounty
|
||||
curatorId={curator}
|
||||
index={index}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
|
||||
176
packages/page-bounties/src/BountyExtraActions.tsx
Normal file
176
packages/page-bounties/src/BountyExtraActions.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
import { determineUnassignCuratorAction } from '@polkadot/app-bounties/helpers';
|
||||
import { useBountyStatus, useUserRole } from '@polkadot/app-bounties/hooks';
|
||||
import { ValidUnassignCuratorAction } from '@polkadot/app-bounties/types';
|
||||
import { Button, Menu, Popup } from '@polkadot/react-components';
|
||||
import { ThemeProps } from '@polkadot/react-components/types';
|
||||
import { useMembers, useToggle } from '@polkadot/react-hooks';
|
||||
import { BlockNumber, BountyIndex, BountyStatus } from '@polkadot/types/interfaces';
|
||||
|
||||
import CloseBounty from './BountyActions/CloseBounty';
|
||||
import ExtendBountyExpiryAction from './BountyActions/ExtendBountyExpiryAction';
|
||||
import SlashCurator from './BountyActions/SlashCurator';
|
||||
import BountyRejectCurator from './BountyRejectCurator';
|
||||
import { useTranslation } from './translate';
|
||||
|
||||
interface Props {
|
||||
bestNumber: BlockNumber;
|
||||
className?: string;
|
||||
description: string;
|
||||
index: BountyIndex;
|
||||
proposals?: DeriveCollectiveProposal[];
|
||||
status: BountyStatus;
|
||||
}
|
||||
|
||||
function BountyExtraActions ({ bestNumber, className, description, index, proposals, status }: Props): React.ReactElement<Props> | null {
|
||||
const [isPopupOpen, togglePopupOpen] = useToggle();
|
||||
const [isCloseBountyOpen, toggleCloseBounty] = useToggle();
|
||||
const [isRejectCuratorOpen, toggleRejectCurator] = useToggle();
|
||||
const [isSlashCuratorOpen, toggleSlashCurator] = useToggle();
|
||||
const [isExtendExpiryOpen, toggleExtendExpiry] = useToggle();
|
||||
const [selectedAction, setSlashAction] = useState<ValidUnassignCuratorAction>();
|
||||
const { t } = useTranslation();
|
||||
const { isMember } = useMembers();
|
||||
const { curator, updateDue } = useBountyStatus(status);
|
||||
|
||||
const blocksUntilUpdate = useMemo(() => updateDue?.sub(bestNumber), [bestNumber, updateDue]);
|
||||
|
||||
const { isCurator, roles } = useUserRole(curator);
|
||||
const availableSlashActions = determineUnassignCuratorAction(roles, status, blocksUntilUpdate);
|
||||
|
||||
const slashCuratorActionNames = useRef<Record<ValidUnassignCuratorAction, string>>({
|
||||
GiveUp: t('Give Up'),
|
||||
SlashCuratorAction: t('Slash Curator'),
|
||||
SlashCuratorMotion: t('Slash Curator (Council)'),
|
||||
UnassignCurator: t('Unassign Curator')
|
||||
});
|
||||
|
||||
const existingCloseBountyProposal = useMemo(() => proposals?.find(({ proposal }) => proposal.method === 'closeBounty'), [proposals]);
|
||||
const existingUnassignCuratorProposal = useMemo(() => proposals?.find(({ proposal }) => proposal.method === 'unassignCurator'), [proposals]);
|
||||
|
||||
const showCloseBounty = (status.isFunded || status.isActive || status.isCuratorProposed) && isMember && !existingCloseBountyProposal;
|
||||
const showRejectCurator = status.isCuratorProposed && isCurator;
|
||||
const showExtendExpiry = status.isActive && isCurator;
|
||||
const showSlashCurator = (status.isCuratorProposed || status.isActive || status.isPendingPayout) && !existingUnassignCuratorProposal && availableSlashActions.length !== 0;
|
||||
|
||||
const hasNoItems = !(showCloseBounty || showRejectCurator || showExtendExpiry || showSlashCurator);
|
||||
|
||||
function slashCuratorClicked (action: ValidUnassignCuratorAction) {
|
||||
setSlashAction(action);
|
||||
toggleSlashCurator();
|
||||
}
|
||||
|
||||
return !hasNoItems
|
||||
? (
|
||||
<div className={className}>
|
||||
{isCloseBountyOpen &&
|
||||
<CloseBounty
|
||||
index={index}
|
||||
toggleOpen={toggleCloseBounty}
|
||||
/>
|
||||
}
|
||||
{isRejectCuratorOpen && curator &&
|
||||
<BountyRejectCurator
|
||||
curatorId={curator}
|
||||
index={index}
|
||||
toggleOpen={toggleRejectCurator}
|
||||
/>
|
||||
}
|
||||
{isExtendExpiryOpen && curator &&
|
||||
<ExtendBountyExpiryAction
|
||||
curatorId={curator}
|
||||
description={description}
|
||||
index={index}
|
||||
toggleOpen={toggleExtendExpiry}
|
||||
/>
|
||||
}
|
||||
{isSlashCuratorOpen && curator && selectedAction &&
|
||||
<SlashCurator
|
||||
action={selectedAction}
|
||||
curatorId={curator}
|
||||
description={description}
|
||||
index={index}
|
||||
toggleOpen={toggleSlashCurator}
|
||||
/>
|
||||
}
|
||||
<Popup
|
||||
isOpen={isPopupOpen}
|
||||
onClose={togglePopupOpen}
|
||||
trigger={
|
||||
<Button
|
||||
className='settings-button'
|
||||
dataTestId='extra-actions'
|
||||
icon='ellipsis-v'
|
||||
onClick={togglePopupOpen}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Menu
|
||||
className='settings-menu'
|
||||
onClick={togglePopupOpen}
|
||||
text
|
||||
vertical
|
||||
>
|
||||
{showCloseBounty &&
|
||||
<Menu.Item
|
||||
key='closeBounty'
|
||||
onClick={toggleCloseBounty}
|
||||
>
|
||||
{t<string>('Close')}
|
||||
</Menu.Item>
|
||||
}
|
||||
{showRejectCurator &&
|
||||
<Menu.Item
|
||||
key='rejectCurator'
|
||||
onClick={toggleRejectCurator}
|
||||
>
|
||||
{t<string>('Reject Curator')}
|
||||
</Menu.Item>
|
||||
}
|
||||
{showExtendExpiry &&
|
||||
<Menu.Item
|
||||
key='extendExpiry'
|
||||
onClick={toggleExtendExpiry}
|
||||
>
|
||||
{t<string>('Extend Expiry')}
|
||||
</Menu.Item>
|
||||
}
|
||||
{showSlashCurator && availableSlashActions.map((actionName) =>
|
||||
<Menu.Item
|
||||
key={actionName}
|
||||
onClick={() => slashCuratorClicked(actionName)}
|
||||
>
|
||||
{slashCuratorActionNames.current[actionName]}
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
</Popup>
|
||||
</div>
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
export default React.memo(styled(BountyExtraActions)(({ theme }: ThemeProps) => `
|
||||
.settings-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
svg {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: ${theme.theme === 'dark' ? 'rgba(244,242,240,0.9)' : '#000 !important'};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border: 1px solid #616161;
|
||||
}
|
||||
}
|
||||
`));
|
||||
62
packages/page-bounties/src/BountyRejectCurator.tsx
Normal file
62
packages/page-bounties/src/BountyRejectCurator.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId, BountyIndex } from '@polkadot/types/interfaces';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useBounties } from '@polkadot/app-bounties/hooks';
|
||||
import { InputAddress, Modal, TxButton } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from './translate';
|
||||
|
||||
interface Props {
|
||||
curatorId: AccountId;
|
||||
index: BountyIndex;
|
||||
toggleOpen: () => void;
|
||||
}
|
||||
|
||||
function BountyRejectCurator ({ curatorId, index, toggleOpen }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { unassignCurator } = useBounties();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
header={t<string>('reject curator')}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('This action will reject your candidacy for the curator of the bounty.')}</p>
|
||||
</Modal.Column>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
help={t<string>('This account will be used to create the unassign curator transaction.')}
|
||||
isDisabled
|
||||
label={t<string>('curator account')}
|
||||
type='account'
|
||||
value={curatorId.toString()}
|
||||
withLabel
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('Only the account proposed as curator by the council can create the unassign curator transaction ')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={toggleOpen}>
|
||||
<TxButton
|
||||
accountId={curatorId}
|
||||
icon='times'
|
||||
label={t<string>('Reject')}
|
||||
onStart={toggleOpen}
|
||||
params={[index]}
|
||||
tx={unassignCurator}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(BountyRejectCurator);
|
||||
@ -47,8 +47,8 @@ function VotersColumn ({ className, option, proposals, status }: Props): React.R
|
||||
<div className='vote-numbers'>
|
||||
<span className='vote-numbers-icon'><Icon icon={icons[option]} /></span>
|
||||
<span className='vote-numbers-label'>
|
||||
{option === 'ayes' && t('Aye: {{count}}', { replace: { count: votes && votes.length } })}
|
||||
{option === 'nays' && t('Nay: {{count}}', { replace: { count: votes && votes.length } })}
|
||||
{option === 'ayes' && t('Aye: {{count}}', { replace: { count: votes ? votes.length : 0 } })}
|
||||
{option === 'nays' && t('Nay: {{count}}', { replace: { count: votes ? votes.length : 0 } })}
|
||||
</span>
|
||||
</div>
|
||||
{voters}
|
||||
|
||||
@ -12,50 +12,56 @@ describe('adjust slash curator component for', () => {
|
||||
const { aBountyStatus } = new BountyFactory(augmentedApi);
|
||||
|
||||
it('Member in Active state', () => {
|
||||
const displayAs = determineUnassignCuratorAction('Member', aBountyStatus('Active'));
|
||||
const displayAs = determineUnassignCuratorAction(['Member'], aBountyStatus('Active'));
|
||||
|
||||
expect(displayAs).toEqual('SlashCuratorMotion');
|
||||
expect(displayAs).toEqual(['SlashCuratorMotion']);
|
||||
});
|
||||
|
||||
it('Member in CuratorProposed state', () => {
|
||||
const displayAs = determineUnassignCuratorAction('Member', aBountyStatus('CuratorProposed'));
|
||||
const displayAs = determineUnassignCuratorAction(['Member'], aBountyStatus('CuratorProposed'));
|
||||
|
||||
expect(displayAs).toEqual('UnassignCurator');
|
||||
expect(displayAs).toEqual(['UnassignCurator']);
|
||||
});
|
||||
|
||||
it('Member in PendingPayout state', () => {
|
||||
const displayAs = determineUnassignCuratorAction('Member', aBountyStatus('PendingPayout'));
|
||||
const displayAs = determineUnassignCuratorAction(['Member'], aBountyStatus('PendingPayout'));
|
||||
|
||||
expect(displayAs).toEqual('SlashCuratorMotion');
|
||||
expect(displayAs).toEqual(['SlashCuratorMotion']);
|
||||
});
|
||||
|
||||
it('Curator in Active state', () => {
|
||||
const displayAs = determineUnassignCuratorAction('Curator', aBountyStatus('Active'));
|
||||
const displayAs = determineUnassignCuratorAction(['Curator'], aBountyStatus('Active'));
|
||||
|
||||
expect(displayAs).toEqual('GiveUp');
|
||||
expect(displayAs).toEqual(['GiveUp']);
|
||||
});
|
||||
|
||||
it('User in Active state with update due blocks remaining', () => {
|
||||
const displayAs = determineUnassignCuratorAction('User', aBountyStatus('Active'), new BN('1'));
|
||||
const displayAs = determineUnassignCuratorAction(['User'], aBountyStatus('Active'), new BN('1'));
|
||||
|
||||
expect(displayAs).toEqual('None');
|
||||
expect(displayAs).toEqual([]);
|
||||
});
|
||||
|
||||
it('User in Active state with no updated state', () => {
|
||||
const displayAs = determineUnassignCuratorAction('User', aBountyStatus('Active'), new BN('-1'));
|
||||
const displayAs = determineUnassignCuratorAction(['User'], aBountyStatus('Active'), new BN('-1'));
|
||||
|
||||
expect(displayAs).toEqual('SlashCuratorAction');
|
||||
expect(displayAs).toEqual(['SlashCuratorAction']);
|
||||
});
|
||||
|
||||
it('Member and User in Active state with no updated state', () => {
|
||||
const displayAs = determineUnassignCuratorAction(['User', 'Member'], aBountyStatus('Active'), new BN('-1'));
|
||||
|
||||
expect(displayAs).toEqual(expect.arrayContaining(['SlashCuratorAction', 'SlashCuratorMotion']));
|
||||
});
|
||||
|
||||
it('Curator in Active state', () => {
|
||||
const displayAs = determineUnassignCuratorAction('Curator', aBountyStatus('PendingPayout'));
|
||||
const displayAs = determineUnassignCuratorAction(['Curator'], aBountyStatus('PendingPayout'));
|
||||
|
||||
expect(displayAs).toEqual('None');
|
||||
expect(displayAs).toEqual([]);
|
||||
});
|
||||
|
||||
it('User in Active state', () => {
|
||||
const displayAs = determineUnassignCuratorAction('User', aBountyStatus('PendingPayout'));
|
||||
const displayAs = determineUnassignCuratorAction(['User'], aBountyStatus('PendingPayout'));
|
||||
|
||||
expect(displayAs).toEqual('None');
|
||||
expect(displayAs).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -7,30 +7,32 @@ import BN from 'bn.js';
|
||||
|
||||
import { BN_ZERO } from '@polkadot/util';
|
||||
|
||||
import { UnassignCuratorAction, UserRole } from '../types';
|
||||
import { UserRole, ValidUnassignCuratorAction } from '../types';
|
||||
|
||||
export function determineUnassignCuratorAction (role: UserRole, status: BountyStatus, blocksUntilUpdate?: BN): UnassignCuratorAction {
|
||||
if (status.isCuratorProposed && role === 'Member') {
|
||||
return 'UnassignCurator';
|
||||
export function determineUnassignCuratorAction (roles: UserRole[], status: BountyStatus, blocksUntilUpdate?: BN): ValidUnassignCuratorAction[] {
|
||||
const actions: ValidUnassignCuratorAction[] = [];
|
||||
|
||||
if (status.isCuratorProposed && roles.includes('Member')) {
|
||||
actions.push('UnassignCurator');
|
||||
}
|
||||
|
||||
if (status.isActive) {
|
||||
if (role === 'Curator') {
|
||||
return 'GiveUp';
|
||||
if (roles.includes('Curator')) {
|
||||
actions.push('GiveUp');
|
||||
}
|
||||
|
||||
if (role === 'Member') {
|
||||
return 'SlashCuratorMotion';
|
||||
if (roles.includes('Member')) {
|
||||
actions.push('SlashCuratorMotion');
|
||||
}
|
||||
|
||||
if (role === 'User' && blocksUntilUpdate && blocksUntilUpdate.lt(BN_ZERO)) {
|
||||
return 'SlashCuratorAction';
|
||||
if (roles.includes('User') && blocksUntilUpdate && blocksUntilUpdate.lt(BN_ZERO)) {
|
||||
actions.push('SlashCuratorAction');
|
||||
}
|
||||
}
|
||||
|
||||
if (status.isPendingPayout && role === 'Member') {
|
||||
return 'SlashCuratorMotion';
|
||||
if (status.isPendingPayout && roles.includes('Member')) {
|
||||
actions.push('SlashCuratorMotion');
|
||||
}
|
||||
|
||||
return 'None';
|
||||
return actions;
|
||||
}
|
||||
|
||||
@ -4,3 +4,4 @@
|
||||
export * from './useBounties';
|
||||
export * from './useBalance';
|
||||
export * from './useUserRole';
|
||||
export * from './useBountyStatus';
|
||||
|
||||
14
packages/page-bounties/src/hooks/useBountyStatus.ts
Normal file
14
packages/page-bounties/src/hooks/useBountyStatus.ts
Normal file
@ -0,0 +1,14 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { getBountyStatus } from '@polkadot/app-bounties/helpers';
|
||||
import { BountyStatusType } from '@polkadot/app-bounties/types';
|
||||
import { BountyStatus } from '@polkadot/types/interfaces';
|
||||
|
||||
export function useBountyStatus (status: BountyStatus): BountyStatusType {
|
||||
const updateStatus = useCallback(() => getBountyStatus(status), [status]);
|
||||
|
||||
return updateStatus();
|
||||
}
|
||||
@ -9,24 +9,28 @@ import { useAccounts, useMembers } from '@polkadot/react-hooks';
|
||||
|
||||
import { UserRole } from '../types';
|
||||
|
||||
export function useUserRole (curatorId: AccountId): UserRole {
|
||||
export type UserRolesInfo = { roles: UserRole[], isCurator: boolean };
|
||||
|
||||
export function useUserRole (curatorId?: AccountId): UserRolesInfo {
|
||||
const { allAccounts, hasAccounts } = useAccounts();
|
||||
|
||||
const { isMember } = useMembers();
|
||||
|
||||
const isCurator = useMemo(() => allAccounts.includes(curatorId.toString()), [allAccounts, curatorId]);
|
||||
const isCurator = useMemo(() => curatorId && allAccounts.includes(curatorId.toString()), [allAccounts, curatorId]);
|
||||
|
||||
const roles: UserRole[] = [];
|
||||
|
||||
if (isCurator) {
|
||||
return 'Curator';
|
||||
roles.push('Curator');
|
||||
}
|
||||
|
||||
if (isMember) {
|
||||
return 'Member';
|
||||
roles.push('Member');
|
||||
}
|
||||
|
||||
if (hasAccounts) {
|
||||
return 'User';
|
||||
roles.push('User');
|
||||
}
|
||||
|
||||
return 'None';
|
||||
return { isCurator: !!isCurator, roles };
|
||||
}
|
||||
|
||||
10
packages/page-bounties/test/utils/clickButtonWithName.ts
Normal file
10
packages/page-bounties/test/utils/clickButtonWithName.ts
Normal file
@ -0,0 +1,10 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
|
||||
export const clickButtonWithName = async (buttonName: string, findByRole: (...args : any[]) => Promise<HTMLElement>): Promise<void> => {
|
||||
const button = await findByRole('button', { name: buttonName });
|
||||
|
||||
fireEvent.click(button);
|
||||
};
|
||||
10
packages/page-bounties/test/utils/clickElementWithTestId.ts
Normal file
10
packages/page-bounties/test/utils/clickElementWithTestId.ts
Normal file
@ -0,0 +1,10 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
|
||||
export const clickElementWithTestId = async (testId: string, findByTestId: (testId: string) => Promise<HTMLElement>): Promise<void> => {
|
||||
const element = await findByTestId(testId);
|
||||
|
||||
fireEvent.click(element);
|
||||
};
|
||||
10
packages/page-bounties/test/utils/clickElementWithText.ts
Normal file
10
packages/page-bounties/test/utils/clickElementWithText.ts
Normal file
@ -0,0 +1,10 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
|
||||
export const clickElementWithText = async (text: string, findByText: (name: string) => Promise<HTMLElement>): Promise<void> => {
|
||||
const element = await findByText(text);
|
||||
|
||||
fireEvent.click(element);
|
||||
};
|
||||
@ -10,7 +10,7 @@ import styled from 'styled-components';
|
||||
import Icon from '../Icon';
|
||||
import Spinner from '../Spinner';
|
||||
|
||||
function Button ({ children, className = '', icon, isBasic, isBusy, isCircular, isDisabled, isFull, isIcon, isSelected, isToplevel, label, onClick, onMouseEnter, onMouseLeave, tabIndex, withoutLink }: ButtonProps): React.ReactElement<ButtonProps> {
|
||||
function Button ({ children, className = '', dataTestId = '', icon, isBasic, isBusy, isCircular, isDisabled, isFull, isIcon, isSelected, isToplevel, label, onClick, onMouseEnter, onMouseLeave, tabIndex, withoutLink }: ButtonProps): React.ReactElement<ButtonProps> {
|
||||
const _onClick = useCallback(
|
||||
() => !(isBusy || isDisabled) && onClick && onClick(),
|
||||
[isBusy, isDisabled, onClick]
|
||||
@ -19,6 +19,7 @@ function Button ({ children, className = '', icon, isBasic, isBusy, isCircular,
|
||||
return (
|
||||
<button
|
||||
className={`ui--Button${label ? ' hasLabel' : ''}${isBasic ? ' isBasic' : ''}${isCircular ? ' isCircular' : ''}${isFull ? ' isFull' : ''}${isIcon ? ' isIcon' : ''}${(isBusy || isDisabled) ? ' isDisabled' : ''}${isBusy ? ' isBusy' : ''}${!onClick ? ' isReadOnly' : ''}${isSelected ? ' isSelected' : ''}${isToplevel ? ' isToplevel' : ''}${withoutLink ? ' withoutLink' : ''} ${className}`}
|
||||
data-testid={dataTestId}
|
||||
onClick={_onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
|
||||
@ -9,6 +9,7 @@ export type Button$Callback = () => void | Promise<void>;
|
||||
export interface ButtonProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
dataTestId?: string;
|
||||
icon: IconName;
|
||||
isBasic?: boolean;
|
||||
isBusy?: boolean;
|
||||
|
||||
@ -24,8 +24,17 @@ export class BountyFactory {
|
||||
public aBountyStatus = (status: string): BountyStatus =>
|
||||
this.#registry.createType('BountyStatus', status);
|
||||
|
||||
public bountyStatusWith = ({ curator = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', status = 'Active', updateDue = 100000 } = {}): BountyStatus =>
|
||||
this.#registry.createType('BountyStatus', { active: { curator, updateDue }, status });
|
||||
public bountyStatusWith = ({ curator = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', status = 'Active', updateDue = 100000 } = {}): BountyStatus => {
|
||||
if (status === 'Active') {
|
||||
return this.#registry.createType('BountyStatus', { active: { curator, updateDue }, status });
|
||||
}
|
||||
|
||||
if (status === 'CuratorProposed') {
|
||||
return this.#registry.createType('BountyStatus', { curatorProposed: { curator }, status });
|
||||
}
|
||||
|
||||
throw new Error('Unsupported status');
|
||||
};
|
||||
|
||||
public bountyWith = ({ status = 'Proposed', value = 1 } = {}): Bounty =>
|
||||
this.aBounty({ status: this.aBountyStatus(status), value: balanceOf(value) })
|
||||
|
||||
Loading…
Reference in New Issue
Block a user