Rework votes display (#3489)
This commit is contained in:
parent
8faaa6df4a
commit
71ab4c3d04
@ -13,7 +13,7 @@ import { BlockToTime } from '@polkadot/react-query';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import Close from './Close';
|
||||
import Voters from './Votes';
|
||||
import Voters from './Voters';
|
||||
import Voting from './Voting';
|
||||
|
||||
interface Props {
|
||||
@ -69,17 +69,19 @@ function Motion ({ className = '', isMember, members, motion: { hash, proposal,
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
<Voters
|
||||
isAye
|
||||
members={members}
|
||||
threshold={threshold}
|
||||
votes={ayes}
|
||||
/>
|
||||
<Voters
|
||||
members={members}
|
||||
threshold={threshold}
|
||||
votes={nays}
|
||||
/>
|
||||
<td className='expand'>
|
||||
<Voters
|
||||
isAye
|
||||
members={members}
|
||||
threshold={threshold}
|
||||
votes={ayes}
|
||||
/>
|
||||
<Voters
|
||||
members={members}
|
||||
threshold={threshold}
|
||||
votes={nays}
|
||||
/>
|
||||
</td>
|
||||
<td className='button'>
|
||||
{isVoteable && !isCloseable && (
|
||||
<Voting
|
||||
|
||||
59
packages/page-council/src/Motions/Voters.tsx
Normal file
59
packages/page-council/src/Motions/Voters.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
// Copyright 2017-2020 @polkadot/app-democracy 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 { AccountId, MemberCount } from '@polkadot/types/interfaces';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { AddressMini, Expander } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
isAye?: boolean;
|
||||
members: string[];
|
||||
votes: AccountId[];
|
||||
threshold: MemberCount;
|
||||
}
|
||||
|
||||
function Voters ({ isAye, members, threshold, votes }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const counter = useMemo(
|
||||
(): string => {
|
||||
const num = threshold.toNumber();
|
||||
const max = isAye
|
||||
? num
|
||||
: members?.length
|
||||
? (members.length - num) + 1
|
||||
: 0;
|
||||
|
||||
return `${votes.length}${max ? `/${max}` : ''}`;
|
||||
},
|
||||
[isAye, members, threshold, votes]
|
||||
);
|
||||
|
||||
if (!counter || !votes.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Expander
|
||||
summary={
|
||||
isAye
|
||||
? t<string>('Aye {{counter}}', { replace: { counter } })
|
||||
: t<string>('Nay {{counter}}', { replace: { counter } })
|
||||
}
|
||||
>
|
||||
{votes.map((address): React.ReactNode => (
|
||||
<AddressMini
|
||||
key={address.toString()}
|
||||
value={address}
|
||||
withBalance={false}
|
||||
/>
|
||||
))}
|
||||
</Expander>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Voters);
|
||||
@ -1,57 +0,0 @@
|
||||
// Copyright 2017-2020 @polkadot/app-democracy 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 { AccountId, MemberCount } from '@polkadot/types/interfaces';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { AddressMini, Expander } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
isAye?: boolean;
|
||||
members: string[];
|
||||
votes: AccountId[];
|
||||
threshold: MemberCount;
|
||||
}
|
||||
|
||||
function Votes ({ isAye, members, threshold, votes }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [counter, setCounter] = useState('');
|
||||
|
||||
useEffect((): void => {
|
||||
const num = threshold.toNumber();
|
||||
const max = isAye
|
||||
? num
|
||||
: members?.length
|
||||
? (members.length - num) + 1
|
||||
: 0;
|
||||
|
||||
setCounter(`${votes.length}${max ? `/${max}` : ''}`);
|
||||
}, [isAye, members, threshold, votes]);
|
||||
|
||||
return (
|
||||
<td className='address'>
|
||||
{counter && votes.length !== 0 && (
|
||||
<Expander
|
||||
summary={
|
||||
isAye
|
||||
? t<string>('Aye {{counter}}', { replace: { counter } })
|
||||
: t<string>('Nay {{counter}}', { replace: { counter } })
|
||||
}
|
||||
>
|
||||
{votes.map((address): React.ReactNode => (
|
||||
<AddressMini
|
||||
key={address.toString()}
|
||||
value={address}
|
||||
withBalance={false}
|
||||
/>
|
||||
))}
|
||||
</Expander>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Votes);
|
||||
@ -29,8 +29,7 @@ function Proposals ({ className = '', motions, prime }: Props): React.ReactEleme
|
||||
[t('motions'), 'start', 2],
|
||||
[t('threshold')],
|
||||
[t('voting end')],
|
||||
[undefined, 'address'],
|
||||
[undefined, 'address'],
|
||||
[t('votes'), 'start'],
|
||||
[],
|
||||
[undefined, 'badge'],
|
||||
[undefined, 'mini']
|
||||
|
||||
@ -6,7 +6,7 @@ import { DeriveReferendumExt } from '@polkadot/api-derive/types';
|
||||
import { Balance, BlockNumber } from '@polkadot/types/interfaces';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Badge, Button, Icon, LinkExternal } from '@polkadot/react-components';
|
||||
import { useAccounts, useApi, useCall } from '@polkadot/react-hooks';
|
||||
@ -42,33 +42,40 @@ function Referendum ({ className = '', value: { allAye, allNay, image, imageHash
|
||||
const { allAccounts } = useAccounts();
|
||||
const bestNumber = useCall<BlockNumber>(api.derive.chain.bestNumber);
|
||||
const totalIssuance = useCall<Balance>(api.query.balances.totalIssuance);
|
||||
const [percentages, setPercentages] = useState<Percentages | null>(null);
|
||||
const [{ hasVoted, hasVotedAye }, setHasVoted] = useState<VoteType>({ hasVoted: false, hasVotedAye: false });
|
||||
const { changeAye, changeNay } = useChangeCalc(status.threshold, votedAye, votedNay, votedTotal);
|
||||
const threshold = useMemo(
|
||||
() => status.threshold.type.toString().replace('majority', ' majority '),
|
||||
[status]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
if (totalIssuance) {
|
||||
const aye = allAye.reduce((total: BN, { balance }) => total.add(balance), new BN(0));
|
||||
const nay = allNay.reduce((total: BN, { balance }) => total.add(balance), new BN(0));
|
||||
const hasVotedAye = allAye.some(({ accountId }) => allAccounts.includes(accountId.toString()));
|
||||
const hasVoted = hasVotedAye || allNay.some(({ accountId }) => allAccounts.includes(accountId.toString()));
|
||||
const [percentages, { hasVoted, hasVotedAye }] = useMemo(
|
||||
(): [Percentages | null, VoteType] => {
|
||||
if (totalIssuance) {
|
||||
const aye = allAye.reduce((total: BN, { balance }) => total.add(balance), new BN(0));
|
||||
const nay = allNay.reduce((total: BN, { balance }) => total.add(balance), new BN(0));
|
||||
const hasVotedAye = allAye.some(({ accountId }) => allAccounts.includes(accountId.toString()));
|
||||
|
||||
setPercentages({
|
||||
aye: votedTotal.isZero()
|
||||
? ''
|
||||
: `${(aye.muln(10000).div(votedTotal).toNumber() / 100).toFixed(2)}%`,
|
||||
nay: votedTotal.isZero()
|
||||
? ''
|
||||
: `${(nay.muln(10000).div(votedTotal).toNumber() / 100).toFixed(2)}%`,
|
||||
turnout: `${((votedTotal.muln(10000).div(totalIssuance).toNumber()) / 100).toFixed(2)}%`
|
||||
});
|
||||
setHasVoted({ hasVoted, hasVotedAye });
|
||||
}
|
||||
}, [allAccounts, allAye, allNay, totalIssuance, votedTotal]);
|
||||
return [
|
||||
{
|
||||
aye: votedTotal.isZero()
|
||||
? ''
|
||||
: `${(aye.muln(10000).div(votedTotal).toNumber() / 100).toFixed(2)}%`,
|
||||
nay: votedTotal.isZero()
|
||||
? ''
|
||||
: `${(nay.muln(10000).div(votedTotal).toNumber() / 100).toFixed(2)}%`,
|
||||
turnout: `${((votedTotal.muln(10000).div(totalIssuance).toNumber()) / 100).toFixed(2)}%`
|
||||
},
|
||||
{
|
||||
hasVoted: hasVotedAye || allNay.some(({ accountId }) => allAccounts.includes(accountId.toString())),
|
||||
hasVotedAye
|
||||
}
|
||||
];
|
||||
} else {
|
||||
return [null, { hasVoted: false, hasVotedAye: false }];
|
||||
}
|
||||
},
|
||||
[allAccounts, allAye, allNay, totalIssuance, votedTotal]
|
||||
);
|
||||
|
||||
if (!bestNumber || status.end.sub(bestNumber).lten(0)) {
|
||||
return null;
|
||||
@ -102,22 +109,6 @@ function Referendum ({ className = '', value: { allAye, allNay, image, imageHash
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
<ReferendumVotes
|
||||
change={changeAye}
|
||||
count={voteCountAye}
|
||||
index={index}
|
||||
isWinning={isPassing}
|
||||
total={votedAye}
|
||||
votes={allAye}
|
||||
/>
|
||||
<ReferendumVotes
|
||||
change={changeNay}
|
||||
count={voteCountNay}
|
||||
index={index}
|
||||
isWinning={!isPassing}
|
||||
total={votedNay}
|
||||
votes={allNay}
|
||||
/>
|
||||
<td className='badge'>
|
||||
{isBoolean(isPassing) && (
|
||||
<Badge
|
||||
@ -131,6 +122,26 @@ function Referendum ({ className = '', value: { allAye, allNay, image, imageHash
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='expand'>
|
||||
<ReferendumVotes
|
||||
change={changeAye}
|
||||
count={voteCountAye}
|
||||
index={index}
|
||||
isAye
|
||||
isWinning={isPassing}
|
||||
total={votedAye}
|
||||
votes={allAye}
|
||||
/>
|
||||
<ReferendumVotes
|
||||
change={changeNay}
|
||||
count={voteCountNay}
|
||||
index={index}
|
||||
isAye={false}
|
||||
isWinning={!isPassing}
|
||||
total={votedNay}
|
||||
votes={allNay}
|
||||
/>
|
||||
</td>
|
||||
<td className='button'>
|
||||
<Button.Group>
|
||||
{!image?.proposal && (
|
||||
|
||||
@ -5,8 +5,7 @@
|
||||
import { DeriveReferendumVote } from '@polkadot/api-derive/types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Expander, Icon, Tooltip } from '@polkadot/react-components';
|
||||
import { FormatBalance } from '@polkadot/react-query';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
@ -18,6 +17,7 @@ interface Props {
|
||||
change: BN;
|
||||
className?: string;
|
||||
count: number;
|
||||
isAye: boolean;
|
||||
isWinning: boolean;
|
||||
index: BN;
|
||||
total: BN;
|
||||
@ -28,75 +28,67 @@ const LOCKS = [1, 10, 20, 30, 40, 50, 60];
|
||||
|
||||
let id = 0;
|
||||
|
||||
function ReferendumVotes ({ change, className = '', count, index, isWinning, total, votes }: Props): React.ReactElement<Props> {
|
||||
function ReferendumVotes ({ change, className, count, index, isAye, isWinning, total, votes }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const [trigger] = useState(`votes-${index.toString()}-${++id}`);
|
||||
const [sorted, setSorted] = useState<DeriveReferendumVote[]>([]);
|
||||
|
||||
useEffect((): void => {
|
||||
setSorted(
|
||||
votes.sort((a, b) => {
|
||||
const ta = a.balance.muln(LOCKS[a.vote.conviction.toNumber()]).divn(10);
|
||||
const tb = b.balance.muln(LOCKS[b.vote.conviction.toNumber()]).divn(10);
|
||||
const sorted = useMemo(
|
||||
() => votes.sort((a, b) => {
|
||||
const ta = a.balance.muln(LOCKS[a.vote.conviction.toNumber()]).divn(10);
|
||||
const tb = b.balance.muln(LOCKS[b.vote.conviction.toNumber()]).divn(10);
|
||||
|
||||
return tb.cmp(ta);
|
||||
})
|
||||
);
|
||||
}, [votes]);
|
||||
return tb.cmp(ta);
|
||||
}),
|
||||
[votes]
|
||||
);
|
||||
|
||||
if (!count) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<td className={`${className} number`}>
|
||||
<Expander
|
||||
summary={
|
||||
<FormatBalance
|
||||
labelPost={count ? ` (${formatNumber(count)})` : '' }
|
||||
value={total}
|
||||
/>
|
||||
}
|
||||
summarySub={
|
||||
change.gtn(0)
|
||||
? (
|
||||
<>
|
||||
<Icon
|
||||
className='double-icon'
|
||||
icon={isWinning ? 'arrow-circle-down' : 'arrow-circle-up'}
|
||||
tooltip={trigger}
|
||||
/>
|
||||
<FormatBalance value={change} />
|
||||
<Tooltip
|
||||
text={
|
||||
isWinning
|
||||
? t<string>('The amount this total can be reduced by to change the referendum outcome. This assumes changes to the convictions of the existing votes, with no additional turnout.')
|
||||
: t<string>('The amount this total should be increased by to change the referendum outcome. This assumes additional turnout with new votes at 1x conviction.')
|
||||
}
|
||||
trigger={trigger}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{sorted.map((vote) =>
|
||||
<ReferendumVote
|
||||
key={vote.accountId.toString()}
|
||||
vote={vote}
|
||||
/>
|
||||
)}
|
||||
</Expander>
|
||||
</td>
|
||||
<Expander
|
||||
className={className}
|
||||
summary={
|
||||
<>
|
||||
{isAye
|
||||
? t<string>('Aye {{count}}', { replace: { count: ` (${formatNumber(count)})` } })
|
||||
: t<string>('Nay {{count}}', { replace: { count: ` (${formatNumber(count)})` } })
|
||||
}
|
||||
<div><FormatBalance value={total} /></div>
|
||||
</>
|
||||
}
|
||||
summarySub={
|
||||
<div>
|
||||
{change.gtn(0) && (
|
||||
<div>
|
||||
<Icon
|
||||
className='double-icon'
|
||||
icon={isWinning ? 'arrow-circle-down' : 'arrow-circle-up'}
|
||||
tooltip={trigger}
|
||||
/>
|
||||
<FormatBalance value={change} />
|
||||
<Tooltip
|
||||
text={
|
||||
isWinning
|
||||
? t<string>('The amount this total can be reduced by to change the referendum outcome. This assumes changes to the convictions of the existing votes, with no additional turnout.')
|
||||
: t<string>('The amount this total should be increased by to change the referendum outcome. This assumes additional turnout with new votes at 1x conviction.')
|
||||
}
|
||||
trigger={trigger}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{sorted.map((vote) =>
|
||||
<ReferendumVote
|
||||
key={vote.accountId.toString()}
|
||||
vote={vote}
|
||||
/>
|
||||
)}
|
||||
</Expander>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(ReferendumVotes)`
|
||||
.ui--Expander .ui--Expander-summary .double-icon {
|
||||
margin-bottom: -0.125rem;
|
||||
margin-right: 0.375rem;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.ui--Expander-summary {
|
||||
.ui--Icon+.ui--Icon {
|
||||
margin-left: -0.375rem;
|
||||
}
|
||||
}
|
||||
`);
|
||||
export default React.memo(ReferendumVotes);
|
||||
|
||||
@ -23,10 +23,9 @@ function Referendums ({ className = '', referendums }: Props): React.ReactElemen
|
||||
[t('remaining'), 'media--1200'],
|
||||
[t('activate'), 'media--1400'],
|
||||
[t('turnout'), 'media--1400'],
|
||||
[t('aye')],
|
||||
[t('nay')],
|
||||
[undefined, undefined, 2],
|
||||
[undefined, 'badge'],
|
||||
[t('votes'), 'start'],
|
||||
[undefined, undefined, 2],
|
||||
[undefined, 'mini media--1000']
|
||||
]);
|
||||
|
||||
|
||||
@ -84,6 +84,14 @@ export default React.memo(styled(Body)`
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
&.expand {
|
||||
text-align: left;
|
||||
|
||||
.ui--Expander+.ui--Expander {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.hash {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user