From 17196c4558ac0a98b365bffff12fd927cd5058ca Mon Sep 17 00:00:00 2001 From: Jaco Date: Mon, 12 Dec 2022 08:53:17 +0200 Subject: [PATCH] Rework voting modal with all type values (#8519) --- .../page-referenda/src/Referenda/Vote.tsx | 170 +++++++++++------- .../src/Referenda/VoteAbstain.tsx | 54 ++++++ .../src/Referenda/VoteBasic.tsx | 19 ++ .../src/Referenda/VoteSplit.tsx | 45 +++++ .../src/Referenda/VoteStandard.tsx | 59 ++++++ .../page-referenda/src/Referenda/types.ts | 11 ++ packages/react-components/src/VoteValue.tsx | 8 +- 7 files changed, 300 insertions(+), 66 deletions(-) create mode 100644 packages/page-referenda/src/Referenda/VoteAbstain.tsx create mode 100644 packages/page-referenda/src/Referenda/VoteBasic.tsx create mode 100644 packages/page-referenda/src/Referenda/VoteSplit.tsx create mode 100644 packages/page-referenda/src/Referenda/VoteStandard.tsx create mode 100644 packages/page-referenda/src/Referenda/types.ts diff --git a/packages/page-referenda/src/Referenda/Vote.tsx b/packages/page-referenda/src/Referenda/Vote.tsx index 4a901c2876..0a2e77c9f3 100644 --- a/packages/page-referenda/src/Referenda/Vote.tsx +++ b/packages/page-referenda/src/Referenda/Vote.tsx @@ -1,18 +1,26 @@ // Copyright 2017-2022 @polkadot/app-democracy authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { TFunction } from 'react-i18next'; +import type { ApiPromise } from '@polkadot/api'; import type { Preimage } from '@polkadot/app-preimages/types'; import type { BN } from '@polkadot/util'; import type { PalletVote, TrackInfo } from '../types'; import React, { useMemo, useState } from 'react'; +import styled from 'styled-components'; -import { Button, ConvictionDropdown, Modal, ProposedAction, TxButton, VoteAccount, VoteValue } from '@polkadot/react-components'; +import { Button, Modal, ProposedAction, ToggleGroup, TxButton, VoteAccount } from '@polkadot/react-components'; import { useAccounts, useApi, useToggle } from '@polkadot/react-hooks'; import { useTranslation } from '../translate'; +import VoteAbstain from './VoteAbstain'; +import VoteBasic from './VoteBasic'; +import VoteSplit from './VoteSplit'; +import VoteStandard from './VoteStandard'; interface Props { + className?: string; id: BN; isConvictionVote: boolean; isMember: boolean; @@ -39,14 +47,51 @@ function filterMembers (allAccounts: string[], members?: string[], ranks?: BN[], return members; } -function Voting ({ id, isConvictionVote, isMember, members, palletVote, preimage, ranks, trackInfo }: Props): React.ReactElement | null { +function createVoteOpts (api: ApiPromise, t: TFunction, isConvictionVote: boolean): { text: string, value: string }[] { + let hasAbstain = false; + + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error This issue will go way with the correct types + hasAbstain = !!api.createType('PalletConvictionVotingVoteAccountVote', { SplitAbstain: { abstain: 1 } }).isSplitAbstain; + } catch { + hasAbstain = false; + } + + return isConvictionVote + ? hasAbstain + ? [ + { text: t('Aye'), value: 'aye' }, + { text: t('Nay'), value: 'nay' }, + { text: t('Split'), value: 'split' }, + { text: t('Abstain'), value: 'abstain' } + ] + : [ + { text: t('Aye'), value: 'aye' }, + { text: t('Nay'), value: 'nay' }, + { text: t('Split'), value: 'split' } + ] + : [ + { text: t('Aye'), value: 'aye' }, + { text: t('Nay'), value: 'nay' } + ]; +} + +function Voting ({ className, id, isConvictionVote, isMember, members, palletVote, preimage, ranks, trackInfo }: Props): React.ReactElement | null { const { t } = useTranslation(); const { api } = useApi(); const { allAccounts, hasAccounts } = useAccounts(); const [accountId, setAccountId] = useState(null); - const [balance, setBalance] = useState(); - const [conviction, setConviction] = useState(0); const [isVotingOpen, toggleVoting] = useToggle(); + const [voteTypeIndex, setVoteTypeIndex] = useState(0); + const [params, setParams] = useState(); + + // Create the options for the vote toggle using the type of vote and also inspecting + // the actual support of the on-chain runtime (e.g. Abstentions) + const voteTypeOpts = useMemo( + () => createVoteOpts(api, t, isConvictionVote), + [api, isConvictionVote, t] + ); const filteredMembers = useMemo( () => filterMembers(allAccounts, members, ranks, trackInfo), @@ -62,23 +107,6 @@ function Voting ({ id, isConvictionVote, isMember, members, palletVote, preimage [filteredMembers, isMember, members] ); - const [paramsAbstain, paramsAye, paramsNay] = useMemo( - () => [ - // only available on conviction votes - // FIXME: We want to do vote splits as well - isConvictionVote - ? [id, { SplitAbstain: { abstain: balance } }] - : null, - isConvictionVote - ? [id, { Standard: { balance, vote: { aye: true, conviction } } }] - : [id, true], - isConvictionVote - ? [id, { Standard: { balance, vote: { aye: false, conviction } } }] - : [id, false] - ], - [balance, conviction, id, isConvictionVote] - ); - if (!hasAccounts) { return null; } @@ -87,6 +115,7 @@ function Voting ({ id, isConvictionVote, isMember, members, palletVote, preimage <> {isVotingOpen && ( ('Vote on proposal')} onClose={toggleVoting} size='large' @@ -106,54 +135,65 @@ function Voting ({ id, isConvictionVote, isMember, members, palletVote, preimage onChange={setAccountId} /> - {isConvictionVote && ( - -

{t('The balance associated with the vote will be locked as per the conviction specified and will not be available for transfer during this period.')}

-

{t('Conviction locks do overlap and are not additive, meaning that funds locked during a previous vote can be locked again.')}

- - } - > - ('The type of vote that you wish to cast on the referendum.')} + > + +
+ {isConvictionVote + ? voteTypeIndex === 0 + ? ( + + ) + : voteTypeIndex === 1 + ? ( + + ) + : voteTypeIndex === 2 + ? ( + + ) + : ( + + ) + : ( + - ('The conviction to use for this vote, with an appropriate lock period.')} - label={t('conviction')} - onChange={setConviction} - value={conviction} - /> - - )} + ) + } - {isConvictionVote && paramsAbstain && ( - ('Abstain')} - onStart={toggleVoting} - params={paramsAbstain} - tx={api.tx[palletVote].vote} - /> - )} ('Vote Nay')} + icon='check-to-slot' + label={t('Vote')} onStart={toggleVoting} - params={paramsNay} - tx={api.tx[palletVote].vote} - /> - ('Vote Aye')} - onStart={toggleVoting} - params={paramsAye} + params={params} tx={api.tx[palletVote].vote} /> @@ -169,4 +209,8 @@ function Voting ({ id, isConvictionVote, isMember, members, palletVote, preimage ); } -export default React.memo(Voting); +export default React.memo(styled(Voting)` + .ui--Modal-Columns.centerVoteType > div:first-child { + text-align: center; + } +`); diff --git a/packages/page-referenda/src/Referenda/VoteAbstain.tsx b/packages/page-referenda/src/Referenda/VoteAbstain.tsx new file mode 100644 index 0000000000..4d0d2f12cd --- /dev/null +++ b/packages/page-referenda/src/Referenda/VoteAbstain.tsx @@ -0,0 +1,54 @@ +// Copyright 2017-2022 @polkadot/app-democracy authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { BN } from '@polkadot/util'; +import type { VoteTypeProps as Props } from './types'; + +import React, { useEffect, useState } from 'react'; + +import { Modal, VoteValue } from '@polkadot/react-components'; + +import { useTranslation } from '../translate'; + +function VoteAbstain ({ accountId, id, onChange }: Props): React.ReactElement { + const { t } = useTranslation(); + const [balanceAbstain, setBalanceAbstain] = useState(); + const [balanceAye, setBalanceAye] = useState(); + const [balanceNay, setBalanceNay] = useState(); + + useEffect((): void => { + onChange([id, { + SplitAbstain: { + abstain: balanceAbstain, + aye: balanceAye, + nay: balanceNay + } + }]); + }, [balanceAbstain, balanceAye, balanceNay, id, onChange]); + + return ( + ('The value of the balance that is to be split to the abstain, aye and nay parts of the vote')}> + ('abstain vote value')} + onChange={setBalanceAbstain} + /> + ('aye vote value')} + noDefault + onChange={setBalanceAye} + /> + ('nay vote value')} + noDefault + onChange={setBalanceNay} + /> + + ); +} + +export default React.memo(VoteAbstain); diff --git a/packages/page-referenda/src/Referenda/VoteBasic.tsx b/packages/page-referenda/src/Referenda/VoteBasic.tsx new file mode 100644 index 0000000000..cdd87973e1 --- /dev/null +++ b/packages/page-referenda/src/Referenda/VoteBasic.tsx @@ -0,0 +1,19 @@ +// Copyright 2017-2022 @polkadot/app-democracy authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { VoteTypeProps as Props } from './types'; + +import React, { useEffect } from 'react'; + +function VoteBasic ({ id, isAye, onChange }: Props): React.ReactElement | null { + useEffect((): void => { + onChange([ + id, + isAye + ]); + }, [id, isAye, onChange]); + + return null; +} + +export default React.memo(VoteBasic); diff --git a/packages/page-referenda/src/Referenda/VoteSplit.tsx b/packages/page-referenda/src/Referenda/VoteSplit.tsx new file mode 100644 index 0000000000..d51605ec18 --- /dev/null +++ b/packages/page-referenda/src/Referenda/VoteSplit.tsx @@ -0,0 +1,45 @@ +// Copyright 2017-2022 @polkadot/app-democracy authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { BN } from '@polkadot/util'; +import type { VoteTypeProps as Props } from './types'; + +import React, { useEffect, useState } from 'react'; + +import { Modal, VoteValue } from '@polkadot/react-components'; + +import { useTranslation } from '../translate'; + +function VoteSplit ({ accountId, id, onChange }: Props): React.ReactElement { + const { t } = useTranslation(); + const [balanceAye, setBalanceAye] = useState(); + const [balanceNay, setBalanceNay] = useState(); + + useEffect((): void => { + onChange([id, { + Split: { + aye: balanceAye, + nay: balanceNay + } + }]); + }, [balanceAye, balanceNay, id, onChange]); + + return ( + ('The value of the balance that is to be split to the aye and nay parts of the vote')}> + ('aye vote value')} + onChange={setBalanceAye} + /> + ('nay vote value')} + noDefault + onChange={setBalanceNay} + /> + + ); +} + +export default React.memo(VoteSplit); diff --git a/packages/page-referenda/src/Referenda/VoteStandard.tsx b/packages/page-referenda/src/Referenda/VoteStandard.tsx new file mode 100644 index 0000000000..d3134586e5 --- /dev/null +++ b/packages/page-referenda/src/Referenda/VoteStandard.tsx @@ -0,0 +1,59 @@ +// Copyright 2017-2022 @polkadot/app-democracy authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { BN } from '@polkadot/util'; +import type { VoteTypeProps as Props } from './types'; + +import React, { useEffect, useState } from 'react'; + +import { ConvictionDropdown, Modal, VoteValue } from '@polkadot/react-components'; + +import { useTranslation } from '../translate'; + +function VoteStandard ({ accountId, id, isAye, onChange }: Props): React.ReactElement { + const { t } = useTranslation(); + const [balance, setBalance] = useState(); + const [conviction, setConviction] = useState(0); + + useEffect((): void => { + onChange([id, { + Standard: { + balance, + vote: { + aye: isAye, + conviction + } + } + }]); + }, [balance, conviction, id, isAye, onChange]); + + return ( + +

