Expand society bid kind displays (#4887)

* Expand society bid kind displays

* classes

* vote column naming
This commit is contained in:
Jaco Greeff 2021-03-21 13:19:42 +01:00 committed by GitHub
parent 1a1b6122c5
commit 697f7a8f85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 133 additions and 65 deletions

View File

@ -3,13 +3,14 @@
import type { Bid } from '@polkadot/types/interfaces';
import React, { useEffect, useState } from 'react';
import React, { useMemo } from 'react';
import { AddressSmall, TxButton } from '@polkadot/react-components';
import { useAccounts, useApi } from '@polkadot/react-hooks';
import { FormatBalance } from '@polkadot/react-query';
import { useTranslation } from '../translate';
import BidType from './BidType';
interface Props {
index: number;
@ -20,22 +21,22 @@ function BidRow ({ index, value: { kind, value, who } }: Props): React.ReactElem
const { t } = useTranslation();
const { api } = useApi();
const { allAccounts } = useAccounts();
const [isBidder, setIsBidder] = useState(false);
useEffect((): void => {
const address = who.toString();
const isBidder = useMemo(
(): boolean => {
const address = who.toString();
setIsBidder(allAccounts.some((accountId) => accountId === address));
}, [allAccounts, who]);
return allAccounts.some((accountId) => accountId === address);
},
[allAccounts, who]
);
return (
<tr>
<td className='all'>
<td className='address all'>
<AddressSmall value={who} />
</td>
<td className='number'>
{kind.type}
</td>
<BidType value={kind} />
<td className='number'>
<FormatBalance value={value} />
</td>

View File

@ -0,0 +1,30 @@
// Copyright 2017-2021 @polkadot/app-society authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BidKind } from '@polkadot/types/interfaces';
import React, { useMemo } from 'react';
import { AddressSmall } from '@polkadot/react-components';
interface Props {
value?: BidKind;
}
function BidType ({ value }: Props): React.ReactElement<Props> {
const vouchId = useMemo(
() => value?.isVouch
? value.asVouch[0]
: null,
[value]
);
return (
<>
<td className='no-pad-right'>{value && value.type}</td>
<td>{vouchId && <AddressSmall value={vouchId} />}</td>
</>
);
}
export default React.memo(BidType);

View File

@ -22,7 +22,7 @@ function Bids ({ className }: Props): React.ReactElement<Props> {
const headerRef = useRef([
[t('bids'), 'start'],
[t('kind')],
[t('bid kind'), 'start', 2],
[t('value')],
[]
]);

View File

@ -13,6 +13,7 @@ import { useApi, useCall } from '@polkadot/react-hooks';
import { FormatBalance } from '@polkadot/react-query';
import Votes from '../Overview/Votes';
import BidType from './BidType';
import CandidateVoting from './CandidateVoting';
interface Props {
@ -28,7 +29,7 @@ function Candidate ({ allMembers, isMember, ownMembers, value: { accountId, kind
transform: (voteOpts: Option<SocietyVote>[]): VoteType[] =>
voteOpts
.map((voteOpt, index): [string, Option<SocietyVote>] => [allMembers[index], voteOpt])
.filter(([, voteOpt]): boolean => voteOpt.isSome)
.filter(([, voteOpt]) => voteOpt.isSome)
.map(([accountId, voteOpt]): VoteType => [accountId, voteOpt.unwrap()])
});
@ -37,9 +38,7 @@ function Candidate ({ allMembers, isMember, ownMembers, value: { accountId, kind
<td className='address all'>
<AddressSmall value={accountId} />
</td>
<td className='number'>
{kind.type}
</td>
<BidType value={kind} />
<td className='number'>
<FormatBalance value={value} />
</td>

View File

@ -23,9 +23,9 @@ function Candidates ({ allMembers, candidates, className = '', isMember, ownMemb
const headerRef = useRef([
[t('candidates'), 'start'],
[t('kind')],
[t('bid kind'), 'start', 2],
[t('value')],
[t('votes'), 'expand'],
[undefined, 'expand'],
[]
]);

View File

@ -35,7 +35,7 @@ function Defender ({ className = '', info, isMember, ownMembers }: Props): React
const headerRef = useRef([
[t('defender'), 'start'],
[t('votes'), 'expand'],
[undefined, 'expand'],
[]
]);

View File

@ -1,6 +1,7 @@
// Copyright 2017-2021 @polkadot/app-society authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type BN from 'bn.js';
import type { DeriveSociety } from '@polkadot/api-derive/types';
import type { BlockNumber } from '@polkadot/types/interfaces';
@ -16,9 +17,10 @@ import { useTranslation } from '../translate';
interface Props {
className?: string;
info?: DeriveSociety;
payoutTotal?: BN;
}
function Summary ({ className = '', info }: Props): React.ReactElement<Props> {
function Summary ({ className = '', info, payoutTotal }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const members = useCall<unknown[]>(api.derive.society.members);
@ -65,6 +67,14 @@ function Summary ({ className = '', info }: Props): React.ReactElement<Props> {
</>
)}
<section>
{payoutTotal && (
<CardSummary label={t<string>('payouts')}>
<FormatBalance
value={payoutTotal}
withSi
/>
</CardSummary>
)}
{pot && (
<CardSummary label={t<string>('pot')}>
<FormatBalance

View File

@ -1,6 +1,7 @@
// Copyright 2017-2021 @polkadot/app-society authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type BN from 'bn.js';
import type { DeriveSociety } from '@polkadot/api-derive/types';
import type { MapMember } from '../types';
@ -17,12 +18,16 @@ interface Props {
isMember: boolean;
mapMembers?: MapMember[];
ownMembers: string[];
payoutTotal?: BN;
}
function Overview ({ className, info, isMember, mapMembers, ownMembers }: Props): React.ReactElement<Props> {
function Overview ({ className, info, isMember, mapMembers, ownMembers, payoutTotal }: Props): React.ReactElement<Props> {
return (
<div className={className}>
<Summary info={info} />
<Summary
info={info}
payoutTotal={payoutTotal}
/>
<Defender
info={info}
isMember={isMember}

View File

@ -1,22 +1,33 @@
// Copyright 2017-2021 @polkadot/app-society authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AccountId } from '@polkadot/types/interfaces';
import type { AccountId, BalanceOf, BidKind } from '@polkadot/types/interfaces';
import React from 'react';
import { AddressSmall } from '@polkadot/react-components';
import { FormatBalance } from '@polkadot/react-query';
import BidType from '../Candidates/BidType';
interface Props {
balance?: BalanceOf;
bid?: BidKind;
value: AccountId;
}
function Suspension ({ value }: Props): React.ReactElement<Props> {
function Suspension ({ balance, bid, value }: Props): React.ReactElement<Props> {
return (
<tr>
<td className='address all'>
<AddressSmall value={value} />
</td>
<BidType value={bid} />
<td className='number'>
{balance && (
<FormatBalance value={balance} />
)}
</td>
</tr>
);
}

View File

@ -1,8 +1,9 @@
// Copyright 2017-2021 @polkadot/app-society authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { StorageKey } from '@polkadot/types';
import type { AccountId } from '@polkadot/types/interfaces';
import type { Option, StorageKey } from '@polkadot/types';
import type { AccountId, BalanceOf, BidKind } from '@polkadot/types/interfaces';
import type { ITuple } from '@polkadot/types/types';
import React, { useRef } from 'react';
@ -16,7 +17,25 @@ interface Props {
className?: string;
}
const optExtract = {
interface CandidateSuspend {
accountId: AccountId;
balance: BalanceOf;
bid: BidKind;
}
const optExtractCandidates = {
transform: (entries: [StorageKey<[AccountId]>, Option<ITuple<[BalanceOf, BidKind]>>][]): CandidateSuspend[] =>
entries
.filter(([{ args: [accountId] }, opt]) => opt.isSome && accountId)
.map(([{ args: [accountId] }, opt]) => {
const [balance, bid] = opt.unwrap();
return { accountId, balance, bid };
})
.sort((a, b) => a.balance.cmp(b.balance))
};
const optExtractAccounts = {
transform: (keys: StorageKey<[AccountId]>[]): AccountId[] =>
keys
.map(({ args: [accountId] }) => accountId)
@ -26,12 +45,14 @@ const optExtract = {
function Suspended ({ className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const candidates = useCall<AccountId[]>(api.query.society.suspendedCandidates.keys, undefined, optExtract);
const members = useCall<AccountId[]>(api.query.society.suspendedMembers.keys, undefined, optExtract);
const candidates = useCall<CandidateSuspend[]>(api.query.society.suspendedCandidates.entries, undefined, optExtractCandidates);
const members = useCall<AccountId[]>(api.query.society.suspendedMembers.keys, undefined, optExtractAccounts);
const headerRef = useRef({
candidates: [
[t('candidates'), 'start']
[t('candidates'), 'start'],
[t('bid kind'), 'start', 2],
[t('value')]
],
members: [
[t('members'), 'start']
@ -45,7 +66,7 @@ function Suspended ({ className }: Props): React.ReactElement<Props> {
empty={members && t<string>('No suspended members')}
header={headerRef.current.members}
>
{members && members.map((accountId): React.ReactNode => (
{members?.map((accountId): React.ReactNode => (
<Suspension
key={accountId.toString()}
value={accountId}
@ -57,8 +78,10 @@ function Suspended ({ className }: Props): React.ReactElement<Props> {
empty={candidates && t<string>('No suspended candidates')}
header={headerRef.current.candidates}
>
{candidates && candidates.map((accountId): React.ReactNode => (
{candidates?.map(({ accountId, balance, bid }): React.ReactNode => (
<Suspension
balance={balance}
bid={bid}
key={accountId.toString()}
value={accountId}
/>

View File

@ -1,10 +1,10 @@
// Copyright 2017-2021 @polkadot/app-society authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type BN from 'bn.js';
import type { DeriveSociety, DeriveSocietyMember } from '@polkadot/api-derive/types';
import type { MapMember } from './types';
import BN from 'bn.js';
import React, { useMemo } from 'react';
import { Route, Switch } from 'react-router';
@ -72,8 +72,8 @@ function finalSort (a: MapMember, b: MapMember): number {
: skepticSort(a, b);
}
function getMapMembers (members: DeriveSocietyMember[], skeptics: string[], voters: string[], info: DeriveSociety, warnStrikes: BN): MapMember[] {
return members
function getMapMembers (members: DeriveSocietyMember[], skeptics: string[], voters: string[], info: DeriveSociety, warnStrikes: BN): [MapMember[], BN] {
const mapMembers = members
.filter((member) => !info.hasDefender || !member.accountId.eq(info.defender))
.map((member): MapMember => ({
isCandidateVoter: voters.includes(member.accountId.toString()),
@ -87,6 +87,13 @@ function getMapMembers (members: DeriveSocietyMember[], skeptics: string[], vote
.sort(({ member: a }, { member: b }) => (b.payouts.length - a.payouts.length) || b.strikes.cmp(a.strikes))
.sort(voterSort)
.sort(finalSort);
return [
mapMembers,
mapMembers.reduce((total, { member: { payouts } }) =>
payouts.reduce((total, [, balance]) => total.iadd(balance), total), new BN(0)
)
];
}
function SocietyApp ({ basePath, className }: Props): React.ReactElement<Props> {
@ -98,10 +105,10 @@ function SocietyApp ({ basePath, className }: Props): React.ReactElement<Props>
const members = useCall<DeriveSocietyMember[]>(api.derive.society.members);
const { candidates, skeptics, voters } = useVoters();
const mapMembers = useMemo(
const [mapMembers, payoutTotal] = useMemo(
() => members && info && skeptics && voters
? getMapMembers(members, skeptics, voters, info, api.consts.society.maxStrikes.mul(BN_TWO).div(BN_THREE))
: undefined,
: [undefined, undefined],
[api, info, members, skeptics, voters]
);
@ -124,12 +131,10 @@ function SocietyApp ({ basePath, className }: Props): React.ReactElement<Props>
return (
<main className={className}>
<header>
<Tabs
basePath={basePath}
items={items}
/>
</header>
<Tabs
basePath={basePath}
items={items}
/>
<Switch>
<Route path={`${basePath}/candidates`}>
<Candidates
@ -148,6 +153,7 @@ function SocietyApp ({ basePath, className }: Props): React.ReactElement<Props>
isMember={isMember}
mapMembers={mapMembers}
ownMembers={ownMembers}
payoutTotal={payoutTotal}
/>
</Route>
</Switch>

View File

@ -122,7 +122,7 @@ export default React.memo(styled(CardSummary)`
text-align: right;
> * {
margin: 0.5rem 0;
margin: 0.25rem 0;
&:first-child {
margin-top: 0;

View File

@ -12,7 +12,6 @@ import { bnToBn } from '@polkadot/util';
interface Props {
className?: string;
isDisabled?: boolean;
size?: 'normal' | 'small'
total?: UInt | BN | number | null;
value?: UInt | BN | number | null;
}
@ -35,7 +34,7 @@ function DivClip ({ angle, type }: RotateProps): React.ReactElement<RotateProps>
const Clip = React.memo(DivClip);
function Progress ({ className = '', isDisabled, size = 'normal', total, value }: Props): React.ReactElement<Props> | null {
function Progress ({ className = '', isDisabled, total, value }: Props): React.ReactElement<Props> | null {
const _total = bnToBn(total || 0);
const angle = _total.gtn(0)
? (bnToBn(value || 0).muln(36000).div(_total).toNumber() / 100)
@ -48,7 +47,7 @@ function Progress ({ className = '', isDisabled, size = 'normal', total, value }
const drawAngle = angle % 360;
return (
<div className={`ui--Progress${isDisabled ? ' isDisabled' : ''} ${size}Size ${className}`}>
<div className={`ui--Progress${isDisabled ? ' isDisabled' : ''} ${className}`}>
<div className='background highlight--bg' />
<Clip
angle={
@ -76,9 +75,9 @@ function Progress ({ className = '', isDisabled, size = 'normal', total, value }
export default React.memo(styled(Progress)`
border-radius: 100%;
clip-path: circle(50%);
height: 4.5rem;
height: 4rem;
position: relative;
width: 4.5rem;
width: 4rem;
&.isDisabled {
filter: grayscale(100%);
@ -142,24 +141,8 @@ export default React.memo(styled(Progress)`
div {
line-height: 1;
font-size: 1.25rem;
font-size: 1.1rem;
text-shadow: 0 0 2px #f5f3f1;
}
}
&.smallSize {
height: 2.25rem;
width: 2.25rem;
.inner {
bottom: 0.1875rem;
left: 0.1875rem;
right: 0.1875rem;
top: 0.1875rem;
}
div {
font-size: 0.625rem;
}
}
`);