Vote for council candidates (#1427)

* Change council to collective/elections

* fix proposal type

* package

* Submit candidacy

* update index

* Minor eslint cleanups

* rename back to council

* blank line

* council voting

* interface

* Cleanup

* tslint

* retract vote

* travis ci

* rv console

* SetIndex type

* types.ts SetIndex import

* bump api & fix retractVoter
This commit is contained in:
kwingram25 2019-07-26 11:21:05 +01:00 committed by Jaco Greeff
parent 64e2f8e99c
commit 43cb425b56
14 changed files with 418 additions and 105 deletions

View File

@ -14,9 +14,9 @@ import { withApi, withCalls, withMulti } from '@polkadot/ui-api';
import translate from '../translate';
type Props = TxModalProps & ApiProps & {
interface Props extends TxModalProps, ApiProps {
memberCount: number;
};
}
interface State extends TxModalState {
method: Method | null;

View File

@ -2,11 +2,13 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AccountId } from '@polkadot/types';
import React from 'react';
import { AddressCard } from '@polkadot/ui-app';
interface Props {
address: string;
address: AccountId;
}
export default class Candidate extends React.PureComponent<Props> {

View File

@ -2,9 +2,9 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { BlockNumber } from '@polkadot/types';
import { I18nProps } from '@polkadot/ui-app/types';
import BN from 'bn.js';
import React from 'react';
import { AddressCard } from '@polkadot/ui-app';
import { formatNumber } from '@polkadot/util';
@ -13,7 +13,7 @@ import translate from '../translate';
interface Props extends I18nProps {
address: string;
block: BN;
block: BlockNumber;
}
class Member extends React.PureComponent<Props> {

View File

@ -4,7 +4,7 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { I18nProps } from '@polkadot/ui-app/types';
import { ElectionsInfo } from './types';
import { ComponentProps } from './types';
import React from 'react';
import { Columar, Column } from '@polkadot/ui-app';
@ -13,9 +13,7 @@ import translate from '../translate';
import Candidate from './Candidate';
import Member from './Member';
interface Props extends I18nProps {
electionsInfo: ElectionsInfo;
}
interface Props extends I18nProps, ComponentProps {}
class Members extends React.PureComponent<Props> {
public render (): React.ReactNode {
@ -28,7 +26,7 @@ class Members extends React.PureComponent<Props> {
emptyText={t('No members found')}
headerText={t('members')}
>
{members.map(([address, block]): React.ReactNode => (
{Object.entries(members).map(([address, block]): React.ReactNode => (
<Member
address={address}
block={block}
@ -43,7 +41,7 @@ class Members extends React.PureComponent<Props> {
{candidates.map((address): React.ReactNode => (
<Candidate
address={address}
key={address}
key={address.toString()}
/>
))}
</Column>

View File

@ -2,20 +2,16 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { ElectionsInfo } from './types';
import { ComponentProps } from './types';
import BN from 'bn.js';
import React from 'react';
import { Button } from '@polkadot/ui-app';
import TxModal, { TxModalState, TxModalProps } from '@polkadot/ui-app/TxModal';
import TxModal, { TxModalState as State, TxModalProps } from '@polkadot/ui-app/TxModal';
import translate from '../translate';
interface Props extends TxModalProps {
electionsInfo: ElectionsInfo;
}
type State = TxModalState;
interface Props extends ComponentProps, TxModalProps {}
class SubmitCandidacy extends TxModal<Props, State> {
protected headerText = (): string => this.props.t('Submit your council candidacy');
@ -44,14 +40,11 @@ class SubmitCandidacy extends TxModal<Props, State> {
const { t } = this.props;
return (
<Button.Group>
<Button
isPrimary
label={t('Submit candidacy')}
labelIcon='add'
onClick={this.showModal}
/>
</Button.Group>
<Button
label={t('Submit candidacy')}
labelIcon='add'
onClick={this.showModal}
/>
);
}
}

View File

@ -4,7 +4,7 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { I18nProps } from '@polkadot/ui-app/types';
import { ElectionsInfo } from './types';
import { ComponentProps } from './types';
import React from 'react';
import { SummaryBox, CardSummary } from '@polkadot/ui-app';
@ -12,29 +12,26 @@ import { formatNumber } from '@polkadot/util';
import translate from '../translate';
interface Props extends I18nProps {
electionsInfo: ElectionsInfo;
}
interface Props extends I18nProps, ComponentProps {}
class Summary extends React.PureComponent<Props> {
public render (): React.ReactNode {
const { electionsInfo, t } = this.props;
const { members, candidateCount, desiredSeats, termDuration, voteCount } = electionsInfo;
return (
<SummaryBox>
<section>
<CardSummary label={t('seats')}>
{formatNumber(members.length)}/{formatNumber(desiredSeats)}
{formatNumber(Object.keys(members).length)}/{formatNumber(desiredSeats)}
</CardSummary>
<CardSummary label={t('candidates')}>
{formatNumber(candidateCount)}
</CardSummary>
</section>
<section>
<CardSummary label={t('total votes')}>
{formatNumber(voteCount)}
<CardSummary label={t('voting round')}>
#{formatNumber(voteCount)}
</CardSummary>
</section>

View File

@ -0,0 +1,319 @@
// 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';
// import { SetIndex } from '@polkadot/types/srml/types';
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 { 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)) : [],
new 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' }]
)
);

View File

@ -2,54 +2,45 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AccountId, BlockNumber } from '@polkadot/types';
import { ElectionsInfo } from './types';
import { createType, BlockNumber, VoteIndex } from '@polkadot/types';
import { SetIndex } from '@polkadot/types/srml/elections/types';
import { DerivedElectionsInfo } from '@polkadot/api-derive/types';
import { ComponentProps as Props } from './types';
import BN from 'bn.js';
import React from 'react';
import { withCalls } from '@polkadot/ui-api';
import { Button } from '@polkadot/ui-app';
import Members from './Members';
import SubmitCandidacy from './SubmitCandidacy';
import Summary from './Summary';
import Vote from './Vote';
interface Props {
elections_members?: [string, BlockNumber][];
elections_candidates?: string[];
elections_candidateCount?: BN;
elections_desiredSeats?: BN;
elections_termDuration?: BN;
elections_voteCount?: BN;
}
interface State {
electionsInfo: ElectionsInfo;
}
class Overview extends React.PureComponent<Props, State> {
public static getDerivedStateFromProps ({
elections_members: members = [],
elections_candidates: candidates = [],
elections_candidateCount: candidateCount = new BN(0),
elections_desiredSeats: desiredSeats = new BN(0),
elections_termDuration: termDuration = new BN(0),
elections_voteCount: voteCount = new BN(0)
}: Props): State {
const electionsInfo: ElectionsInfo = {
members, candidates, candidateCount, desiredSeats, termDuration, voteCount
};
return { electionsInfo };
}
const NULL_INFO: DerivedElectionsInfo = {
members: {},
candidates: [],
candidateCount: new BN(0),
desiredSeats: new BN(0),
nextVoterSet: createType<SetIndex>('SetIndex', 0),
termDuration: new BlockNumber(0),
voteCount: new VoteIndex(0),
voterCount: createType<SetIndex>('SetIndex', 0)
};
class Overview extends React.PureComponent<Props> {
public render (): React.ReactNode {
const { electionsInfo } = this.state;
const { electionsInfo = NULL_INFO } = this.props;
return (
<>
<Summary electionsInfo={electionsInfo} />
<SubmitCandidacy electionsInfo={electionsInfo} />
<Button.Group>
<SubmitCandidacy electionsInfo={electionsInfo} />
<Button.Or />
<Vote electionsInfo={electionsInfo} />
</Button.Group>
<Members electionsInfo={electionsInfo} />
</>
);
@ -57,20 +48,10 @@ class Overview extends React.PureComponent<Props, State> {
}
export default withCalls<Props>(
['query.elections.members', {
transform: (active: [AccountId, BlockNumber][]): [string, BlockNumber][] =>
active.map(([accountId, blockNumber]): [string, BlockNumber] =>
[accountId.toString(), blockNumber]
)
}],
['query.elections.candidates', {
transform: (candidates: AccountId[]): string[] =>
candidates.map((accountId): string =>
accountId.toString()
)
}],
'query.elections.candidateCount',
'query.elections.desiredSeats',
'query.elections.termDuration',
'query.elections.voteCount'
[
'derive.elections.info',
{
propName: 'electionsInfo'
}
]
)(Overview);

View File

@ -2,15 +2,16 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { BlockNumber } from '@polkadot/types';
import { SetIndex } from '@polkadot/types/srml/types';
import { DerivedElectionsInfo } from '@polkadot/api-derive/types';
import BN from 'bn.js';
export interface ElectionsInfo {
members: [string, BlockNumber][];
candidates: string[];
candidateCount: BN;
desiredSeats: BN;
termDuration: BN;
voteCount: BN;
export interface ComponentProps {
electionsInfo: DerivedElectionsInfo;
}
export interface VoterPosition {
setIndex: SetIndex;
globalIndex: BN;
}

View File

@ -13,7 +13,7 @@ import Overview from './Overview';
import Motions from './Motions';
import translate from './translate';
type Props = AppProps & BareProps & I18nProps;
interface Props extends AppProps, BareProps, I18nProps {}
interface State {
tabs: TabItem[];

View File

@ -106,6 +106,7 @@ class General extends React.PureComponent<Props, State> {
return (
<>
<Toggle
asSwitch
className='settings--cutomToggle'
defaultValue={isCustomNode}
label={t('custom endpoint')}

View File

@ -9,28 +9,33 @@ import SUICheckbox from 'semantic-ui-react/dist/commonjs/modules/Checkbox';
import styled from 'styled-components';
interface Props extends BareProps {
asSwitch?: boolean;
defaultValue?: boolean;
isDisabled?: boolean;
label: React.ReactNode;
onChange?: (isChecked: boolean) => void;
value?: boolean;
}
class Toggle extends React.PureComponent<Props> {
public render (): React.ReactNode {
const { className, defaultValue, label } = this.props;
const { className, asSwitch, defaultValue, isDisabled, value, label } = this.props;
return (
<div className={className}>
<SUICheckbox
checked={value}
disabled={isDisabled}
defaultChecked={defaultValue}
onChange={this.onChange}
toggle
toggle={asSwitch}
/>
<label>{label}</label>
</div>
);
}
private onChange = (event: React.FormEvent<HTMLInputElement>, { checked }: any): void => {
private onChange = (_: React.FormEvent<HTMLInputElement>, { checked }: any): void => {
const { onChange } = this.props;
onChange && onChange(checked);

View File

@ -37,6 +37,7 @@ interface Props extends ApiProps {
isPrimary?: boolean;
isUnsigned?: boolean;
label: React.ReactNode;
labelIcon?: string;
onClick?: () => any;
onFailed?: TxFailedCallback;
onStart?: () => void;
@ -61,7 +62,7 @@ class TxButtonInner extends React.PureComponent<InnerProps> {
};
public render (): React.ReactNode {
const { accountId, className, icon, iconSize, isBasic, isDisabled, isNegative, isPrimary, isUnsigned, label, tooltip } = this.props;
const { accountId, className, icon, iconSize, isBasic, isDisabled, isNegative, isPrimary, isUnsigned, label, labelIcon, tooltip } = this.props;
const { isSending } = this.state;
const needsAccount = isUnsigned
? false
@ -78,6 +79,7 @@ class TxButtonInner extends React.PureComponent<InnerProps> {
isNegative={isNegative}
isPrimary={isUndefined(isPrimary) ? (!isNegative && !isBasic) : isPrimary}
label={label}
labelIcon={labelIcon}
onClick={this.send}
size={iconSize}
/>

View File

@ -67,16 +67,20 @@ class Vector extends React.PureComponent<Props, State> {
style={style}
>
{values.map((value, index): React.ReactNode => (
<Component
defaultValue={value}
isDisabled={isDisabled}
key={index}
label={`${index}: ${subType.type}`}
onChange={this.onChange(index)}
onEnter={onEnter}
type={subType}
withLabel={withLabel}
/>
this.isParamHidden(index)
? null
: (
<Component
defaultValue={value}
isDisabled={isDisabled}
key={index}
label={`${index}: ${subType.type}`}
onChange={this.onChange(index)}
onEnter={onEnter}
type={subType}
withLabel={withLabel}
/>
)
))}
{this.renderButtons()}
</Bare>
@ -108,6 +112,16 @@ class Vector extends React.PureComponent<Props, State> {
);
}
private isParamHidden = (index: number): boolean => {
const { isDisabled, type } = this.props;
const { values } = this.state;
if (type.type === 'Vec<bool>' && isDisabled && values[index].value === false) {
return true;
}
return false;
}
private onChange = (index: number): (value: RawParam) => void => {
return (value: RawParam): void => {
this.setState(