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 <jacogr@gmail.com>
This commit is contained in:
Keith Ingram 2020-03-14 00:39:37 +13:00 committed by GitHub
parent 0fa5f18ab4
commit 38405a8d3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 403 additions and 219 deletions

View File

@ -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<Props> | null {
function Accounts ({ className }: Props): React.ReactElement<Props> | 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<Prop
<p>
<Link
to='/accounts'
onClick={_onClose}
onClick={toggleHidden}
>
{t('Create an account now.')}
</Link>
@ -46,3 +41,5 @@ export default function Accounts ({ className }: Props): React.ReactElement<Prop
</BaseOverlay>
);
}
export default React.memo(Accounts);

View File

@ -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<Props> | 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
<Icon
className='closeIcon'
name='close'
onClick={_onClose}
onClick={toggleHidden}
/>
</div>
</div>

View File

@ -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<Props> | null {
function Connecting ({ className }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { isApiConnected, isWaitingInjected } = useApi();
@ -51,3 +51,5 @@ export default function Connecting ({ className }: Props): React.ReactElement<Pr
return null;
}
export default React.memo(Connecting);

View File

@ -2,6 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AccountId } from '@polkadot/types/interfaces';
import { DerivedCollectiveProposal } from '@polkadot/api-derive/types';
import React from 'react';
@ -17,9 +18,10 @@ interface Props {
isMember: boolean;
members: string[];
motion: DerivedCollectiveProposal;
prime: AccountId | null;
}
export default function Motion ({ className, isMember, members, motion: { hash, proposal, votes } }: Props): React.ReactElement<Props> | null {
export default function Motion ({ className, isMember, members, motion: { hash, proposal, votes }, prime }: Props): React.ReactElement<Props> | 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}
/>
<LinkExternal

View File

@ -2,10 +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 { Hash, Proposal, ProposalIndex } from '@polkadot/types/interfaces';
import { AccountId, Hash, Proposal, ProposalIndex } from '@polkadot/types/interfaces';
import React, { useState } from 'react';
import { Button, Modal, ProposedAction, VoteAccount, VoteActions, VoteToggle } from '@polkadot/react-components';
import { Button, Icon, Modal, ProposedAction, VoteAccount, VoteActions, VoteToggle } from '@polkadot/react-components';
import { useAccounts, useToggle } from '@polkadot/react-hooks';
import { isBoolean } from '@polkadot/util';
@ -16,10 +16,11 @@ interface Props {
idNumber: ProposalIndex;
isDisabled: boolean;
members: string[];
prime: AccountId | null;
proposal: Proposal;
}
export default function Voting ({ hash, idNumber, isDisabled, members, proposal }: Props): React.ReactElement<Props> | null {
export default function Voting ({ hash, idNumber, isDisabled, members, prime, proposal }: Props): React.ReactElement<Props> | 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 && (
<article className='warning'>
<div><Icon name='warning sign' />{t('You are voting with this collective\'s prime account. The vote will be the default outcome in case of any abstentions.')}</div>
</article>
)}
</Modal.Content>
<VoteActions
accountId={accountId}

View File

@ -3,6 +3,7 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { DerivedCollectiveProposals, DerivedCollectiveProposal } from '@polkadot/api-derive/types';
import { AccountId } from '@polkadot/types/interfaces';
import React from 'react';
import { Button, Table } from '@polkadot/react-components';
@ -16,9 +17,10 @@ import Slashing from './Slashing';
interface Props {
className?: string;
motions?: DerivedCollectiveProposals;
prime: AccountId | null;
}
export default function Proposals ({ className, motions }: Props): React.ReactElement<Props> {
function Proposals ({ className, motions, prime }: Props): React.ReactElement<Props> {
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}
/>
))}
</Table.Body>
@ -56,3 +59,5 @@ export default function Proposals ({ className, motions }: Props): React.ReactEl
</div>
);
}
export default React.memo(Proposals);

View File