{t('The balance associated with the vote will be locked as per the conviction specified and will not be available for transfer during this period.')}

+

{t('Conviction locks do overlap and are not additive, meaning that funds locked during a previous vote can be locked again.')}

+ + } + > + ('aye vote value') + : t('nay vote value') + } + onChange={setBalance} + /> + ('The conviction to use for this vote, with an appropriate lock period.')} + label={t('conviction')} + onChange={setConviction} + value={conviction} + /> +
+ ); +} + +export default React.memo(VoteStandard); diff --git a/packages/page-referenda/src/Referenda/types.ts b/packages/page-referenda/src/Referenda/types.ts new file mode 100644 index 0000000000..c270a4747b --- /dev/null +++ b/packages/page-referenda/src/Referenda/types.ts @@ -0,0 +1,11 @@ +// Copyright 2017-2022 @polkadot/app-democracy authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { BN } from '@polkadot/util'; + +export interface VoteTypeProps { + accountId: string | null; + id: BN | number; + isAye?: boolean; + onChange: (params: unknown[]) => void; +} diff --git a/packages/react-components/src/VoteValue.tsx b/packages/react-components/src/VoteValue.tsx index e7e06beb92..52b5ff8b07 100644 --- a/packages/react-components/src/VoteValue.tsx +++ b/packages/react-components/src/VoteValue.tsx @@ -17,6 +17,8 @@ interface Props { accountId?: string | null; autoFocus?: boolean; isCouncil?: boolean; + label?: string; + noDefault?: boolean; onChange: (value: BN) => void; } @@ -44,7 +46,7 @@ function getValues (selectedId: string | null | undefined, isCouncil: boolean | }; } -function VoteValue ({ accountId, autoFocus, isCouncil, onChange }: Props): React.ReactElement | null { +function VoteValue ({ accountId, autoFocus, isCouncil, label, noDefault, onChange }: Props): React.ReactElement | null { const { t } = useTranslation(); const { api } = useApi(); const allBalances = useCall(api.derive.balances?.all, [accountId]); @@ -79,14 +81,14 @@ function VoteValue ({ accountId, autoFocus, isCouncil, onChange }: Props): React ('The amount that is associated with this vote. This value is locked for the duration of the vote.')} isDisabled={isDisabled} isZeroable - label={t('vote value')} + label={label || t('vote value')} labelExtra={