ghost-portal/packages/app-council/src/Overview/Vote.tsx
Jaco Greeff b4de745cda
Support latest api (with dynamic types) (#1447)
* Make a start, halfway down with issues found...

* Around  80% through untangling  the linter

* Seems linted...

* Adjust latest

* Update for latest (Moment -> u64)

* Method -> Call

* Bump api from npm

* formatNumber (no unwrap needed)

* Update to latest ui packages

* Revert internal version downgrades
2019-07-29 12:24:43 +02:00

320 lines
8.3 KiB
TypeScript

// 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/ui-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/ui-api';
import { AddressRow, Button, Icon, Toggle, TxButton } from '@polkadot/ui-app';
import TxModal, { TxModalState, TxModalProps } from '@polkadot/ui-app/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')}
labelIcon='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')}
labelIcon='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' }]
)
);