Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multi-address selection component #2028

Merged
merged 1 commit into from
Dec 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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