Add fav & current as nominations (#1869)
* Add fav & current as nominations * Toggle nominee at end * Selection icons for council
This commit is contained in:
parent
c27bed0857
commit
5ec79faa4a
@ -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
|
||||
>
|
||||
|
||||
@ -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
|
||||
>
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -31,6 +31,8 @@ interface State extends TxModalState {
|
||||
// voterPositions: DerivedVoterPositions;
|
||||
}
|
||||
|
||||
// const MAX_VOTES = 16;
|
||||
|
||||
// const AlreadyVoted = styled.article`
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
5
packages/app-staking/src/constants.ts
Normal file
5
packages/app-staking/src/constants.ts
Normal 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';
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user