Add fav & current as nominations (#1869)

* Add fav & current as nominations

* Toggle nominee at end

* Selection icons for council
This commit is contained in:
Jaco Greeff 2019-11-11 23:35:00 +01:00 committed by GitHub
parent c27bed0857
commit 5ec79faa4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 182 additions and 51 deletions

View File

@ -6,7 +6,7 @@ import { I18nProps } from '@polkadot/react-components/types';
import { AccountId } from '@polkadot/types/interfaces';
import React from 'react';
import { AddressCard } from '@polkadot/react-components';
import { AddressCard, Badge, Icon } from '@polkadot/react-components';
import translate from '../translate';
import Voters from './Voters';
@ -21,6 +21,14 @@ function Candidate ({ address, isRunnerUp, t, voters }: Props): React.ReactEleme
return (
<AddressCard
defaultName={isRunnerUp ? t('runner up') : t('candidate')}
iconInfo={isRunnerUp && (
<Badge
hover={t('Runner up')}
info={<Icon name='chevron down' />}
isTooltip
type='runnerup'
/>
)}
value={address}
withIndexOrAddress
>

View File

@ -6,7 +6,7 @@ import { I18nProps } from '@polkadot/react-components/types';
import { AccountId } from '@polkadot/types/interfaces';
import React from 'react';
import { AddressCard } from '@polkadot/react-components';
import { AddressCard, Badge, Icon } from '@polkadot/react-components';
import translate from '../translate';
import Voters from './Voters';
@ -20,6 +20,14 @@ function Member ({ address, t, voters }: Props): React.ReactElement<Props> {
return (
<AddressCard
defaultName={t('council member')}
iconInfo={
<Badge
hover={t('Current member')}
info={<Icon name='check' />}
isTooltip
type='selected'
/>
}
value={address}
withIndexOrAddress
>

View File

@ -24,8 +24,11 @@ function Summary ({ bestNumber, electionsInfo: { members, candidateCount, desire
<CardSummary label={t('seats')}>
{formatNumber(members.length)}/{formatNumber(desiredSeats)}
</CardSummary>
<CardSummary label={t('runners up')}>
{formatNumber(runnersUp.length)}
</CardSummary>
<CardSummary label={t('candidates')}>
{formatNumber(candidateCount.addn(runnersUp.length))}
{formatNumber(candidateCount)}
</CardSummary>
</section>
{voteCount && (

View File

@ -31,6 +31,8 @@ interface State extends TxModalState {
// voterPositions: DerivedVoterPositions;
}
// const MAX_VOTES = 16;
// const AlreadyVoted = styled.article`
// display: flex;
// align-items: center;

View File

@ -5,14 +5,16 @@
import { I18nProps } from '@polkadot/react-components/types';
import { KeyringSectionOption } from '@polkadot/ui-keyring/options/types';
import React, { useState } from 'react';
import { Button, InputAddress, Modal, TxButton } from '@polkadot/react-components';
import React, { useEffect, useState } from 'react';
import store from 'store';
import styled from 'styled-components';
import { AddressMini, Button, InputAddress, Modal, Toggle, TxButton } from '@polkadot/react-components';
import { STORE_FAVS } from '../../constants';
import translate from '../../translate';
interface Props extends I18nProps {
controllerId: string;
isOpen: boolean;
nominees?: string[];
onClose: () => void;
stashId: string;
@ -22,27 +24,52 @@ interface Props extends I18nProps {
// We only allow a maximum of 16 nominees, negative to slice
const MAX_NOMINEES = -16;
function Nominate ({ controllerId, isOpen, onClose, stashId, stashOptions, t }: Props): React.ReactElement<Props> | null {
const [nominees, setNominees] = useState<string[]>([]);
function Nominate ({ className, controllerId, nominees, onClose, stashId, stashOptions, t }: Props): React.ReactElement<Props> | null {
const [favorites] = useState<string[]>(store.get(STORE_FAVS, []));
const [next, setNext] = useState<string[] | undefined>();
const [{ options, shortlist }, setShortlist] = useState<{ options: KeyringSectionOption[]; shortlist: string[] }>({ options: [], shortlist: [] });
if (!isOpen) {
return null;
}
useEffect((): void => {
if (!next && nominees) {
setNext(nominees);
}
}, [next, nominees]);
useEffect((): void => {
if (nominees) {
const shortlist = [
// ensure that the favorite is included in the list of stashes
...favorites.filter((accountId): boolean => stashOptions.some(({ value }): boolean => value === accountId)),
// make sure the nominee is not in our favorites already
...nominees.filter((accountId): boolean => !favorites.includes(accountId))
];
setShortlist({
options: stashOptions.filter(({ value }): boolean => !shortlist.includes(value as string)),
shortlist
});
}
}, [favorites, nominees, stashOptions]);
const _onChangeNominees = (_nominees: string[]): void => {
const newNominees = _nominees.slice(MAX_NOMINEES);
if (JSON.stringify(newNominees) !== JSON.stringify(nominees)) {
setNominees(newNominees);
setNext(newNominees);
}
};
const _onToggleNominee = (nominee: string): void =>
setNext(
(next || []).includes(nominee)
? (next || []).filter((accountId): boolean => accountId !== nominee)
: [...(next || []), nominee].slice(MAX_NOMINEES)
);
return (
<Modal
className='staking--Nominating'
className={`staking--Nominating ${className}`}
dimmer='inverted'
open
size='small'
>
<Modal.Header>
{t('Nominate Validators')}
@ -67,11 +94,39 @@ function Nominate ({ controllerId, isOpen, onClose, stashId, stashOptions, t }:
isMultiple
label={t('nominate the following addresses')}
onChangeMulti={_onChangeNominees}
options={stashOptions}
options={options}
placeholder={t('select accounts(s) nominate')}
type='account'
value={nominees}
value={next || []}
/>
{shortlist.length !== 0 && (
<div className='shortlist'>
{shortlist.map((address): React.ReactNode => {
const isAye = next?.includes(address);
const _onChange = (): void => _onToggleNominee(address);
return (
<AddressMini
className={`candidate ${isAye ? 'isAye' : 'isNay'}`}
key={address}
value={address}
>
<div className='candidate-right'>
<Toggle
label={
isAye
? t('Aye')
: t('Nay')
}
onChange={_onChange}
value={isAye}
/>
</div>
</AddressMini>
);
})}
</div>
)}
</Modal.Content>
<Modal.Actions>
<Button.Group>
@ -84,10 +139,10 @@ function Nominate ({ controllerId, isOpen, onClose, stashId, stashOptions, t }:
<Button.Or />
<TxButton
accountId={controllerId}
isDisabled={nominees.length === 0}
isDisabled={!next || next.length === 0}
isPrimary
onClick={onClose}
params={[nominees]}
params={[next]}
label={t('Nominate')}
icon='hand paper outline'
tx='staking.nominate'
@ -98,4 +153,53 @@ function Nominate ({ controllerId, isOpen, onClose, stashId, stashOptions, t }:
);
}
export default translate(Nominate);
export default translate(
styled(Nominate)`
.shortlist {
display: flex;
flex-wrap: wrap;
justify-content: center;
.candidate {
border: 1px solid #eee;
border-radius: 0.25rem;
margin: 0.25rem;
padding-bottom: 0.25rem;
padding-right: 0.5rem;
position: relative;
&::after {
content: '';
position: absolute;
top: 0;
right: 0;
border-color: transparent;
border-style: solid;
border-radius: 0.25em;
border-width: 0.25em;
}
&.isAye {
background: #fff;
border-color: #ccc;
}
&.member::after {
border-color: green;
}
&.runnerup::after {
border-color: steelblue;
}
.ui--AddressMini-icon {
z-index: 1;
}
.candidate-right {
text-align: right;
}
}
}
`
);

View File

@ -252,14 +252,13 @@ class Account extends React.PureComponent<Props, State> {
const { stashOptions } = this.props;
const { controllerId, isNominateOpen, nominees, stashId } = this.state;
if (!stashId || !controllerId) {
if (!isNominateOpen || !stashId || !controllerId) {
return null;
}
return (
<Nominate
controllerId={controllerId}
isOpen={isNominateOpen}
nominees={nominees}
onClose={this.toggleNominate}
stashId={stashId}

View File

@ -4,7 +4,7 @@
import { DerivedHeartbeats, DerivedStakingOverview } from '@polkadot/api-derive/types';
import { I18nProps } from '@polkadot/react-components/types';
import { AccountId } from '@polkadot/types/interfaces';
import { AccountId, EraPoints, Points } from '@polkadot/types/interfaces';
import { ValidatorFilter } from '../types';
import React, { useContext, useEffect, useState } from 'react';
@ -13,6 +13,7 @@ import { Columar, Column, Dropdown, FilterOverlay } from '@polkadot/react-compon
import store from 'store';
import keyring from '@polkadot/ui-keyring';
import { STORE_FAVS } from '../constants';
import translate from '../translate';
import Address from './Address';
@ -24,31 +25,31 @@ interface Props extends I18nProps {
stakingOverview?: DerivedStakingOverview;
}
type AccountExtend = [string, boolean, boolean, number];
type AccountExtend = [string, boolean, boolean, Points?];
const STORE_FAVS = 'staking:favorites';
function filterAccounts (accounts: string[] = [], elected: string[], favorites: string[], without: string[], noPoints = false): AccountExtend[] {
function filterAccounts (accounts: string[] = [], elected: string[], favorites: string[], without: string[], eraPoints?: EraPoints): AccountExtend[] {
return accounts
.filter((accountId): boolean => !without.includes(accountId as any))
.sort((a, b): number => {
const aIdx = favorites.includes(a);
const bIdx = favorites.includes(b);
const isFavA = favorites.includes(a);
const isFavB = favorites.includes(b);
return aIdx === bIdx
return isFavA === isFavB
? 0
: aIdx
? -1
: 1;
: (isFavA ? -1 : 1);
})
.map((accountId): AccountExtend => [
accountId,
elected.includes(accountId),
favorites.includes(accountId),
noPoints
? -1
: elected.indexOf(accountId)
]);
.map((accountId): AccountExtend => {
const electedIdx = elected.indexOf(accountId);
return [
accountId,
elected.includes(accountId),
favorites.includes(accountId),
electedIdx !== -1
? eraPoints?.individual[electedIdx]
: undefined
];
});
}
function accountsToString (accounts: AccountId[]): string[] {
@ -66,13 +67,13 @@ function CurrentList ({ authorsMap, lastAuthors, next, recentlyOnline, stakingOv
if (stakingOverview) {
const _elected = accountsToString(stakingOverview.currentElected);
const _validators = accountsToString(stakingOverview.validators);
const validators = filterAccounts(_validators, _elected, favorites, []);
const elected = isSubstrateV2 ? filterAccounts(_elected, _elected, favorites, _validators, true) : [];
const validators = filterAccounts(_validators, _elected, favorites, [], stakingOverview.eraPoints);
const elected = isSubstrateV2 ? filterAccounts(_elected, _elected, favorites, _validators) : [];
setFiltered({
elected,
validators,
waiting: filterAccounts(next, [], favorites, _elected, true)
waiting: filterAccounts(next, [], favorites, _elected)
});
}
}, [favorites, next, stakingOverview]);
@ -88,7 +89,7 @@ function CurrentList ({ authorsMap, lastAuthors, next, recentlyOnline, stakingOv
);
const _renderColumn = (addresses: AccountExtend[], defaultName: string, withOnline: boolean): React.ReactNode =>
addresses.map(([address, isElected, isFavorite, pointIndex]): React.ReactNode => (
addresses.map(([address, isElected, isFavorite, points]): React.ReactNode => (
<Address
address={address}
authorsMap={authorsMap}
@ -100,11 +101,7 @@ function CurrentList ({ authorsMap, lastAuthors, next, recentlyOnline, stakingOv
key={address}
myAccounts={myAccounts}
onFavorite={_onFavorite}
points={
pointIndex !== -1
? stakingOverview?.eraPoints.individual[pointIndex]
: undefined
}
points={points}
recentlyOnline={
withOnline
? recentlyOnline

View File

@ -0,0 +1,5 @@
// Copyright 2017-2019 @polkadot/app-staking authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
export const STORE_FAVS = 'staking:favorites';

View File

@ -12,7 +12,7 @@ interface Props {
hover: React.ReactNode;
info: React.ReactNode;
isTooltip?: boolean;
type: 'online' | 'offline' | 'next';
type: 'online' | 'offline' | 'next' | 'runnerup' | 'selected';
}
let badgeId = 0;
@ -71,7 +71,12 @@ export default styled(Badge)`
background: red;
}
&.online {
&.runnerup {
background: brown;
}
&.online,
&.selected {
background: green;
}