From 38405a8d3ae42ecd2eaa1b32dbc4181c315c59ea Mon Sep 17 00:00:00 2001 From: Keith Ingram Date: Sat, 14 Mar 2020 00:39:37 +1300 Subject: [PATCH] Display collective prime voter info and warnings (#2374) * Display prime voter info * enhancements * Update packages/page-council/src/index.tsx * Use React.memo, align society with council displays * Fix merge lints Co-authored-by: Jaco Greeff --- packages/apps/src/overlays/Accounts.tsx | 17 ++--- packages/apps/src/overlays/Base.tsx | 12 ++-- packages/apps/src/overlays/Connecting.tsx | 8 ++- packages/page-council/src/Motions/Motion.tsx | 5 +- packages/page-council/src/Motions/Voting.tsx | 13 +++- packages/page-council/src/Motions/index.tsx | 7 +- .../page-council/src/Overview/Candidate.tsx | 41 ++++++++++- .../page-council/src/Overview/Candidates.tsx | 72 ++++++++++--------- .../page-council/src/Overview/Members.tsx | 47 +++++++----- .../page-council/src/Overview/Summary.tsx | 10 ++- packages/page-council/src/Overview/Vote.tsx | 25 +++++-- .../page-council/src/Overview/VoteValue.tsx | 11 ++- packages/page-council/src/Overview/Voters.tsx | 4 +- packages/page-council/src/Overview/index.tsx | 49 ++++++------- packages/page-council/src/Overview/types.ts | 2 +- packages/page-council/src/index.tsx | 18 ++++- packages/page-society/src/Overview/Bids.tsx | 36 +++++----- .../page-society/src/Overview/Candidates.tsx | 38 +++++----- packages/page-society/src/Overview/Member.tsx | 49 +++++++++---- .../page-society/src/Overview/Members.tsx | 34 ++++----- .../page-society/src/Overview/Summary.tsx | 24 ++++--- packages/page-society/src/Overview/index.tsx | 6 -- .../page-tech-comm/src/Overview/Members.tsx | 54 +++++++++++--- .../page-tech-comm/src/Overview/index.tsx | 7 +- .../page-tech-comm/src/Proposals/Proposal.tsx | 6 +- .../page-tech-comm/src/Proposals/Voting.tsx | 13 +++- .../page-tech-comm/src/Proposals/index.tsx | 3 +- packages/page-tech-comm/src/index.tsx | 10 ++- packages/page-tech-comm/src/types.ts | 1 + 29 files changed, 403 insertions(+), 219 deletions(-) diff --git a/packages/apps/src/overlays/Accounts.tsx b/packages/apps/src/overlays/Accounts.tsx index 32ed44b13f..c0f8ace07f 100644 --- a/packages/apps/src/overlays/Accounts.tsx +++ b/packages/apps/src/overlays/Accounts.tsx @@ -2,9 +2,9 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import React, { useCallback, useState } from 'react'; +import React from 'react'; import { Link } from 'react-router-dom'; -import { useAccounts, useApi } from '@polkadot/react-hooks'; +import { useAccounts, useApi, useToggle } from '@polkadot/react-hooks'; import { useTranslation } from '../translate'; import BaseOverlay from './Base'; @@ -13,16 +13,11 @@ interface Props { className?: string; } -export default function Accounts ({ className }: Props): React.ReactElement | null { +function Accounts ({ className }: Props): React.ReactElement | null { const { t } = useTranslation(); const { hasAccounts } = useAccounts(); const { isApiReady } = useApi(); - const [isHidden, setIsHidden] = useState(false); - - const _onClose = useCallback( - (): void => setIsHidden(true), - [] - ); + const [isHidden, toggleHidden] = useToggle(); if (!isApiReady || hasAccounts || isHidden) { return null; @@ -38,7 +33,7 @@ export default function Accounts ({ className }: Props): React.ReactElement {t('Create an account now.')} @@ -46,3 +41,5 @@ export default function Accounts ({ className }: Props): React.ReactElement ); } + +export default React.memo(Accounts); diff --git a/packages/apps/src/overlays/Base.tsx b/packages/apps/src/overlays/Base.tsx index 774779015b..7cbb8d8104 100644 --- a/packages/apps/src/overlays/Base.tsx +++ b/packages/apps/src/overlays/Base.tsx @@ -2,9 +2,10 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import React, { useCallback, useState } from 'react'; +import React from 'react'; import styled from 'styled-components'; import { Icon } from '@polkadot/react-components'; +import { useToggle } from '@polkadot/react-hooks'; interface Props { children: React.ReactNode; @@ -14,12 +15,7 @@ interface Props { } function BaseOverlay ({ children, className, icon, type }: Props): React.ReactElement | null { - const [isHidden, setIsHidden] = useState(false); - - const _onClose = useCallback( - (): void => setIsHidden(true), - [] - ); + const [isHidden, toggleHidden] = useToggle(); if (isHidden) { return null; @@ -39,7 +35,7 @@ function BaseOverlay ({ children, className, icon, type }: Props): React.ReactEl diff --git a/packages/apps/src/overlays/Connecting.tsx b/packages/apps/src/overlays/Connecting.tsx index d1c72bf1ce..4b3b18ab99 100644 --- a/packages/apps/src/overlays/Connecting.tsx +++ b/packages/apps/src/overlays/Connecting.tsx @@ -10,15 +10,15 @@ import { useTranslation } from '../translate'; import BaseOverlay from './Base'; const wsUrl = settings.apiUrl; -const isWs = wsUrl.startsWith('ws://'); -const isWsLocal = wsUrl.includes('127.0.0.1'); +const isWs = typeof wsUrl === 'string' && wsUrl.startsWith('ws://'); +const isWsLocal = typeof wsUrl === 'string' && wsUrl.includes('127.0.0.1'); const isHttps = window.location.protocol.startsWith('https:'); interface Props { className?: string; } -export default function Connecting ({ className }: Props): React.ReactElement | null { +function Connecting ({ className }: Props): React.ReactElement | null { const { t } = useTranslation(); const { isApiConnected, isWaitingInjected } = useApi(); @@ -51,3 +51,5 @@ export default function Connecting ({ className }: Props): React.ReactElement | null { +export default function Motion ({ className, isMember, members, motion: { hash, proposal, votes }, prime }: Props): React.ReactElement | null { const { t } = useTranslation(); if (!votes) { @@ -66,6 +68,7 @@ export default function Motion ({ className, isMember, members, motion: { hash, idNumber={index} isDisabled={!isMember} members={members} + prime={prime} proposal={proposal} /> | null { +export default function Voting ({ hash, idNumber, isDisabled, members, prime, proposal }: Props): React.ReactElement | null { const { t } = useTranslation(); const { hasAccounts } = useAccounts(); const [isVotingOpen, toggleVoting] = useToggle(); @@ -31,6 +32,7 @@ export default function Voting ({ hash, idNumber, isDisabled, members, proposal } const _onChangeVote = (vote?: boolean): void => setVoteValue(isBoolean(vote) ? vote : true); + const isPrime = prime?.toString() === accountId; return ( <> @@ -52,6 +54,11 @@ export default function Voting ({ hash, idNumber, isDisabled, members, proposal onChange={_onChangeVote} value={voteValue} /> + {isPrime && ( +
+
{t('You are voting with this collective\'s prime account. The vote will be the default outcome in case of any abstentions.')}
+
+ )} { +function Proposals ({ className, motions, prime }: Props): React.ReactElement { const { t } = useTranslation(); const { isMember, members } = useCouncilMembers(); @@ -46,6 +48,7 @@ export default function Proposals ({ className, motions }: Props): React.ReactEl key={motion.hash.toHex()} members={members} motion={motion} + prime={prime} /> ))} @@ -56,3 +59,5 @@ export default function Proposals ({ className, motions }: Props): React.ReactEl ); } + +export default React.memo(Proposals); diff --git a/packages/page-council/src/Overview/Candidate.tsx b/packages/page-council/src/Overview/Candidate.tsx index bfbc43bbc4..ddf9337719 100644 --- a/packages/page-council/src/Overview/Candidate.tsx +++ b/packages/page-council/src/Overview/Candidate.tsx @@ -5,26 +5,43 @@ import { AccountId, Balance } from '@polkadot/types/interfaces'; import React from 'react'; -import { AddressSmall } from '@polkadot/react-components'; +import styled from 'styled-components'; +import { AddressSmall, Badge, Icon } from '@polkadot/react-components'; import { FormatBalance } from '@polkadot/react-query'; import { useTranslation } from '../translate'; import Voters from './Voters'; interface Props { + className?: string; address: AccountId; balance?: Balance; + isPrime?: boolean; voters?: AccountId[]; } -export default function Candidate ({ address, balance, voters }: Props): React.ReactElement { +function Candidate ({ className, address, balance, isPrime, voters }: Props): React.ReactElement { const { t } = useTranslation(); return ( - + + + {isPrime && ( +
+ } + isInline + isTooltip + type='green' + /> +  {t('prime voter')} +
+ )} + {balance && balance.gtn(0) && ( {t('backing')}} value={balance} /> @@ -38,3 +55,21 @@ export default function Candidate ({ address, balance, voters }: Props): React.R ); } + +export default React.memo(styled(Candidate)` + &.council--isPrime { + td { + background-color: rgba(239, 255, 239, 0.8) !important; + } + } + + .council--prime > div { + display: inline-flex; + align-items: center; + white-space: nowrap; + + > span { + color: green; + } + } +`); diff --git a/packages/page-council/src/Overview/Candidates.tsx b/packages/page-council/src/Overview/Candidates.tsx index 4899602ce2..b3ee097a39 100644 --- a/packages/page-council/src/Overview/Candidates.tsx +++ b/packages/page-council/src/Overview/Candidates.tsx @@ -7,7 +7,7 @@ import { AccountId } from '@polkadot/types/interfaces'; import { ComponentProps } from './types'; import React from 'react'; -import { Table } from '@polkadot/react-components'; +import { Spinner, Table } from '@polkadot/react-components'; import { useTranslation } from '../translate'; import Candidate from './Candidate'; @@ -17,46 +17,52 @@ interface Props extends ComponentProps { className?: string; } -export default function Candidates ({ allVotes = {}, className, electionsInfo: { candidates, runnersUp } }: Props): React.ReactElement { +function Candidates ({ allVotes = {}, className, electionsInfo }: Props): React.ReactElement { const { t } = useTranslation(); return (

{t('runners up')}

- {runnersUp.length - ? ( - - - {runnersUp.map(([accountId, balance]): React.ReactNode => ( - - ))} - -
- ) - : t('No runners up found') + {electionsInfo + ? electionsInfo.runnersUp.length + ? ( + + + {electionsInfo.runnersUp.map(([accountId, balance]): React.ReactNode => ( + + ))} + +
+ ) + : t('No runners up found') + : }

{t('candidates')}

- {candidates.length - ? ( - - - {candidates.map((accountId): React.ReactNode => ( - - ))} - -
- ) - : t('No candidates found') + {electionsInfo + ? electionsInfo.candidates.length + ? ( + + + {electionsInfo.candidates.map((accountId): React.ReactNode => ( + + ))} + +
+ ) + : t('No candidates found') + : }
); } + +export default React.memo(Candidates); diff --git a/packages/page-council/src/Overview/Members.tsx b/packages/page-council/src/Overview/Members.tsx index c4dde01307..0d89e74b86 100644 --- a/packages/page-council/src/Overview/Members.tsx +++ b/packages/page-council/src/Overview/Members.tsx @@ -6,7 +6,7 @@ import { AccountId } from '@polkadot/types/interfaces'; import { ComponentProps } from './types'; import React from 'react'; -import { Table } from '@polkadot/react-components'; +import { Spinner, Table } from '@polkadot/react-components'; import { useTranslation } from '../translate'; import Candidate from './Candidate'; @@ -14,30 +14,41 @@ import Candidate from './Candidate'; interface Props extends ComponentProps { allVotes?: Record; className?: string; + prime?: AccountId | null; } -export default function Members ({ allVotes = {}, className, electionsInfo: { members } }: Props): React.ReactElement { +function Members ({ allVotes = {}, className, electionsInfo, prime }: Props): React.ReactElement { const { t } = useTranslation(); return (
- {members.length - ? ( - - - {members.map(([accountId, balance]): React.ReactNode => ( - - ))} - -
- ) - : t('No members found') +

{t('members')}

+ {electionsInfo + ? electionsInfo.members.length + ? ( + + + {electionsInfo.members.map(([accountId, balance]): React.ReactNode => { + const isPrime = prime?.toString() === accountId.toString(); + + return ( + + ); + })} + +
+ ) + : t('No members found') + : }
); } + +export default React.memo(Members); diff --git a/packages/page-council/src/Overview/Summary.tsx b/packages/page-council/src/Overview/Summary.tsx index 04eacb42cf..93a47dd1b0 100644 --- a/packages/page-council/src/Overview/Summary.tsx +++ b/packages/page-council/src/Overview/Summary.tsx @@ -16,9 +16,15 @@ interface Props extends ComponentProps { className?: string; } -export default function Summary ({ bestNumber, className, electionsInfo: { members, candidateCount, desiredSeats, runnersUp, termDuration, voteCount } }: Props): React.ReactElement { +function Summary ({ bestNumber, className, electionsInfo }: Props): React.ReactElement | null { const { t } = useTranslation(); + if (!electionsInfo) { + return null; + } + + const { members, candidateCount, desiredSeats, runnersUp, termDuration, voteCount } = electionsInfo; + return (
@@ -54,3 +60,5 @@ export default function Summary ({ bestNumber, className, electionsInfo: { membe ); } + +export default React.memo(Summary); diff --git a/packages/page-council/src/Overview/Vote.tsx b/packages/page-council/src/Overview/Vote.tsx index cedc9a0802..8a4bf1005f 100644 --- a/packages/page-council/src/Overview/Vote.tsx +++ b/packages/page-council/src/Overview/Vote.tsx @@ -64,17 +64,22 @@ class Vote extends TxModal { } protected renderTrigger = (): React.ReactNode => { - const { electionsInfo: { candidates, members, runnersUp }, t } = this.props; - const available = members - .map(([accountId]): AccountId => accountId) - .concat(runnersUp.map(([accountId]): AccountId => accountId)) - .concat(candidates); + let available: AccountId[] = []; + + if (this.props.electionsInfo) { + const { electionsInfo: { candidates, members, runnersUp } } = this.props; + + available = members + .map(([accountId]): AccountId => accountId) + .concat(runnersUp.map(([accountId]): AccountId => accountId)) + .concat(candidates); + } return (