@ -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<Props> {
function Candidate ({ className, address, balance, isPrime, voters }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
<tr>
<tr className={`${className} ${isPrime ? 'council--isPrime' : ''}`}>
<td className='top'>
<AddressSmall value={address} />
</td>
<td className='council--prime'>
{isPrime && (
<div>
<Badge
hover={t('Prime member')}
info={<Icon name='chess king' />}
isInline
isTooltip
type='green'
/>
<span>&nbsp;{t('prime voter')}</span>
</div>
)}
</td>
<td className='top together right'>
{balance && balance.gtn(0) && (
<FormatBalance label={<label>{t('backing')}</label>} value={balance} />
@ -38,3 +55,21 @@ export default function Candidate ({ address, balance, voters }: Props): React.R
</tr>
);
}
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;
}
}
`);

View File

@ -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<Props> {
function Candidates ({ allVotes = {}, className, electionsInfo }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
<div className={className}>
<h1>{t('runners up')}</h1>
{runnersUp.length
? (
<Table>
<Table.Body>
{runnersUp.map(([accountId, balance]): React.ReactNode => (
<Candidate
address={accountId}
balance={balance}
key={accountId.toString()}
voters={allVotes[accountId.toString()]}
/>
))}
</Table.Body>
</Table>
)
: t('No runners up found')
{electionsInfo
? electionsInfo.runnersUp.length
? (
<Table>
<Table.Body>
{electionsInfo.runnersUp.map(([accountId, balance]): React.ReactNode => (
<Candidate
address={accountId}
balance={balance}
key={accountId.toString()}
voters={allVotes[accountId.toString()]}
/>
))}
</Table.Body>
</Table>
)
: t('No runners up found')
: <Spinner />
}
<h1>{t('candidates')}</h1>
{candidates.length
? (
<Table>
<Table.Body>
{candidates.map((accountId): React.ReactNode => (
<Candidate
address={accountId}
key={accountId.toString()}
voters={allVotes[accountId.toString()]}
/>
))}
</Table.Body>
</Table>
)
: t('No candidates found')
{electionsInfo
? electionsInfo.candidates.length
? (
<Table>
<Table.Body>
{electionsInfo.candidates.map((accountId): React.ReactNode => (
<Candidate
address={accountId}
key={accountId.toString()}
voters={allVotes[accountId.toString()]}
/>
))}
</Table.Body>
</Table>
)
: t('No candidates found')
: <Spinner />
}
</div>
);
}
export default React.memo(Candidates);

View File

@ -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<string, AccountId[]>;
className?: string;
prime?: AccountId | null;
}
export default function Members ({ allVotes = {}, className, electionsInfo: { members } }: Props): React.ReactElement<Props> {
function Members ({ allVotes = {}, className, electionsInfo, prime }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
<div className={className}>
{members.length
? (
<Table>
<Table.Body>
{members.map(([accountId, balance]): React.ReactNode => (
<Candidate
address={accountId}
balance={balance}
key={accountId.toString()}
voters={allVotes[accountId.toString()]}
/>
))}
</Table.Body>
</Table>
)
: t('No members found')
<h1>{t('members')}</h1>
{electionsInfo
? electionsInfo.members.length
? (
<Table>
<Table.Body>
{electionsInfo.members.map(([accountId, balance]): React.ReactNode => {
const isPrime = prime?.toString() === accountId.toString();
return (
<Candidate
address={accountId}
balance={balance}
isPrime={isPrime}
key={accountId.toString()}
voters={allVotes[accountId.toString()]}
/>
);
})}
</Table.Body>
</Table>
)
: t('No members found')
: <Spinner />
}
</div>
);
}
export default React.memo(Members);

View File

@ -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<Props> {
function Summary ({ bestNumber, className, electionsInfo }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
if (!electionsInfo) {
return null;
}
const { members, candidateCount, desiredSeats, runnersUp, termDuration, voteCount } = electionsInfo;
return (
<SummaryBox className={className}>
<section>
@ -54,3 +60,5 @@ export default function Summary ({ bestNumber, className, electionsInfo: { membe
</SummaryBox>
);
}
export default React.memo(Summary);

View File

@ -64,17 +64,22 @@ class Vote extends TxModal<Props, State> {
}
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 (
<Button
isDisabled={available.length === 0}
isPrimary
label={t('Vote')}
label={this.props.t('Vote')}
icon='check'
onClick={this.showModal}
/>
@ -82,6 +87,10 @@ class Vote extends TxModal<Props, State> {
}
protected renderContent = (): React.ReactNode => {
if (!this.props.electionsInfo) {
return null;
}
const { electionsInfo: { candidates, members, runnersUp }, t } = this.props;
const { accountId, votes } = this.state;
const available = members
@ -121,6 +130,10 @@ class Vote extends TxModal<Props, State> {
(api.query.electionsPhragmen || api.query.elections)
.votesOf<[AccountId[]] & Codec>(accountId)
.then(([existingVotes]): void => {
if (!this.props.electionsInfo) {
return;
}
const { electionsInfo: { candidates, members, runnersUp } } = this.props;
const available = members
.map(([accountId]): string => accountId.toString())

View File

@ -5,7 +5,7 @@
import { DerivedBalancesAll } from '@polkadot/api-derive/types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { InputBalance } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import { BalanceVoting } from '@polkadot/react-query';
@ -23,7 +23,7 @@ interface ValueState {
value?: BN | string;
}
export default function VoteValue ({ accountId, onChange }: Props): React.ReactElement<Props> | null {
function VoteValue ({ accountId, onChange }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const allBalances = useCall<DerivedBalancesAll>(api.derive.balances.all as any, [accountId]);
@ -56,7 +56,10 @@ export default function VoteValue ({ accountId, onChange }: Props): React.ReactE
isBn(value) && onChange(value);
}, [value]);
const _setValue = (value?: BN): void => setValue({ selectedId, value });
const _setValue = useCallback(
(value?: BN): void => setValue({ selectedId, value }),
[selectedId]
);
return (
<InputBalance
@ -69,3 +72,5 @@ export default function VoteValue ({ accountId, onChange }: Props): React.ReactE
/>
);
}
export default React.memo(VoteValue);

View File

@ -13,7 +13,7 @@ interface Props {
voters: AccountId[];
}
export default function Voters ({ voters }: Props): React.ReactElement<Props> | null {
function Voters ({ voters }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
return (
@ -35,3 +35,5 @@ export default function Voters ({ voters }: Props): React.ReactElement<Props> |
</details>
);
}
export default React.memo(Voters);

View File

@ -6,10 +6,8 @@ import { DerivedElectionsInfo } from '@polkadot/api-derive/types';
import { AccountId, BlockNumber } from '@polkadot/types/interfaces';
import React from 'react';
import { registry } from '@polkadot/react-api';
import { Button } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import { createType } from '@polkadot/types';
import Candidates from './Candidates';
import Members from './Members';
@ -19,38 +17,32 @@ import Vote from './Vote';
interface Props {
className?: string;
prime: AccountId | null;
}
const NULL_INFO: DerivedElectionsInfo = {
candidates: [],
candidateCount: createType(registry, 'u32'),
desiredSeats: createType(registry, 'u32'),
members: [],
runnersUp: [],
termDuration: createType(registry, 'BlockNumber')
};
function transformVotes ([voters, casted]: [AccountId[], AccountId[][]]): Record<string, AccountId[]> {
return voters.reduce((result: Record<string, AccountId[]>, voter, index): Record<string, AccountId[]> => {
casted[index].forEach((candidate): void => {
const address = candidate.toString();
export default function Overview ({ className }: Props): React.ReactElement<Props> {
if (!result[address]) {
result[address] = [];
}
result[address].push(voter);
});
return result;
}, {});
}
function Overview ({ className, prime }: Props): React.ReactElement<Props> {
const { api } = useApi();
const bestNumber = useCall<BlockNumber>(api.derive.chain.bestNumber, []);
const _electionsInfo = useCall<DerivedElectionsInfo>(api.derive.elections.info, []);
const electionsInfo = useCall<DerivedElectionsInfo>(api.derive.elections.info, []);
const allVotes = useCall<Record<string, AccountId[]>>(api.query.electionsPhragmen?.votesOf, [], {
transform: ([voters, casted]: [AccountId[], AccountId[][]]): Record<string, AccountId[]> =>
voters.reduce((result: Record<string, AccountId[]>, voter, index): Record<string, AccountId[]> => {
casted[index].forEach((candidate): void => {
const address = candidate.toString();
if (!result[address]) {
result[address] = [];
}
result[address].push(voter);
});
return result;
}, {})
transform: transformVotes
});
const electionsInfo = _electionsInfo || NULL_INFO;
return (
<div className={className}>
@ -66,6 +58,7 @@ export default function Overview ({ className }: Props): React.ReactElement<Prop
<Members
allVotes={allVotes}
electionsInfo={electionsInfo}
prime={prime}
/>
<Candidates
allVotes={allVotes}
@ -74,3 +67,5 @@ export default function Overview ({ className }: Props): React.ReactElement<Prop
</div>
);
}
export default React.memo(Overview);

View File

@ -8,7 +8,7 @@ import { DerivedElectionsInfo } from '@polkadot/api-derive/types';
import BN from 'bn.js';
export interface ComponentProps {
electionsInfo: DerivedElectionsInfo;
electionsInfo?: DerivedElectionsInfo;
}
export interface VoterPosition {

View File

@ -2,6 +2,8 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { Option } from '@polkadot/types';
import { AccountId } from '@polkadot/types/interfaces';
import { DerivedCollectiveProposals } from '@polkadot/api-derive/types';
import { AppProps, BareProps } from '@polkadot/react-components/types';
@ -26,7 +28,11 @@ function CouncilApp ({ basePath, className }: Props): React.ReactElement<Props>
const { api } = useApi();
const { pathname } = useLocation();
const numMotions = useCounter();
const motions = useCall<DerivedCollectiveProposals>(api.derive.council.proposals, []);
const prime = useCall<AccountId | null>(api.query.council.prime, [], {
transform: (result: Option<AccountId>): AccountId | null => result.unwrapOr(null)
}) || null;
const motions = useCall<DerivedCollectiveProposals>(api.derive.council.proposals);
const items = useMemo(() => [
{
isRoot: true,
@ -49,10 +55,16 @@ function CouncilApp ({ basePath, className }: Props): React.ReactElement<Props>
</header>
<Switch>
<Route path={`${basePath}/motions`}>
<Motions motions={motions} />
<Motions
motions={motions}
prime={prime}
/>
</Route>
</Switch>
<Overview className={[basePath, `${basePath}/candidates`].includes(pathname) ? '' : 'council--hidden'} />
<Overview
className={[basePath, `${basePath}/candidates`].includes(pathname) ? '' : 'council--hidden'}
prime={prime}
/>
</main>
);
}

View File

@ -5,7 +5,7 @@
import { Bid } from '@polkadot/types/interfaces';
import React from 'react';
import { Table } from '@polkadot/react-components';
import { Spinner, Table } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
@ -15,7 +15,7 @@ interface Props {
className?: string;
}
export default function Bids ({ className }: Props): React.ReactElement<Props> {
function Bids ({ className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const bids = useCall<Bid[]>(api.query.society.bids, []);
@ -23,21 +23,25 @@ export default function Bids ({ className }: Props): React.ReactElement<Props> {
return (
<div className={`overviewSection ${className}`}>
<h1>{t('bids')}</h1>
{bids?.length
? (
<Table>
<Table.Body>
{bids.map((bid): React.ReactNode => (
<BidRow
key={bid.who.toString()}
value={bid}
/>
))}
</Table.Body>
</Table>
)
: t('No bids')
{bids
? bids.length
? (
<Table>
<Table.Body>
{bids.map((bid): React.ReactNode => (
<BidRow
key={bid.who.toString()}
value={bid}
/>
))}
</Table.Body>
</Table>
)
: t('No bids')
: <Spinner />
}
</div>
);
}
export default React.memo(Bids);

View File

@ -6,7 +6,7 @@ import { DeriveSocietyCandidate } from '@polkadot/api-derive/types';
import { OwnMembers } from '../types';
import React from 'react';
import { Table } from '@polkadot/react-components';
import { Spinner, Table } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
@ -24,23 +24,25 @@ export default function Candidates ({ allMembers, className, isMember, ownMember
return (
<div className={`overviewSection ${className}`}>
<h1>{t('candidates')}</h1>
{candidates?.length
? (
<Table>
<Table.Body>
{candidates.map((candidate): React.ReactNode => (
<Candidate
allMembers={allMembers}
isMember={isMember}
key={candidate.accountId.toString()}
ownMembers={ownMembers}
value={candidate}
/>
))}
</Table.Body>
</Table>
)
: t('No candidates')
{candidates
? candidates.length
? (
<Table>
<Table.Body>
{candidates.map((candidate): React.ReactNode => (
<Candidate
allMembers={allMembers}
isMember={isMember}
key={candidate.accountId.toString()}
ownMembers={ownMembers}
value={candidate}
/>
))}
</Table.Body>
</Table>
)
: t('No candidates')
: <Spinner />
}
</div>
);

View File

@ -5,34 +5,39 @@
import { DeriveSocietyMember } from '@polkadot/api-derive/types';
import React from 'react';
import styled from 'styled-components';
import { AddressSmall, Badge, Icon } from '@polkadot/react-components';
import { useTranslation } from '../translate';
interface Props {
className?: string;
isHead?: boolean;
value: DeriveSocietyMember;
}
export default function Member ({ isHead, value: { accountId, strikes } }: Props): React.ReactElement<Props> {
function Member ({ className, isHead, value: { accountId, strikes } }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
<tr>
<td className={`${isHead && 'head-icon'}`}>
{isHead && (
<Badge
hover={t('Current head')}
info={<Icon name='check' />}
isInline
isTooltip
type='green'
/>
)}
</td>
<tr className={`${className} ${isHead && 'society--isHead'}`}>
<td className='top'>
<AddressSmall value={accountId} />
</td>
<td className='society--head'>
{isHead && (
<div>
<Badge
hover={t('Current head')}
info={<Icon name='chess king' />}
isInline
isTooltip
type='green'
/>
<span>&nbsp;{t('society head')}</span>
</div>
)}
</td>
<td className='number top'>
<label>{t('strikes')}</label>
{strikes.toString()}
@ -40,3 +45,21 @@ export default function Member ({ isHead, value: { accountId, strikes } }: Props
</tr>
);
}
export default React.memo(styled(Member)`
&.society--isHead {
td {
background-color: rgba(239, 255, 239, 0.8) !important;
}
}
.society--head > div {
display: inline-flex;
align-items: center;
white-space: nowrap;
> span {
color: green;
}
}
`);

View File

@ -5,7 +5,7 @@
import { DeriveSociety, DeriveSocietyMember } from '@polkadot/api-derive/types';
import React, { useEffect, useState } from 'react';
import { Table } from '@polkadot/react-components';
import { Spinner, Table } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
@ -31,21 +31,23 @@ export default function Members ({ className, info }: Props): React.ReactElement
return (
<div className={`overviewSection ${className}`}>
<h1>{t('members')}</h1>
{filtered.length
? (
<Table>
<Table.Body>
{filtered.map((member): React.ReactNode => (
<Member
isHead={info?.head?.eq(member.accountId)}
key={member.accountId.toString()}
value={member}
/>
))}
</Table.Body>
</Table>
)
: t('No active members')
{info
? filtered.length
? (
<Table>
<Table.Body>
{filtered.map((member): React.ReactNode => (
<Member
isHead={info?.head?.eq(member.accountId)}
key={member.accountId.toString()}
value={member}
/>
))}
</Table.Body>
</Table>
)
: t('No active members')
: <Spinner />
}
</div>
);

View File

@ -5,7 +5,7 @@
import { DeriveSociety } from '@polkadot/api-derive/types';
import { BlockNumber } from '@polkadot/types/interfaces';
import React from 'react';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { SummaryBox, CardSummary } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
@ -24,9 +24,11 @@ function Summary ({ className, info }: Props): React.ReactElement<Props> {
const members = useCall<any[]>(api.derive.society.members, []);
const bestNumber = useCall<BlockNumber>(api.derive.chain.bestNumber, []);
const pot = info?.pot.gtn(0)
? info.pot.toString()
: null;
const pot = useMemo((): string | null => {
return info?.pot.gtn(0)
? info.pot.toString()
: null;
}, [info]);
return (
<SummaryBox className={className}>
@ -62,12 +64,14 @@ function Summary ({ className, info }: Props): React.ReactElement<Props> {
</>
)}
<section>
<CardSummary label={t('pot')}>
<FormatBalance
value={pot}
withSi
/>
</CardSummary>
{pot && (
<CardSummary label={t('pot')}>
<FormatBalance
value={pot}
withSi
/>
</CardSummary>
)}
</section>
</SummaryBox>
);

View File

@ -47,11 +47,5 @@ function Overview ({ className }: Props): React.ReactElement<Props> {
export default React.memo(styled(Overview)`
.overviewSection {
margin-bottom: 1.5rem;
.head-icon {
padding-left: 1.5rem;
text-align: center;
width: 1rem;
}
}
`);

View File

@ -5,16 +5,18 @@
import { AccountId } from '@polkadot/types/interfaces';
import React from 'react';
import { AddressSmall, Table } from '@polkadot/react-components';
import styled from 'styled-components';
import { AddressSmall, Badge, Icon, Table } from '@polkadot/react-components';
import { useTranslation } from '../translate';
interface Props {
className?: string;
members?: AccountId[];
prime?: AccountId | null;
}
export default function Members ({ className, members }: Props): React.ReactElement<Props> {
function Members ({ className, members, prime }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
@ -23,13 +25,32 @@ export default function Members ({ className, members }: Props): React.ReactElem
? (
<Table>
<Table.Body>
{members.map((accountId): React.ReactNode => (
<tr key={accountId.toString()}>
<td>
<AddressSmall value={accountId} />
</td>
</tr>
))}
{members.map((accountId): React.ReactNode => {
const isPrime = prime?.toString() === accountId.toString();
return (
<tr
className={isPrime ? 'techcomm--isPrime' : ''}
key={accountId.toString()}
>
<td className='all'>
<AddressSmall value={accountId} />
</td>
<td className='right techcomm--prime'>
{isPrime && (
<div>
<Badge
info={<Icon name='chess king' />}
isInline
type='green'
/>
<span>{' '}{t('prime voter')}</span>
</div>
)}
</td>
</tr>
);
})}
</Table.Body>
</Table>
)
@ -38,3 +59,18 @@ export default function Members ({ className, members }: Props): React.ReactElem
</div>
);
}
export default styled(Members)`
.techcomm--isPrime td {
background: rgba(239, 255, 239, 0.8);
}
.techcomm--prime > div {
display: inline-flex;
align-items: center;
white-space: nowrap;
> span {
color: green;
}
}
`;

View File

@ -9,14 +9,17 @@ import React from 'react';
import Members from './Members';
import Summary from './Summary';
export default function Overview ({ className, members, proposals }: Props): React.ReactElement<Props> {
export default function Overview ({ className, members, prime, proposals }: Props): React.ReactElement<Props> {
return (
<div className={className}>
<Summary
members={members}
proposals={proposals}
/>
<Members members={members} />
<Members
members={members}
prime={prime}
/>
</div>
);
}

View File

@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { Proposal as ProposalType, Votes } from '@polkadot/types/interfaces';
import { AccountId, Proposal as ProposalType, Votes } from '@polkadot/types/interfaces';
import React from 'react';
import { AddressMini } from '@polkadot/react-components';
@ -17,9 +17,10 @@ import Voting from './Voting';
interface Props {
className?: string;
hash: string;
prime?: AccountId | null;
}
export default function Proposal ({ className, hash }: Props): React.ReactElement<Props> | null {
export default function Proposal ({ className, hash, prime }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const optProposal = useCall<Option<ProposalType>>(api.query.technicalCommittee.proposalOf, [hash]);
@ -67,6 +68,7 @@ export default function Proposal ({ className, hash }: Props): React.ReactElemen
<td className='number top together'>
<Voting
hash={hash}
prime={prime}
proposalId={index}
/>
</td>

View File

@ -2,9 +2,11 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AccountId } from '@polkadot/types/interfaces';
import BN from 'bn.js';
import React, { useState } from 'react';
import { Button, Modal, VoteAccount, VoteActions, VoteToggle } from '@polkadot/react-components';
import { Button, Icon, Modal, VoteAccount, VoteActions, VoteToggle } from '@polkadot/react-components';
import { useAccounts, useToggle } from '@polkadot/react-hooks';
import { isBoolean } from '@polkadot/util';
@ -12,10 +14,11 @@ import { useTranslation } from '../translate';
interface Props {
hash: string;
prime?: AccountId | null;
proposalId: BN | number;
}
export default function Voting ({ hash, proposalId }: Props): React.ReactElement<Props> | null {
export default function Voting ({ hash, prime, proposalId }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { hasAccounts } = useAccounts();
const [accountId, setAccountId] = useState<string | null>(null);
@ -27,6 +30,7 @@ export default function Voting ({ hash, proposalId }: Props): React.ReactElement
}
const _onChangeVote = (vote?: boolean): void => setVoteValue(isBoolean(vote) ? vote : true);
const isPrime = accountId === prime?.toString();
return (
<>
@ -41,6 +45,11 @@ export default function Voting ({ hash, proposalId }: Props): React.ReactElement
onChange={_onChangeVote}
value={voteValue}
/>
{isPrime && (
<article className='warning'>
<div><Icon name='warning sign' />{t('You are voting with this collective\'s prime account. The vote will be the default outcome in case of any abstentions.')}</div>
</article>
)}
</Modal.Content>
<VoteActions
accountId={accountId}

View File

@ -13,7 +13,7 @@ import { useTranslation } from '../translate';
import Proposal from './Proposal';
import Propose from './Propose';
export default function Proposals ({ className, members, proposals }: Props): React.ReactElement<Props> {
export default function Proposals ({ className, members, prime, proposals }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [isProposeOpen, togglePropose] = useToggle(false);
@ -41,6 +41,7 @@ export default function Proposals ({ className, members, proposals }: Props): Re
<Proposal
hash={hash.toHex()}
key={hash.toHex()}
prime={prime}
/>
))}
</Table.Body>

View File

@ -2,6 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { Option } from '@polkadot/types';
import { AccountId, Hash } from '@polkadot/types/interfaces';
import { AppProps, BareProps } from '@polkadot/react-components/types';
@ -21,8 +22,11 @@ interface Props extends AppProps, BareProps {}
export default function TechCommApp ({ basePath, className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const members = useCall<AccountId[]>(api.query.technicalCommittee.members, []);
const proposals = useCall<Hash[]>(api.query.technicalCommittee.proposals, []);
const members = useCall<AccountId[]>(api.query.technicalCommittee.members);
const prime = useCall<AccountId | null>(api.query.council.prime, [], {
transform: (result: Option<AccountId>): AccountId | null => result?.unwrapOr(null) || null
}) || null;
const proposals = useCall<Hash[]>(api.query.technicalCommittee.proposals);
const items = useMemo(() => [
{
isRoot: true,
@ -47,12 +51,14 @@ export default function TechCommApp ({ basePath, className }: Props): React.Reac
<Route path={`${basePath}/proposals`}>
<Proposals
members={members}
prime={prime}
proposals={proposals}
/>
</Route>
<Route path={basePath}>
<Overview
members={members}
prime={prime}
proposals={proposals}
/>
</Route>

View File

@ -6,6 +6,7 @@ import { AccountId, Hash } from '@polkadot/types/interfaces';
export interface ComponentProps {
className?: string;
prime?: AccountId | null;
proposals?: Hash[];
members?: AccountId[];
}