Show society skeptics & voters (#4879)
* Show society skeptics & voters * useCounter for candidates * Sort founder to bottom * payouts due * sizing * remove header
This commit is contained in:
parent
96ac7519f1
commit
268d4dd557
@ -6,22 +6,20 @@ import type { DeriveSocietyCandidate } from '@polkadot/api-derive/types';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@polkadot/react-components';
|
||||
import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import Candidate from './Candidate';
|
||||
|
||||
interface Props {
|
||||
allMembers: string[];
|
||||
candidates?: DeriveSocietyCandidate[];
|
||||
className?: string;
|
||||
isMember: boolean;
|
||||
ownMembers: string[];
|
||||
}
|
||||
|
||||
function Candidates ({ allMembers, className = '', isMember, ownMembers }: Props): React.ReactElement<Props> {
|
||||
function Candidates ({ allMembers, candidates, className = '', isMember, ownMembers }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const candidates = useCall<DeriveSocietyCandidate[]>(api.derive.society.candidates);
|
||||
|
||||
const headerRef = useRef([
|
||||
[t('candidates'), 'start'],
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
// Copyright 2017-2021 @polkadot/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveSocietyCandidate } from '@polkadot/api-derive/types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '@polkadot/react-components';
|
||||
@ -13,12 +15,13 @@ import AllCandidates from './Candidates';
|
||||
|
||||
interface Props {
|
||||
allMembers: string[];
|
||||
candidates?: DeriveSocietyCandidate[];
|
||||
className?: string;
|
||||
isMember: boolean;
|
||||
ownMembers: string[];
|
||||
}
|
||||
|
||||
function Candidates ({ allMembers, className, isMember, ownMembers }: Props): React.ReactElement<Props> {
|
||||
function Candidates ({ allMembers, candidates, className, isMember, ownMembers }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [isBidOpen, toggleBidOpen] = useToggle();
|
||||
|
||||
@ -36,6 +39,7 @@ function Candidates ({ allMembers, className, isMember, ownMembers }: Props): Re
|
||||
</Button.Group>
|
||||
<AllCandidates
|
||||
allMembers={allMembers}
|
||||
candidates={candidates}
|
||||
isMember={isMember}
|
||||
ownMembers={ownMembers}
|
||||
/>
|
||||
|
||||
@ -1,22 +1,35 @@
|
||||
// Copyright 2017-2021 @polkadot/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveSocietyMember } from '@polkadot/api-derive/types';
|
||||
import type BN from 'bn.js';
|
||||
import type { bool } from '@polkadot/types';
|
||||
import type { BalanceOf, BlockNumber } from '@polkadot/types/interfaces';
|
||||
import type { MapMember } from '../types';
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AddressSmall, Icon, Modal, Tag } from '@polkadot/react-components';
|
||||
import { useApi, useCall, useToggle } from '@polkadot/react-hooks';
|
||||
import { KUSAMA_GENESIS } from '@polkadot/apps-config';
|
||||
import { AddressSmall, Columar, Expander, Icon, Modal, Tag } from '@polkadot/react-components';
|
||||
import { useApi, useCallMulti, useToggle } from '@polkadot/react-hooks';
|
||||
import { BlockToTime, FormatBalance } from '@polkadot/react-query';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import drawCanary from '../draw/canary';
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
bestNumber?: BN;
|
||||
className?: string;
|
||||
isFounder?: boolean;
|
||||
isHead?: boolean;
|
||||
value: DeriveSocietyMember;
|
||||
isSkeptic?: boolean;
|
||||
value: MapMember;
|
||||
}
|
||||
|
||||
interface MultiState {
|
||||
isSuspended: boolean;
|
||||
payouts: [BlockNumber, BalanceOf][];
|
||||
}
|
||||
|
||||
const CANVAS_STYLE = {
|
||||
@ -24,13 +37,47 @@ const CANVAS_STYLE = {
|
||||
margin: '0 auto'
|
||||
};
|
||||
|
||||
function Member ({ className = '', isFounder, isHead, value: { accountId, strikes } }: Props): React.ReactElement<Props> {
|
||||
const optMulti = {
|
||||
defaultValue: {
|
||||
isSuspended: false,
|
||||
payouts: [] as [BlockNumber, BalanceOf][]
|
||||
},
|
||||
transform: ([suspended, payouts]: [bool, [BlockNumber, BalanceOf][]]): MultiState => ({
|
||||
isSuspended: suspended.isTrue,
|
||||
payouts
|
||||
})
|
||||
};
|
||||
|
||||
function Member ({ bestNumber, className = '', value: { isFounder, isHead, isSkeptic, isVoter, member: { accountId, strikes } } }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const suspended = useCall<bool>(api.query.society.suspendedMembers, [accountId]);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const [canInk] = useState(() => api.genesisHash.eq('0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe'));
|
||||
const [canInk] = useState(() => api.genesisHash.eq(KUSAMA_GENESIS));
|
||||
const [isInkShowing, toggleInk] = useToggle();
|
||||
const { isSuspended, payouts } = useCallMulti<MultiState>([
|
||||
[api.query.society.suspendedMembers, accountId],
|
||||
[api.query.society.payouts, accountId]
|
||||
], optMulti);
|
||||
|
||||
const renderPayouts = useCallback(
|
||||
() => bestNumber && payouts.map(([bn, value], index) => (
|
||||
<p key={index}>
|
||||
<Columar>
|
||||
<Columar.Column>
|
||||
<FormatBalance value={value} />
|
||||
</Columar.Column>
|
||||
<Columar.Column>
|
||||
<div>#{formatNumber(bn)}</div>
|
||||
<BlockToTime
|
||||
key={index}
|
||||
value={bn.sub(bestNumber)}
|
||||
/>
|
||||
</Columar.Column>
|
||||
</Columar>
|
||||
</p>
|
||||
)),
|
||||
[bestNumber, payouts]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
if (canvasRef.current) {
|
||||
@ -60,17 +107,34 @@ function Member ({ className = '', isFounder, isHead, value: { accountId, strike
|
||||
label={t<string>('society founder')}
|
||||
/>
|
||||
)}
|
||||
{suspended?.isTrue && (
|
||||
{isSkeptic && (
|
||||
<Tag
|
||||
color='yellow'
|
||||
label={t<string>('skeptic')}
|
||||
/>
|
||||
)}
|
||||
{isVoter && (
|
||||
<Tag
|
||||
color='blue'
|
||||
label={t<string>('voted')}
|
||||
/>
|
||||
)}
|
||||
{isSuspended && (
|
||||
<Tag
|
||||
color='red'
|
||||
label={t<string>('suspended')}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='all'> </td>
|
||||
<td className='number'>
|
||||
{strikes.toString()}
|
||||
<td className='all number'>
|
||||
{payouts.length !== 0 && (
|
||||
<Expander
|
||||
renderChildren={renderPayouts}
|
||||
summary={t<string>('Payouts ({{count}})', { replace: { count: formatNumber(payouts.length) } })}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='number'>{formatNumber(strikes)}</td>
|
||||
<td>
|
||||
{canInk && (
|
||||
<>
|
||||
@ -104,4 +168,16 @@ function Member ({ className = '', isFounder, isHead, value: { accountId, strike
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Member);
|
||||
export default React.memo(styled(Member)`
|
||||
.ui--Column {
|
||||
&:first-child {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
max-width: 13ch;
|
||||
min-width: 13ch;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
@ -1,59 +1,43 @@
|
||||
// Copyright 2017-2021 @polkadot/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveSociety, DeriveSocietyMember } from '@polkadot/api-derive/types';
|
||||
import type { MapMember } from '../types';
|
||||
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@polkadot/react-components';
|
||||
import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
import { useBestNumber } from '@polkadot/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import Member from './Member';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
info?: DeriveSociety;
|
||||
mapMembers?: MapMember[];
|
||||
}
|
||||
|
||||
function Members ({ className = '', info }: Props): React.ReactElement<Props> {
|
||||
function Members ({ className = '', mapMembers }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const members = useCall<DeriveSocietyMember[]>(api.derive.society.members);
|
||||
const bestNumber = useBestNumber();
|
||||
|
||||
const headerRef = useRef([
|
||||
[t('members'), 'start', 3],
|
||||
[t('members'), 'start', 2],
|
||||
[],
|
||||
[t('strikes')],
|
||||
[]
|
||||
]);
|
||||
|
||||
const filtered = useMemo(
|
||||
() => members && info
|
||||
? members
|
||||
.filter((member) => !info.hasDefender || !member.accountId.eq(info.defender))
|
||||
.sort((a, b) =>
|
||||
!info.head?.eq(a.accountId)
|
||||
? 1
|
||||
: !info.head?.eq(b.accountId)
|
||||
? -1
|
||||
: 0
|
||||
)
|
||||
: undefined,
|
||||
[info, members]
|
||||
);
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={info && filtered && t<string>('No active members')}
|
||||
empty={mapMembers && t<string>('No active members')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{info && filtered && filtered.map((member): React.ReactNode => (
|
||||
{mapMembers?.map((value): React.ReactNode => (
|
||||
<Member
|
||||
isFounder={info.founder?.eq(member.accountId)}
|
||||
isHead={info.head?.eq(member.accountId)}
|
||||
key={member.accountId.toString()}
|
||||
value={member}
|
||||
bestNumber={bestNumber}
|
||||
key={value.key}
|
||||
value={value}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveSociety } from '@polkadot/api-derive/types';
|
||||
import type { MapMember } from '../types';
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
@ -14,10 +15,11 @@ interface Props {
|
||||
className?: string;
|
||||
info?: DeriveSociety;
|
||||
isMember: boolean;
|
||||
mapMembers?: MapMember[];
|
||||
ownMembers: string[];
|
||||
}
|
||||
|
||||
function Overview ({ className, info, isMember, ownMembers }: Props): React.ReactElement<Props> {
|
||||
function Overview ({ className, info, isMember, mapMembers, ownMembers }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<div className={className}>
|
||||
<Summary info={info} />
|
||||
@ -26,7 +28,7 @@ function Overview ({ className, info, isMember, ownMembers }: Props): React.Reac
|
||||
isMember={isMember}
|
||||
ownMembers={ownMembers}
|
||||
/>
|
||||
<Members info={info} />
|
||||
<Members mapMembers={mapMembers} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
// Copyright 2017-2021 @polkadot/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveSociety } from '@polkadot/api-derive/types';
|
||||
import type { DeriveSociety, DeriveSocietyMember } from '@polkadot/api-derive/types';
|
||||
import type { MapMember } from './types';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { Route, Switch } from 'react-router';
|
||||
@ -15,6 +16,7 @@ import Suspended from './Suspended';
|
||||
import { useTranslation } from './translate';
|
||||
import useCounter from './useCounter';
|
||||
import useMembers from './useMembers';
|
||||
import useVoters from './useVoters';
|
||||
|
||||
interface Props {
|
||||
basePath: string;
|
||||
@ -23,12 +25,53 @@ interface Props {
|
||||
|
||||
export { useCounter };
|
||||
|
||||
function getMapMembers (members: DeriveSocietyMember[], skeptics: string[], voters: string[], info: DeriveSociety): MapMember[] {
|
||||
return members
|
||||
.filter((member) => !info.hasDefender || !member.accountId.eq(info.defender))
|
||||
.map((member): MapMember => ({
|
||||
isFounder: info.founder?.eq(member.accountId),
|
||||
isHead: info.head?.eq(member.accountId),
|
||||
isSkeptic: skeptics.includes(member.accountId.toString()),
|
||||
isVoter: voters.includes(member.accountId.toString()),
|
||||
key: member.accountId.toString(),
|
||||
member
|
||||
}))
|
||||
.sort((a, b) =>
|
||||
a.isHead
|
||||
? -1
|
||||
: b.isHead
|
||||
? 1
|
||||
: a.isSkeptic
|
||||
? -1
|
||||
: b.isSkeptic
|
||||
? 1
|
||||
: a.isVoter
|
||||
? -1
|
||||
: b.isVoter
|
||||
? 1
|
||||
: a.isFounder
|
||||
? 1
|
||||
: b.isFounder
|
||||
? -1
|
||||
: 0
|
||||
);
|
||||
}
|
||||
|
||||
function SocietyApp ({ basePath, className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const candidateCount = useCounter();
|
||||
const { allMembers, isMember, ownMembers } = useMembers();
|
||||
const info = useCall<DeriveSociety>(api.derive.society.info);
|
||||
const members = useCall<DeriveSocietyMember[]>(api.derive.society.members);
|
||||
const { candidates, skeptics, voters } = useVoters();
|
||||
|
||||
const mapMembers = useMemo(
|
||||
() => members && info
|
||||
? getMapMembers(members, skeptics, voters, info)
|
||||
: undefined,
|
||||
[info, members, skeptics, voters]
|
||||
);
|
||||
|
||||
const items = useMemo(() => [
|
||||
{
|
||||
@ -59,6 +102,7 @@ function SocietyApp ({ basePath, className }: Props): React.ReactElement<Props>
|
||||
<Route path={`${basePath}/candidates`}>
|
||||
<Candidates
|
||||
allMembers={allMembers}
|
||||
candidates={candidates}
|
||||
isMember={isMember}
|
||||
ownMembers={ownMembers}
|
||||
/>
|
||||
@ -70,6 +114,7 @@ function SocietyApp ({ basePath, className }: Props): React.ReactElement<Props>
|
||||
<Overview
|
||||
info={info}
|
||||
isMember={isMember}
|
||||
mapMembers={mapMembers}
|
||||
ownMembers={ownMembers}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
@ -1,8 +1,18 @@
|
||||
// Copyright 2017-2021 @polkadot/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveSocietyCandidate, DeriveSocietyMember } from '@polkadot/api-derive/types';
|
||||
import type { SocietyVote } from '@polkadot/types/interfaces';
|
||||
|
||||
export interface MapMember {
|
||||
isFounder?: boolean;
|
||||
isHead?: boolean;
|
||||
isSkeptic?: boolean;
|
||||
isVoter?: boolean;
|
||||
key: string;
|
||||
member: DeriveSocietyMember;
|
||||
}
|
||||
|
||||
export interface OwnMembers {
|
||||
allMembers: string[];
|
||||
isMember: boolean;
|
||||
@ -16,3 +26,9 @@ export interface VoteSplit {
|
||||
allNay: VoteType[];
|
||||
allSkeptic: VoteType[];
|
||||
}
|
||||
|
||||
export interface Voters {
|
||||
candidates: DeriveSocietyCandidate[];
|
||||
skeptics: string[];
|
||||
voters: string[];
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
|
||||
export default function useCounter (): number {
|
||||
const { api } = useApi();
|
||||
const bids = useCall<Bid[]>(api.query.society?.bids);
|
||||
const bids = useCall<Bid[]>(api.query.society?.candidates);
|
||||
|
||||
return bids?.length || 0;
|
||||
}
|
||||
|
||||
54
packages/page-society/src/useVoters.ts
Normal file
54
packages/page-society/src/useVoters.ts
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright 2017-2021 @polkadot/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
import type { DeriveSocietyCandidate } from '@polkadot/api-derive/types';
|
||||
import type { Voters } from './types';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useApi, useCall, useEventTrigger } from '@polkadot/react-hooks';
|
||||
|
||||
const EMPTY: Voters = { candidates: [], skeptics: [], voters: [] };
|
||||
|
||||
async function getVoters (api: ApiPromise, candidates: DeriveSocietyCandidate[]): Promise<Voters> {
|
||||
const entries = await Promise.all(
|
||||
candidates.map(({ accountId }) => api.query.society.votes.entries(accountId))
|
||||
);
|
||||
const skeptics: string[] = [];
|
||||
const voters: string[] = [];
|
||||
|
||||
entries.forEach((list): void => {
|
||||
list.forEach(([{ args: [, accountId] }, opt]) => {
|
||||
if (opt.isSome) {
|
||||
const key = accountId.toString();
|
||||
const vote = opt.unwrap();
|
||||
|
||||
if (vote.isSkeptic) {
|
||||
!skeptics.includes(key) && skeptics.push(key);
|
||||
} else {
|
||||
!voters.includes(key) && voters.push(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return { candidates, skeptics, voters };
|
||||
}
|
||||
|
||||
export default function useVoters (): Voters {
|
||||
const { api } = useApi();
|
||||
const voteTrigger = useEventTrigger([api.events.society.Vote]);
|
||||
const candidates = useCall<DeriveSocietyCandidate[]>(api.derive.society.candidates);
|
||||
const [state, setState] = useState<Voters>(EMPTY);
|
||||
|
||||
useEffect((): void => {
|
||||
voteTrigger && candidates && (
|
||||
candidates.length
|
||||
? getVoters(api, candidates).then(setState).catch(console.error)
|
||||
: setState(EMPTY)
|
||||
);
|
||||
}, [api, candidates, voteTrigger]);
|
||||
|
||||
return state;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user