|
@@ -2,7 +2,8 @@
|
|
|
// This software may be modified and distributed under the terms
|
|
|
// of the Apache-2.0 license. See the LICENSE file for details.
|
|
|
|
|
|
-import { VoteIndex } from '@polkadot/types/interfaces';
|
|
|
+import { AccountId, VoteIndex } from '@polkadot/types/interfaces';
|
|
|
+import { Codec } from '@polkadot/types/types';
|
|
|
import { DerivedVoterPositions } from '@polkadot/api-derive/types';
|
|
|
import { ApiProps } from '@polkadot/react-api/types';
|
|
|
import { ComponentProps } from './types';
|
|
@@ -11,35 +12,36 @@ import BN from 'bn.js';
|
|
|
import React from 'react';
|
|
|
import styled from 'styled-components';
|
|
|
import { createType } from '@polkadot/types';
|
|
|
-import { withApi, withCalls, withMulti } from '@polkadot/react-api';
|
|
|
-import { AddressRow, Button, Icon, Toggle, TxButton } from '@polkadot/react-components';
|
|
|
+import { withCalls, withMulti } from '@polkadot/react-api';
|
|
|
+import { AddressRow, Button, Toggle } from '@polkadot/react-components';
|
|
|
import TxModal, { TxModalState, TxModalProps } from '@polkadot/react-components/TxModal';
|
|
|
|
|
|
import translate from '../translate';
|
|
|
+import VoteValue from './VoteValue';
|
|
|
|
|
|
interface Props extends ApiProps, ComponentProps, TxModalProps {
|
|
|
voterPositions?: DerivedVoterPositions;
|
|
|
}
|
|
|
|
|
|
interface State extends TxModalState {
|
|
|
- approvals: boolean[] | null;
|
|
|
- oldApprovals: boolean[] | null;
|
|
|
+ votes: Record<string, boolean>;
|
|
|
+ voteValue: BN;
|
|
|
// voterPositions: DerivedVoterPositions;
|
|
|
}
|
|
|
|
|
|
-const AlreadyVoted = styled.article`
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- margin: 0.5rem 0;
|
|
|
+// const AlreadyVoted = styled.article`
|
|
|
+// display: flex;
|
|
|
+// align-items: center;
|
|
|
+// margin: 0.5rem 0;
|
|
|
|
|
|
- & > :first-child {
|
|
|
- flex: 1 1;
|
|
|
- }
|
|
|
+// & > :first-child {
|
|
|
+// flex: 1 1;
|
|
|
+// }
|
|
|
|
|
|
- & > :not(:first-child) {
|
|
|
- margin: 0;
|
|
|
- }
|
|
|
-`;
|
|
|
+// & > :not(:first-child) {
|
|
|
+// margin: 0;
|
|
|
+// }
|
|
|
+// `;
|
|
|
|
|
|
const Candidates = styled.div`
|
|
|
display: flex;
|
|
@@ -52,8 +54,8 @@ const Candidate = styled.div`
|
|
|
min-width: calc(50% - 1rem);
|
|
|
border-radius: 0.5rem;
|
|
|
border: 1px solid #eee;
|
|
|
- padding: 0.5rem;
|
|
|
- margin: 0.5rem;
|
|
|
+ padding: 0.75rem 0.5rem 0.25rem;
|
|
|
+ margin: 0.25rem;
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
b {
|
|
@@ -71,6 +73,11 @@ const Candidate = styled.div`
|
|
|
&.nay {
|
|
|
background-color: rgba(0, 0, 0, 0.05);
|
|
|
}
|
|
|
+
|
|
|
+ .ui--Row-children {
|
|
|
+ text-align: right;
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
`;
|
|
|
|
|
|
class Vote extends TxModal<Props, State> {
|
|
@@ -78,35 +85,13 @@ class Vote extends TxModal<Props, State> {
|
|
|
return [...new Array(length).keys()].map((): boolean => false);
|
|
|
}
|
|
|
|
|
|
- public static getDerivedStateFromProps ({ electionsInfo: { candidateCount } }: Props, { approvals }: State): Partial<State> {
|
|
|
- const state: Partial<State> = {};
|
|
|
- // if (voterPositions) {
|
|
|
- // state.voters = Object.keys(voterSets).reduce(
|
|
|
- // (result: Record<string, VoterPosition>, accountId, globalIndex): Record<string, VoterPosition> => {
|
|
|
- // result[accountId] = {
|
|
|
- // setIndex: voterSets[accountId],
|
|
|
- // globalIndex: new BN(globalIndex)
|
|
|
- // };
|
|
|
- // return result;
|
|
|
- // },
|
|
|
- // {}
|
|
|
- // );
|
|
|
- // }
|
|
|
-
|
|
|
- if (candidateCount && !approvals) {
|
|
|
- state.approvals = state.oldApprovals || Vote.emptyApprovals(candidateCount.toNumber());
|
|
|
- }
|
|
|
-
|
|
|
- return state;
|
|
|
- }
|
|
|
-
|
|
|
- public constructor (props: Props) {
|
|
|
+ constructor (props: Props) {
|
|
|
super(props);
|
|
|
|
|
|
this.defaultState = {
|
|
|
...this.defaultState,
|
|
|
- approvals: null,
|
|
|
- oldApprovals: null
|
|
|
+ votes: {},
|
|
|
+ voteValue: new BN(0)
|
|
|
};
|
|
|
|
|
|
this.state = {
|
|
@@ -114,51 +99,60 @@ class Vote extends TxModal<Props, State> {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
- public componentDidMount (): void {
|
|
|
- this.fetchApprovals();
|
|
|
- }
|
|
|
-
|
|
|
- public componentDidUpdate (_: Props, prevState: State): void {
|
|
|
- const { accountId } = this.state;
|
|
|
-
|
|
|
- if (accountId !== prevState.accountId) {
|
|
|
- this.fetchApprovals();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
protected headerText = (): string => this.props.t('Vote for current candidates');
|
|
|
|
|
|
protected accountLabel = (): string => this.props.t('Voting account');
|
|
|
|
|
|
protected accountHelp = (): string => this.props.t('This account will be use to approve or disapprove each candidate.');
|
|
|
|
|
|
- protected txMethod = (): string => 'elections.setApprovals';
|
|
|
+ protected txMethod = (): string =>
|
|
|
+ this.props.api.tx.electionsPhragmen
|
|
|
+ ? 'electionsPhragmen.vote'
|
|
|
+ : 'elections.setApprovals';
|
|
|
|
|
|
- protected txParams = (): [boolean[] | null, VoteIndex, BN | null] => {
|
|
|
- const { electionsInfo: { nextVoterSet, voteCount }, voterPositions } = this.props;
|
|
|
- const { accountId, approvals } = this.state;
|
|
|
+ protected txParams = (): [boolean[] | null, VoteIndex, BN | null] | [string[], BN] => {
|
|
|
+ const { api, electionsInfo: { candidates, nextVoterSet, voteCount }, voterPositions } = this.props;
|
|
|
+ const { accountId, votes, voteValue } = this.state;
|
|
|
+
|
|
|
+ if (api.tx.electionsPhragmen) {
|
|
|
+ return [
|
|
|
+ Object.entries(votes).filter(([, vote]): boolean => vote).map(([accountId]): string => accountId),
|
|
|
+ voteValue
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ const approvals = candidates.map((accountId): boolean => votes[accountId.toString()] === true);
|
|
|
|
|
|
return [
|
|
|
- approvals ? approvals.slice(0, 1 + approvals.lastIndexOf(true)) : [],
|
|
|
+ approvals
|
|
|
+ ? approvals.slice(0, 1 + approvals.lastIndexOf(true))
|
|
|
+ : [],
|
|
|
createType('VoteIndex', voteCount),
|
|
|
voterPositions && accountId && voterPositions[accountId]
|
|
|
? voterPositions[accountId].setIndex
|
|
|
- : nextVoterSet
|
|
|
+ : nextVoterSet || null
|
|
|
];
|
|
|
}
|
|
|
|
|
|
protected isDisabled = (): boolean => {
|
|
|
- const { accountId, oldApprovals } = this.state;
|
|
|
+ const { accountId, votes } = this.state;
|
|
|
+ const hasApprovals = Object.values(votes).some((vote): boolean => vote);
|
|
|
|
|
|
- return !accountId || !!oldApprovals;
|
|
|
+ return !accountId || !hasApprovals;
|
|
|
}
|
|
|
|
|
|
protected renderTrigger = (): React.ReactNode => {
|
|
|
- const { electionsInfo: { candidates }, t } = this.props;
|
|
|
+ const { api, electionsInfo: { candidates, members, runnersUp }, t } = this.props;
|
|
|
+ const available = api.tx.electionsPhragmen
|
|
|
+ ? members
|
|
|
+ .map(([accountId]): AccountId => accountId)
|
|
|
+ .concat(runnersUp.map(([accountId]): AccountId => accountId))
|
|
|
+ .concat(candidates)
|
|
|
+ : candidates;
|
|
|
|
|
|
return (
|
|
|
<Button
|
|
|
- isDisabled={candidates.length === 0}
|
|
|
+ isDisabled={available.length === 0}
|
|
|
isPrimary
|
|
|
label={t('Vote')}
|
|
|
icon='check'
|
|
@@ -168,102 +162,85 @@ class Vote extends TxModal<Props, State> {
|
|
|
}
|
|
|
|
|
|
protected renderContent = (): React.ReactNode => {
|
|
|
- const { electionsInfo: { candidates }, voterPositions, t } = this.props;
|
|
|
- const { accountId, approvals, oldApprovals } = this.state;
|
|
|
+ const { api, electionsInfo: { candidates, members, runnersUp }, t } = this.props;
|
|
|
+ const { accountId, votes } = this.state;
|
|
|
+ const _candidates = candidates.map((accountId): [AccountId, boolean] => [accountId, false]);
|
|
|
+ const available = api.tx.electionsPhragmen
|
|
|
+ ? members
|
|
|
+ .map(([accountId]): [AccountId, boolean] => [accountId, true])
|
|
|
+ .concat(runnersUp.map(([accountId]): [AccountId, boolean] => [accountId, false]))
|
|
|
+ .concat(_candidates)
|
|
|
+ : _candidates;
|
|
|
|
|
|
return (
|
|
|
<>
|
|
|
- {
|
|
|
- (oldApprovals && accountId && voterPositions && voterPositions[accountId]) && (
|
|
|
- <AlreadyVoted className='warning padded'>
|
|
|
- <div>
|
|
|
- <Icon name='warning sign' />
|
|
|
- {t('You have already voted in this round')}
|
|
|
- </div>
|
|
|
- <Button.Group>
|
|
|
- <TxButton
|
|
|
- accountId={accountId}
|
|
|
- isNegative
|
|
|
- label={t('Retract vote')}
|
|
|
- icon='delete'
|
|
|
- onSuccess={this.onRetractVote}
|
|
|
- params={[voterPositions[accountId].globalIndex]}
|
|
|
- tx='elections.retractVoter'
|
|
|
- />
|
|
|
- </Button.Group>
|
|
|
- </AlreadyVoted>
|
|
|
- )
|
|
|
- }
|
|
|
+ {api.tx.electionsPhragmen && (
|
|
|
+ <VoteValue
|
|
|
+ accountId={accountId}
|
|
|
+ onChange={this.setVoteValue}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ {/* {(oldApprovals && accountId && voterPositions && voterPositions[accountId]) && (
|
|
|
+ <AlreadyVoted className='warning padded'>
|
|
|
+ <div>
|
|
|
+ <Icon name='warning sign' />
|
|
|
+ {t('You have already voted in this round')}
|
|
|
+ </div>
|
|
|
+ <Button.Group>
|
|
|
+ <TxButton
|
|
|
+ accountId={accountId}
|
|
|
+ isNegative
|
|
|
+ label={t('Retract vote')}
|
|
|
+ icon='delete'
|
|
|
+ onSuccess={this.onRetractVote}
|
|
|
+ params={[voterPositions[accountId].globalIndex]}
|
|
|
+ tx='elections.retractVoter'
|
|
|
+ />
|
|
|
+ </Button.Group>
|
|
|
+ </AlreadyVoted>
|
|
|
+ )} */}
|
|
|
<Candidates>
|
|
|
- {
|
|
|
- candidates.map((accountId, index): React.ReactNode => {
|
|
|
- if (!approvals) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- const { [index]: isAye } = approvals;
|
|
|
- return (
|
|
|
- <Candidate
|
|
|
- className={isAye ? 'aye' : 'nay'}
|
|
|
- key={accountId.toString()}
|
|
|
- {...(
|
|
|
- !oldApprovals
|
|
|
- ? { onClick: (): void => this.onChangeVote(index)() }
|
|
|
- : {}
|
|
|
- )}
|
|
|
+ {available.map(([accountId, isMember]): React.ReactNode => {
|
|
|
+ const key = accountId.toString();
|
|
|
+ const isAye = votes[key] || false;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Candidate
|
|
|
+ className={isAye ? 'aye' : 'nay'}
|
|
|
+ key={key}
|
|
|
+ >
|
|
|
+ <AddressRow
|
|
|
+ defaultName={isMember ? t('member') : t('candidate')}
|
|
|
+ isInline
|
|
|
+ value={accountId}
|
|
|
+ withIndexOrAddress
|
|
|
>
|
|
|
- <AddressRow
|
|
|
- isInline
|
|
|
- value={accountId}
|
|
|
- >
|
|
|
- {this.renderToggle(index)}
|
|
|
- </AddressRow>
|
|
|
- </Candidate>
|
|
|
- );
|
|
|
- })
|
|
|
- }
|
|
|
+ <Toggle
|
|
|
+ label={
|
|
|
+ isAye
|
|
|
+ ? t('Aye')
|
|
|
+ : t('Nay')
|
|
|
+ }
|
|
|
+ onChange={this.onChangeVote(key)}
|
|
|
+ value={isAye}
|
|
|
+ />
|
|
|
+ </AddressRow>
|
|
|
+ </Candidate>
|
|
|
+ );
|
|
|
+ })}
|
|
|
</Candidates>
|
|
|
</>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- private renderToggle = (index: number): React.ReactNode => {
|
|
|
- const { t } = this.props;
|
|
|
- const { approvals, oldApprovals } = this.state;
|
|
|
-
|
|
|
- if (!approvals) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- const { [index]: bool } = approvals;
|
|
|
-
|
|
|
- return (
|
|
|
- <Toggle
|
|
|
- isDisabled={!!oldApprovals}
|
|
|
- label={
|
|
|
- bool
|
|
|
- ? (
|
|
|
- <b>{t('Aye')}</b>
|
|
|
- )
|
|
|
- : (
|
|
|
- <b>{t('No vote')}</b>
|
|
|
- )
|
|
|
- }
|
|
|
- value={bool}
|
|
|
- />
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- private emptyApprovals = (): boolean[] => {
|
|
|
- const { electionsInfo: { candidateCount } } = this.props;
|
|
|
-
|
|
|
- return Vote.emptyApprovals(candidateCount.toNumber());
|
|
|
+ private setVoteValue = (voteValue?: BN): void => {
|
|
|
+ this.setState({ voteValue: voteValue || new BN(0) });
|
|
|
}
|
|
|
|
|
|
- private fetchApprovals = (): void => {
|
|
|
- const { api, electionsInfo: { voteCount }, voterPositions } = this.props;
|
|
|
- const { accountId } = this.state;
|
|
|
+ private fetchApprovals = (accountId: string | null): void => {
|
|
|
+ const { api, electionsInfo: { candidates, voteCount } } = this.props;
|
|
|
|
|
|
- if (!accountId) {
|
|
|
+ if (!accountId || !voteCount) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
@@ -271,50 +248,55 @@ class Vote extends TxModal<Props, State> {
|
|
|
api.derive.elections
|
|
|
.approvalsOfAt(accountId as any, voteCount)
|
|
|
.then((approvals: boolean[]): void => {
|
|
|
- if ((voterPositions && voterPositions[accountId.toString()]) && approvals && approvals.length && approvals !== this.state.approvals) {
|
|
|
- this.setState({
|
|
|
- approvals,
|
|
|
- oldApprovals: approvals
|
|
|
- });
|
|
|
- } else {
|
|
|
- this.setState({
|
|
|
- approvals: this.emptyApprovals()
|
|
|
- });
|
|
|
- }
|
|
|
+ this.setState({
|
|
|
+ votes: candidates.reduce((votes: Record<string, boolean>, accountId, index): Record<string, boolean> => ({
|
|
|
+ ...votes,
|
|
|
+ [accountId.toString()]: approvals[index] || false
|
|
|
+ }), {})
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private fetchVotes = (accountId: string | null): void => {
|
|
|
+ const { api } = this.props;
|
|
|
+
|
|
|
+ if (!accountId || !api.tx.electionsPhragmen) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ api.query.electionsPhragmen
|
|
|
+ .votesOf<[AccountId[]] & Codec>(accountId)
|
|
|
+ .then(([existingVotes]): void => {
|
|
|
+ existingVotes.forEach((accountId): void => {
|
|
|
+ this.onChangeVote(accountId.toString())(true);
|
|
|
+ });
|
|
|
});
|
|
|
}
|
|
|
|
|
|
protected onChangeAccount = (accountId: string | null): void => {
|
|
|
- this.setState({
|
|
|
- accountId,
|
|
|
- oldApprovals: null
|
|
|
- });
|
|
|
+ const { api } = this.props;
|
|
|
+
|
|
|
+ this.setState({ accountId });
|
|
|
+
|
|
|
+ api.tx.electionsPhragmen
|
|
|
+ ? this.fetchVotes(accountId)
|
|
|
+ : this.fetchApprovals(accountId);
|
|
|
}
|
|
|
|
|
|
- private onChangeVote = (index: number): (isChecked?: boolean) => void =>
|
|
|
- (isChecked?: boolean): void => {
|
|
|
- this.setState(({ approvals }: State): Pick<State, never> => {
|
|
|
- if (!approvals) {
|
|
|
- return {};
|
|
|
+ private onChangeVote = (accountId: string): (isChecked: boolean) => void =>
|
|
|
+ (isChecked: boolean): void => {
|
|
|
+ this.setState(({ votes }: State): Pick<State, never> => ({
|
|
|
+ votes: {
|
|
|
+ ...votes,
|
|
|
+ [accountId]: isChecked
|
|
|
}
|
|
|
- return {
|
|
|
- approvals: approvals.map((b, i): boolean => i === index ? isChecked || !approvals[index] : b)
|
|
|
- };
|
|
|
- });
|
|
|
+ }));
|
|
|
}
|
|
|
-
|
|
|
- private onRetractVote = (): void => {
|
|
|
- this.setState({
|
|
|
- approvals: this.emptyApprovals(),
|
|
|
- oldApprovals: null
|
|
|
- });
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
export default withMulti(
|
|
|
Vote,
|
|
|
translate,
|
|
|
- withApi,
|
|
|
withCalls<Props>(
|
|
|
['derive.elections.voterPositions', { propName: 'voterPositions' }]
|
|
|
)
|