Single-click close for tips & motions (#3669)

* Single-click close for tips & motions

* Protect against non-close chains
This commit is contained in:
Jaco Greeff 2020-09-17 12:44:54 +02:00 committed by GitHub
parent 31868c5600
commit 2c0fff2931
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 80 additions and 295 deletions

View File

@ -1,94 +0,0 @@
// Copyright 2017-2020 @polkadot/app-council authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { Hash, Proposal, ProposalIndex } from '@polkadot/types/interfaces';
import React, { useState } from 'react';
import { Button, InputAddress, Modal, ProposedAction, TxButton } from '@polkadot/react-components';
import { useApi, useToggle, useWeight } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
interface Props {
hasFailed: boolean;
hash: Hash;
idNumber: ProposalIndex;
isDisabled: boolean;
members: string[];
proposal: Proposal;
}
function Close ({ hasFailed, hash, idNumber, isDisabled, members, proposal }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const [isOpen, toggleOpen] = useToggle();
const [accountId, setAccountId] = useState<string | null>(null);
const [proposalWeight, proposalLength] = useWeight(proposal);
// protect against older versions
if (!api.tx.council.close) {
return null;
}
return (
<>
{isOpen && (
<Modal
header={t<string>('Close proposal')}
size='large'
>
<Modal.Content>
<Modal.Columns>
<Modal.Column>
<ProposedAction
idNumber={idNumber}
proposal={proposal}
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('The proposal that will be affected. Once closed for the current voting round, it would need to be re-submitted to council for a subsequent voting round.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<InputAddress
filter={members}
help={t<string>('Select the account you wish close the proposal with.')}
label={t<string>('sending account')}
onChange={setAccountId}
type='account'
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('The council account that will apply the close for the current round.')}</p>
</Modal.Column>
</Modal.Columns>
</Modal.Content>
<Modal.Actions onCancel={toggleOpen}>
<TxButton
accountId={accountId}
onStart={toggleOpen}
params={
api.tx.council.close.meta.args.length === 4
? hasFailed
? [hash, idNumber, 0, 0]
: [hash, idNumber, proposalWeight, proposalLength]
: [hash, idNumber]
}
tx='council.close'
/>
</Modal.Actions>
</Modal>
)}
<Button
icon='times'
isDisabled={isDisabled}
label={t<string>('Close')}
onClick={toggleOpen}
/>
</>
);
}
export default React.memo(Close);

View File

@ -5,14 +5,14 @@
import { AccountId } from '@polkadot/types/interfaces';
import { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import React, { useEffect, useState } from 'react';
import React, { useMemo } from 'react';
import ProposalCell from '@polkadot/app-democracy/Overview/ProposalCell';
import { Icon, LinkExternal } from '@polkadot/react-components';
import { useAccounts, useVotingStatus } from '@polkadot/react-hooks';
import { Icon, LinkExternal, TxButton } from '@polkadot/react-components';
import { useAccounts, useApi, useVotingStatus, useWeight } from '@polkadot/react-hooks';
import { BlockToTime } from '@polkadot/react-query';
import { formatNumber } from '@polkadot/util';
import Close from './Close';
import { useTranslation } from '../translate';
import Voters from './Voters';
import Voting from './Voting';
@ -30,20 +30,32 @@ interface VoterState {
}
function Motion ({ className = '', isMember, members, motion: { hash, proposal, votes }, prime }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const { allAccounts } = useAccounts();
const { hasFailed, isCloseable, isVoteable, remainingBlocks } = useVotingStatus(votes, members.length, 'council');
const [{ hasVoted, hasVotedAye }, setIsVoter] = useState<VoterState>({ hasVoted: false, hasVotedAye: false });
const [proposalWeight, proposalLength] = useWeight(proposal);
useEffect((): void => {
if (votes) {
const hasVotedAye = allAccounts.some((address) => votes.ayes.some((accountId) => accountId.eq(address)));
const councilId = useMemo(
() => allAccounts.find((accountId) => members.includes(accountId)) || null,
[allAccounts, members]
);
setIsVoter({
hasVoted: hasVotedAye || allAccounts.some((address) => votes.nays.some((accountId) => accountId.eq(address))),
hasVotedAye
});
}
}, [allAccounts, votes]);
const { hasVoted, hasVotedAye } = useMemo(
(): VoterState => {
if (votes) {
const hasVotedAye = allAccounts.some((address) => votes.ayes.some((accountId) => accountId.eq(address)));
return {
hasVoted: hasVotedAye || allAccounts.some((address) => votes.nays.some((accountId) => accountId.eq(address))),
hasVotedAye
};
}
return { hasVoted: false, hasVotedAye: false };
},
[allAccounts, votes]
);
if (!votes) {
return null;
@ -94,13 +106,18 @@ function Motion ({ className = '', isMember, members, motion: { hash, proposal,
/>
)}
{isCloseable && (
<Close
hasFailed={hasFailed}
hash={hash}
idNumber={index}
isDisabled={!isMember}
members={members}
proposal={proposal}
<TxButton
accountId={councilId}
icon='times'
label={t<string>('Close')}
params={
api.tx.council.close?.meta.args.length === 4
? hasFailed
? [hash, index, 0, 0]
: [hash, index, proposalWeight, proposalLength]
: [hash, index]
}
tx='council.close'
/>
)}
</td>

View File

@ -1,93 +0,0 @@
// Copyright 2017-2020 @polkadot/app-tech-comm authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { Hash, Proposal, ProposalIndex } from '@polkadot/types/interfaces';
import React, { useState } from 'react';
import { Button, InputAddress, Modal, ProposedAction, TxButton } from '@polkadot/react-components';
import { useApi, useToggle, useWeight } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
interface Props {
hasFailed: boolean;
hash: Hash;
idNumber: ProposalIndex;
isDisabled: boolean;
members: string[];
proposal: Proposal;
}
function Close ({ hasFailed, hash, idNumber, isDisabled, members, proposal }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const [isOpen, toggleOpen] = useToggle();
const [accountId, setAccountId] = useState<string | null>(null);
const [proposalWeight, proposalLength] = useWeight(proposal);
if (!api.tx.technicalCommittee.close) {
return null;
}
return (
<>
{isOpen && (
<Modal
header={t<string>('Close proposal')}
size='large'
>
<Modal.Content>
<Modal.Columns>
<Modal.Column>
<ProposedAction
idNumber={idNumber}
proposal={proposal}
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('The proposal that will be affected. Once closed for the current voting round, it would need to be re-submitted for a subsequent voting round.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<InputAddress
filter={members}
help={t<string>('Select the account you wish close the proposal with.')}
label={t<string>('sending account')}
onChange={setAccountId}
type='account'
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('The committee account that will apply the close for the current round.')}</p>
</Modal.Column>
</Modal.Columns>
</Modal.Content>
<Modal.Actions onCancel={toggleOpen}>
<TxButton
accountId={accountId}
onStart={toggleOpen}
params={
api.tx.technicalCommittee.close.meta.args.length === 4
? hasFailed
? [hash, idNumber, 0, 0]
: [hash, idNumber, proposalWeight, proposalLength]
: [hash, idNumber]
}
tx='technicalCommittee.close'
/>
</Modal.Actions>
</Modal>
)}
<Button
icon='times'
isDisabled={isDisabled}
label={t<string>('Close')}
onClick={toggleOpen}
/>
</>
);
}
export default React.memo(Close);

View File

@ -4,15 +4,15 @@
import { AccountId, Hash, Proposal as ProposalType, Votes } from '@polkadot/types/interfaces';
import React from 'react';
import { AddressMini } from '@polkadot/react-components';
import { useApi, useCall, useVotingStatus } from '@polkadot/react-hooks';
import React, { useMemo } from 'react';
import { AddressMini, TxButton } from '@polkadot/react-components';
import { useAccounts, useApi, useCall, useVotingStatus, useWeight } from '@polkadot/react-hooks';
import { BlockToTime } from '@polkadot/react-query';
import ProposalCell from '@polkadot/app-democracy/Overview/ProposalCell';
import { Option } from '@polkadot/types';
import { formatNumber } from '@polkadot/util';
import Close from './Close';
import { useTranslation } from '../translate';
import Voting from './Voting';
interface Props {
@ -31,11 +31,19 @@ const transformVotes = {
transform: (optVotes: Option<Votes>) => optVotes.unwrapOr(null)
};
function Proposal ({ className = '', imageHash, isMember, members, prime }: Props): React.ReactElement<Props> | null {
function Proposal ({ className = '', imageHash, members, prime }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const { allAccounts } = useAccounts();
const proposal = useCall<ProposalType | null>(api.query.technicalCommittee.proposalOf, [imageHash], transformProposal);
const votes = useCall<Votes | null>(api.query.technicalCommittee.voting, [imageHash], transformVotes);
const { hasFailed, isCloseable, isVoteable, remainingBlocks } = useVotingStatus(votes, members.length, 'technicalCommittee');
const [proposalWeight, proposalLength] = useWeight(proposal);
const councilId = useMemo(
() => allAccounts.find((accountId) => members.includes(accountId)) || null,
[allAccounts, members]
);
if (!proposal || !votes) {
return null;
@ -88,13 +96,18 @@ function Proposal ({ className = '', imageHash, isMember, members, prime }: Prop
/>
)}
{isCloseable && (
<Close
hasFailed={hasFailed}
hash={imageHash}
idNumber={index}
isDisabled={!isMember}
members={members}
proposal={proposal}
<TxButton
accountId={councilId}
icon='times'
label={t<string>('Close')}
params={
api.tx.technicalCommittee.close?.meta.args.length === 4
? hasFailed
? [imageHash, index, 0, 0]
: [imageHash, index, proposalWeight, proposalLength]
: [imageHash, index]
}
tx='technicalCommittee.close'
/>
)}
</td>

View File

@ -13,7 +13,6 @@ import { BlockToTime, FormatBalance } from '@polkadot/react-query';
import { BN_ZERO, formatNumber } from '@polkadot/util';
import { useTranslation } from '../translate';
import TipClose from './TipClose';
import TipEndorse from './TipEndorse';
import TipReason from './TipReason';
@ -86,6 +85,11 @@ function Tip ({ bestNumber, className = '', defaultId, hash, isMember, members,
[allAccounts, hash, tip]
);
const councilId = useMemo(
() => allAccounts.find((accountId) => members.includes(accountId)) || null,
[allAccounts, members]
);
const [isMedianSelected, setMedianTip] = useState(false);
useEffect((): void => {
@ -164,10 +168,12 @@ function Tip ({ bestNumber, className = '', defaultId, hash, isMember, members,
/>
)
: (
<TipClose
hash={hash}
isMember={isMember}
members={members}
<TxButton
accountId={councilId}
icon='times'
label={t<string>('Close')}
params={[hash]}
tx='treasury.closeTip'
/>
)
}

View File

@ -1,67 +0,0 @@
// Copyright 2017-2020 @polkadot/app-treasury authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useState } from 'react';
import { Button, InputAddress, Modal, TxButton } from '@polkadot/react-components';
import { useToggle } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
interface Props {
hash: string;
isMember: boolean;
members: string[];
}
function TipClose ({ hash, isMember, members }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [isOpen, toggleOpen] = useToggle();
const [accountId, setAccountId] = useState<string | null>(null);
return (
<>
<Button
icon='times'
isDisabled={!isMember}
label={t<string>('Close')}
onClick={toggleOpen}
/>
{isOpen && (
<Modal
header={t<string>('Close tip')}
size='large'
>
<Modal.Content>
<Modal.Columns>
<Modal.Column>
<InputAddress
filter={members}
help={t<string>('Select the account you wish to close from.')}
label={t<string>('submit with account')}
onChange={setAccountId}
type='account'
withLabel
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('Your close will be applied for this council account.')}</p>
</Modal.Column>
</Modal.Columns>
</Modal.Content>
<Modal.Actions onCancel={toggleOpen}>
<TxButton
accountId={accountId}
isDisabled={!accountId}
onStart={toggleOpen}
params={[hash]}
tx='treasury.closeTip'
/>
</Modal.Actions>
</Modal>
)}
</>
);
}
export default React.memo(TipClose);

View File

@ -7,6 +7,7 @@ import { BlockNumber, Votes } from '@polkadot/types/interfaces';
import BN from 'bn.js';
import { useMemo } from 'react';
import { ApiPromise } from '@polkadot/api';
import { isFunction } from '@polkadot/util';
import useApi from './useApi';
import useCall from './useCall';
@ -39,9 +40,11 @@ function getStatus (api: ApiPromise, bestNumber: BlockNumber, votes: Votes, numM
return {
hasFailed,
hasPassed,
isCloseable: api.tx[section].close?.meta.args.length === 4 // current-generation
? isEnd || hasPassed || hasFailed
: isEnd,
isCloseable: isFunction(api.tx[section].close)
? api.tx[section].close.meta.args.length === 4 // current-generation
? isEnd || hasPassed || hasFailed
: isEnd
: false,
isVoteable: !isEnd,
remainingBlocks: isEnd
? null