123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319 |
- // Copyright 2017-2019 @polkadot/ui-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.
- import { VoteIndex } from '@polkadot/types/interfaces';
- import { DerivedVoterPositions } from '@polkadot/api-derive/types';
- import { ApiProps } from '@polkadot/react-api/types';
- import { ComponentProps } from './types';
- 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 TxModal, { TxModalState, TxModalProps } from '@polkadot/react-components/TxModal';
- import translate from '../translate';
- interface Props extends ApiProps, ComponentProps, TxModalProps {
- voterPositions?: DerivedVoterPositions;
- }
- interface State extends TxModalState {
- approvals: boolean[] | null;
- oldApprovals: boolean[] | null;
- // voterPositions: DerivedVoterPositions;
- }
- const AlreadyVoted = styled.article`
- display: flex;
- align-items: center;
- margin: 0.5rem 0;
- & > :first-child {
- flex: 1 1;
- }
- & > :not(:first-child) {
- margin: 0;
- }
- `;
- const Candidates = styled.div`
- display: flex;
- flex-wrap: wrap;
- `;
- const Candidate = styled.div`
- cursor: pointer;
- width: 25rem;
- min-width: calc(50% - 1rem);
- border-radius: 0.5rem;
- border: 1px solid #eee;
- padding: 0.5rem;
- margin: 0.5rem;
- transition: all 0.2s;
- b {
- min-width: 5rem;
- }
- &.aye {
- background-color: rgba(0, 255, 0, 0.05);
- b {
- color: green;
- }
- }
- &.nay {
- background-color: rgba(0, 0, 0, 0.05);
- }
- `;
- class Vote extends TxModal<Props, State> {
- public static emptyApprovals (length: number): boolean[] {
- 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) {
- super(props);
- this.defaultState = {
- ...this.defaultState,
- approvals: null,
- oldApprovals: null
- };
- this.state = {
- ...this.defaultState
- };
- }
- 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 txParams = (): [boolean[] | null, VoteIndex, BN | null] => {
- const { electionsInfo: { nextVoterSet, voteCount }, voterPositions } = this.props;
- const { accountId, approvals } = this.state;
- return [
- approvals ? approvals.slice(0, 1 + approvals.lastIndexOf(true)) : [],
- createType('VoteIndex', voteCount),
- voterPositions && accountId && voterPositions[accountId]
- ? voterPositions[accountId].setIndex
- : nextVoterSet
- ];
- }
- protected isDisabled = (): boolean => {
- const { accountId, oldApprovals } = this.state;
- return !accountId || !!oldApprovals;
- }
- protected renderTrigger = (): React.ReactNode => {
- const { electionsInfo: { candidates }, t } = this.props;
- return (
- <Button
- isDisabled={candidates.length === 0}
- isPrimary
- label={t('Vote')}
- icon='check'
- onClick={this.showModal}
- />
- );
- }
- protected renderContent = (): React.ReactNode => {
- const { electionsInfo: { candidates }, voterPositions, t } = this.props;
- const { accountId, approvals, oldApprovals } = this.state;
- 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>
- )
- }
- <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)() }
- : {}
- )}
- >
- <AddressRow
- isInline
- value={accountId}
- >
- {this.renderToggle(index)}
- </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 fetchApprovals = (): void => {
- const { api, electionsInfo: { voteCount }, voterPositions } = this.props;
- const { accountId } = this.state;
- if (!accountId) {
- return;
- }
- api.derive.elections.approvalsOfAt(accountId, 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()
- });
- }
- });
- }
- protected onChangeAccount = (accountId: string | null): void => {
- this.setState({
- accountId,
- oldApprovals: null
- });
- }
- private onChangeVote = (index: number): (isChecked?: boolean) => void =>
- (isChecked?: boolean): void => {
- this.setState(({ approvals }: State): Pick<State, never> => {
- if (!approvals) {
- return {};
- }
- 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' }]
- )
- );
|