Skip to content

Commit

Permalink
Add multi-address selection component (#2028)
Browse files Browse the repository at this point in the history
  • Loading branch information
jacogr authored Dec 10, 2019
1 parent d1c8cff commit ef38822
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 210 deletions.
129 changes: 21 additions & 108 deletions packages/app-council/src/Overview/Vote.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,81 +10,28 @@ import { ComponentProps } from './types';
import BN from 'bn.js';
import React from 'react';
import { withApi } from '@polkadot/react-api';
import styled from 'styled-components';
import { AddressMini, Button, Toggle } from '@polkadot/react-components';
import { AddressMulti, Button } from '@polkadot/react-components';
import TxModal, { TxModalState, TxModalProps } from '@polkadot/react-components/TxModal';

import translate from '../translate';
import VoteValue from './VoteValue';

type VoteType = 'member' | 'runnerup' | 'candidate';

interface Props extends ApiProps, ComponentProps, TxModalProps {}

interface State extends TxModalState {
votes: Record<string, boolean>;
votes: string[];
voteValue: BN;
}

// const MAX_VOTES = 16;

const Candidates = styled.div`
display: flex;
flex-wrap: wrap;
justify-content: center;
.candidate {
border: 1px solid #eee;
border-radius: 0.25rem;
margin: 0.25rem;
padding: 0 0.5rem 0.25rem;
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;
}
}
`;
const MAX_VOTES = 16;

class Vote extends TxModal<Props, State> {
public static emptyApprovals (length: number): boolean[] {
return [...new Array(length).keys()].map((): boolean => false);
}

constructor (props: Props) {
super(props);

this.defaultState = {
...this.defaultState,
votes: {},
votes: [],
voteValue: new BN(0)
};

Expand All @@ -107,20 +54,13 @@ class Vote extends TxModal<Props, State> {
protected txParams = (): [boolean[] | null, VoteIndex, BN | null] | [string[], BN] => {
const { votes, voteValue } = this.state;

return [
Object
.entries(votes)
.filter(([, vote]): boolean => vote)
.map(([accountId]): string => accountId),
voteValue
];
return [votes, voteValue];
}

protected isDisabled = (): boolean => {
const { accountId, votes } = this.state;
const hasApprovals = Object.values(votes).some((vote): boolean => vote);

return !accountId || !hasApprovals;
return !accountId || votes.length === 0;
}

protected renderTrigger = (): React.ReactNode => {
Expand All @@ -144,44 +84,25 @@ class Vote extends TxModal<Props, State> {
protected renderContent = (): React.ReactNode => {
const { electionsInfo: { candidates, members, runnersUp }, t } = this.props;
const { accountId, votes } = this.state;
const _candidates = candidates.map((accountId): [AccountId, VoteType] => [accountId, 'candidate']);
const available = members
.map(([accountId]): [AccountId, VoteType] => [accountId, 'member'])
.concat(runnersUp.map(([accountId]): [AccountId, VoteType] => [accountId, 'runnerup']))
.concat(_candidates);
.map(([accountId]): string => accountId.toString())
.concat(runnersUp.map(([accountId]): string => accountId.toString()))
.concat(candidates.map((accountId): string => accountId.toString()));

return (
<>
<VoteValue
accountId={accountId}
onChange={this.setVoteValue}
/>
<Candidates>
{available.map(([accountId, type]): React.ReactNode => {
const key = accountId.toString();
const isAye = votes[key] || false;

return (
<AddressMini
className={`candidate ${isAye ? 'isAye' : 'isNay'} ${type}`}
key={key}
value={key}
>
<div className='candidate-right'>
<Toggle
label={
isAye
? t('Aye')
: t('Nay')
}
onChange={this.onChangeVote(key)}
value={isAye}
/>
</div>
</AddressMini>
);
})}
</Candidates>
<AddressMulti
available={available}
help={t('Filter available candidates based on name, address or short account index.')}
label={t('filter candidates')}
maxCount={MAX_VOTES}
onChange={this.onChangeVotes}
value={votes}
/>
</>
);
}
Expand All @@ -199,22 +120,14 @@ class Vote extends TxModal<Props, State> {
(api.query.electionsPhragmen || api.query.elections)
.votesOf<[AccountId[]] & Codec>(accountId)
.then(([existingVotes]): void => {
existingVotes.forEach((accountId): void => {
this.onChangeVote(accountId.toString())(true);
});
this.setState({ votes: existingVotes.map((accountId): string => accountId.toString()) });
});
}
}

private onChangeVote = (accountId: string): (isChecked: boolean) => void =>
(isChecked: boolean): void => {
this.setState(({ votes }: State): Pick<State, never> => ({
votes: {
...votes,
[accountId]: isChecked
}
}));
}
private onChangeVotes = (votes: string[]): void => {
this.setState({ votes });
}
}

export default translate(withApi(Vote));
119 changes: 38 additions & 81 deletions packages/app-staking/src/Actions/Account/Nominate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,72 +2,60 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import { DerivedStakingOverview } from '@polkadot/api-derive/types';
import { I18nProps } from '@polkadot/react-components/types';
import { KeyringSectionOption } from '@polkadot/ui-keyring/options/types';

import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { AddressMini, Button, InputAddress, Modal, Toggle, TxButton } from '@polkadot/react-components';
import { AddressMulti, Button, InputAddress, Modal, TxButton } from '@polkadot/react-components';
import { useFavorites } from '@polkadot/react-hooks';

import { STORE_FAVS_BASE } from '../../constants';
import translate from '../../translate';

interface Props extends I18nProps {
controllerId: string;
next: string[];
nominees?: string[];
onClose: () => void;
stakingOverview?: DerivedStakingOverview;
stashId: string;
stashOptions: KeyringSectionOption[];
}

// We only allow a maximum of 16 nominees, negative to slice
const MAX_NOMINEES = -16;
const MAX_NOMINEES = 16;

function Nominate ({ className, controllerId, nominees, onClose, stashId, stashOptions, t }: Props): React.ReactElement<Props> | null {
function Nominate ({ className, controllerId, nominees, onClose, next, stakingOverview, stashId, t }: Props): React.ReactElement<Props> | null {
const [favorites] = useFavorites(STORE_FAVS_BASE);
const [next, setNext] = useState<string[] | undefined>();
const [{ options, shortlist }, setShortlist] = useState<{ options: KeyringSectionOption[]; shortlist: string[] }>({ options: [], shortlist: [] });
const [validators, setValidators] = useState<string[]>([]);
const [selection, setSelection] = useState<string[]>([]);
const [available, setAvailable] = useState<string[]>([]);

useEffect((): void => {
if (!next && nominees) {
setNext(nominees);
if (!selection.length && nominees) {
setSelection(nominees);
}
}, [next, nominees]);
}, [selection, nominees]);

useEffect((): void => {
if (nominees) {
const _favorites = favorites.filter((accountId): boolean =>
stashOptions.some(({ value }): boolean => value === accountId)
);

const shortlist = [
// ensure that the favorite is included in the list of stashes
..._favorites,
// 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
});
if (stakingOverview) {
setValidators((stakingOverview.currentElected || []).map((acc): string => acc.toString()));
}
}, [favorites, nominees, stashOptions]);
}, [stakingOverview]);

const _onChangeNominees = (_nominees: string[]): void => {
const newNominees = _nominees.slice(MAX_NOMINEES);

if (JSON.stringify(newNominees) !== JSON.stringify(nominees)) {
setNext(newNominees);
}
};
const _onToggleNominee = (nominee: string): void =>
setNext(
(next || []).includes(nominee)
? (next || []).filter((accountId): boolean => accountId !== nominee)
: [...(next || []), nominee].slice(MAX_NOMINEES)
);
useEffect((): void => {
const shortlist = [
// ensure that the favorite is included in the list of stashes
...favorites.filter((acc): boolean => validators.includes(acc) || next.includes(acc)),
// make sure the nominee is not in our favorites already
...(nominees || []).filter((acc): boolean => !favorites.includes(acc))
];

setAvailable([
...shortlist,
...validators.filter((acc): boolean => !shortlist.includes(acc)),
...next.filter((acc): boolean => !shortlist.includes(acc))
]);
}, [favorites, next, nominees, validators]);

return (
<Modal
Expand All @@ -88,46 +76,15 @@ function Nominate ({ className, controllerId, nominees, onClose, stashId, stashO
isDisabled
label={t('stash account')}
/>
<InputAddress
<AddressMulti
available={available}
className='medium'
help={t('Stash accounts that are to be nominated. Block rewards are split between validators and nominators. Only 16 nominees will be taken into account.')}
isInput={false}
isMultiple
label={t('nominate the following addresses')}
onChangeMulti={_onChangeNominees}
options={options}
placeholder={t('select accounts(s) nominate')}
type='account'
value={next || []}
help={t('Filter available candidates based on name, address or short account index.')}
label={t('filter candidates')}
maxCount={MAX_NOMINEES}
onChange={setSelection}
value={selection}
/>
{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>
Expand All @@ -140,10 +97,10 @@ function Nominate ({ className, controllerId, nominees, onClose, stashId, stashO
<Button.Or />
<TxButton
accountId={controllerId}
isDisabled={!next || next.length === 0}
isDisabled={!selection.length}
isPrimary
onClick={onClose}
params={[next]}
params={[selection]}
label={t('Nominate')}
icon='hand paper outline'
tx='staking.nominate'
Expand Down
Loading

0 comments on commit ef38822

Please sign in to comment.