Skip to content
This repository has been archived by the owner on Apr 15, 2019. It is now read-only.

Commit

Permalink
Merge pull request #593 from LiskHQ/492-MIGRATE-VOTING-FOR-DELEGATES-…
Browse files Browse the repository at this point in the history
…ON-VOTING

Migrate voting for delegates on voting component - Closes #492
  • Loading branch information
yasharAyari authored Aug 14, 2017
2 parents eb6d03c + be92b01 commit 2810a4c
Show file tree
Hide file tree
Showing 22 changed files with 1,093 additions and 102 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@
"imports-loader": "=0.6.5",
"js-nacl": "=1.2.2",
"json-loader": "=0.5.4",
"karma": "=1.6.0",
"karma": "^1.7.0",
"karma-chai": "=0.1.0",
"karma-chrome-launcher": "=2.0.0",
"karma-chrome-launcher": "^2.2.0",
"karma-coverage": "=1.1.1",
"karma-jenkins-reporter": "0.0.2",
"karma-mocha": "=1.3.0",
Expand All @@ -103,6 +103,7 @@
"should": "=11.2.0",
"sinon": "=2.0.0",
"sinon-chai": "=2.8.0",
"sinon-stub-promise": "^4.0.0",
"style-loader": "=0.16.1",
"stylelint": "=7.12.0",
"url-loader": "=0.5.7",
Expand Down
31 changes: 31 additions & 0 deletions src/actions/voting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import actionTypes from '../constants/actions';

/**
* Add data to the list of voted delegates
*/
export const addedToVoteList = data => ({
type: actionTypes.addedToVoteList,
data,
});

/**
* Remove data from the list of voted delegates
*/
export const removedFromVoteList = data => ({
type: actionTypes.removedFromVoteList,
data,
});

/**
* Remove all data from the list of voted delegates and list of unvoted delegates
*/
export const clearVoteLists = () => ({
type: actionTypes.votesCleared,
});

/**
* Add pending variable to the list of voted delegates and list of unvoted delegates
*/
export const pendingVotesAdded = () => ({
type: actionTypes.pendingVotesAdded,
});
50 changes: 50 additions & 0 deletions src/actions/voting.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expect } from 'chai';
import actionTypes from '../constants/actions';
import {
addedToVoteList,
removedFromVoteList,
clearVoteLists,
pendingVotesAdded,
} from './voting';

describe('actions: voting', () => {
const data = {
label: 'dummy',
};

describe('addedToVoteList', () => {
it('should create an action to add data to vote list', () => {
const expectedAction = {
data,
type: actionTypes.addedToVoteList,
};
expect(addedToVoteList(data)).to.be.deep.equal(expectedAction);
});
});

describe('removedFromVoteList', () => {
it('should create an action to remove data from vote list', () => {
const expectedAction = {
data,
type: actionTypes.removedFromVoteList,
};
expect(removedFromVoteList(data)).to.be.deep.equal(expectedAction);
});
});
describe('clearVoteLists', () => {
it('should create an action to remove all pending rows from vote list', () => {
const expectedAction = {
type: actionTypes.votesCleared,
};
expect(clearVoteLists()).to.be.deep.equal(expectedAction);
});
});
describe('pendingVotesAdded', () => {
it('should create an action to remove all pending rows from vote list', () => {
const expectedAction = {
type: actionTypes.pendingVotesAdded,
};
expect(pendingVotesAdded()).to.be.deep.equal(expectedAction);
});
});
});
116 changes: 116 additions & 0 deletions src/components/voting/confirmVotes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React from 'react';
import { connect } from 'react-redux';
import Input from 'react-toolbox/lib/input';
import { vote } from '../../utils/api/delegate';
import { alertDialogDisplayed } from '../../actions/dialog';
import { clearVoteLists, pendingVotesAdded } from '../../actions/voting';
import InfoParagraph from '../infoParagraph';
import ActionBar from '../actionBar';
import { SYNC_ACTIVE_INTERVAL } from '../../constants/api';
import Fees from '../../constants/fees';

export class ConfirmVotes extends React.Component {
constructor() {
super();
this.state = {
secondSecret: '',
};
}

confirm() {
const secondSecret = this.state.secondSecret.length === 0 ? null : this.state.secondSecret;
const text = 'Your votes were successfully submitted. It can take several seconds before they are processed.';

vote(
this.props.activePeer,
this.props.account.passphrase,
this.props.account.publicKey,
this.props.votedList,
this.props.unvotedList,
secondSecret,
).then((data) => {
this.props.pendingVotesAdded();

// add to pending transaction
this.props.addTransaction({
id: data.transactionId,
senderPublicKey: this.props.account.publicKey,
senderId: this.props.account.address,
amount: 0,
fee: Fees.vote,
type: 3,
});

// remove pending votes
setTimeout(() => {
this.props.clearVoteLists();
}, SYNC_ACTIVE_INTERVAL);
this.props.showSuccessAlert({
title: 'Success',
type: 'success',
text,
});
});
}

setSecondPass(name, value) {
this.setState({ ...this.state, [name]: value });
}

render() {
const secondPassphrase = this.props.account.secondSignature === 1 ?
<Input type='text' label='Second Passphrase' name='secondSecret'
className='secondSecret' value={this.state.secondSecret}
onChange={this.setSecondPass.bind(this, 'secondSecret')}/> : null;

return (
<article>
<h3>Add vote to</h3>
<ul>
{this.props.votedList.map(item => <li key={item.username}>{item.username}</li>)}
</ul>
<h3>Remove vote from</h3>
<ul>
{this.props.unvotedList.map(item => <li key={item.username}>{item.username}</li>)}
</ul>

{secondPassphrase}

<InfoParagraph>
You can select up to 33 delegates in one voting turn.
<br />
You can vote for up to 101 delegates in total.
</InfoParagraph>

<ActionBar
secondaryButton={{
onClick: this.props.closeDialog,
}}
primaryButton={{
label: 'Vote',
fee: Fees.vote,
disabled: (
this.props.votedList.length === 0 &&
this.props.unvotedList.length === 0),
onClick: this.confirm.bind(this),
}} />
</article>
);
}
}


const mapStateToProps = state => ({
votedList: state.voting.votedList,
unvotedList: state.voting.unvotedList,
account: state.account,
activePeer: state.peers.data,
});

const mapDispatchToProps = dispatch => ({
showSuccessAlert: data => dispatch(alertDialogDisplayed(data)),
clearVoteLists: () => dispatch(clearVoteLists()),
pendingVotesAdded: () => dispatch(pendingVotesAdded()),
});

export default connect(mapStateToProps, mapDispatchToProps)(ConfirmVotes);
85 changes: 85 additions & 0 deletions src/components/voting/confirmVotes.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from 'react';
import chai, { expect } from 'chai';
import { mount } from 'enzyme';
import chaiEnzyme from 'chai-enzyme';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import PropTypes from 'prop-types';
import sinonStubPromise from 'sinon-stub-promise';
import store from '../../store';
import ConfrimVotesContainer, { ConfirmVotes } from './confirmVotes';
import * as delegateApi from '../../utils/api/delegate';

sinonStubPromise(sinon);
chai.use(sinonChai);
chai.use(chaiEnzyme());

const props = {
activePeer: {},
account: {
passphrase: 'pass',
publicKey: 'key',
secondSignature: 0,
},
votedList: [
{
username: 'yashar',
},
{
username: 'tom',
},
],
unvotedList: [
{
username: 'john',
},
{
username: 'test',
},
],
closeDialog: sinon.spy(),
showSuccessAlert: sinon.spy(),
clearVoteLists: sinon.spy(),
pendingVotesAdded: sinon.spy(),
addTransaction: sinon.spy(),
};
describe('ConfrimVotesContainer', () => {
it('should render ConfrimVotes', () => {
const wrapper = mount(<ConfrimVotesContainer {...props} store={store} />, {
context: { store },
childContextTypes: { store: PropTypes.object.isRequired },
});
expect(wrapper.find('ConfirmVotes').exists()).to.be.equal(true);
});
});
describe('ConfrimVotes', () => {
let wrapper;
const delegateApiMock = sinon.stub(delegateApi, 'vote');
beforeEach(() => {
wrapper = mount(<ConfirmVotes {...props} />, {
context: { store },
childContextTypes: { store: PropTypes.object.isRequired },
});
});

it('should call vote api when confirm button is pressed', () => {
const clock = sinon.useFakeTimers();
delegateApiMock.returnsPromise().resolves({ success: true });
wrapper.instance().confirm();
expect(props.pendingVotesAdded).to.have.been.calledWith();
expect(props.addTransaction).to.have.been.calledWith();
expect(props.showSuccessAlert).to.have.been.calledWith();
// it should triger 'props.clearVoteLists' after 10000 ms
clock.tick(10000);
expect(props.clearVoteLists).to.have.been.calledWith();
});

it('should update state when "setSecondPass" is called', () => {
wrapper.setProps({
account: Object.assign(props.account, { secondSignature: 1 }),
});
wrapper.find('.secondSecret input').simulate('change', { target: { value: 'this is test' } });
expect(wrapper.state('secondSecret')).to.be.equal('this is test');
});
});

7 changes: 5 additions & 2 deletions src/components/voting/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { connect } from 'react-redux';
import VotingComponent from './votingComponent';
import Voting from './voting';

const mapStateToProps = state => ({
address: state.account.address,
activePeer: state.peers.data,
votedList: state.voting.votedList,
unvotedList: state.voting.unvotedList,
refreshDelegates: state.voting.refresh,
});

export default connect(mapStateToProps)(VotingComponent);
export default connect(mapStateToProps)(Voting);
2 changes: 1 addition & 1 deletion src/components/voting/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ describe('Voting', () => {
});

it('should render VotingComponent', () => {
expect(wrapper.find('VotingComponent')).to.have.lengthOf(1);
expect(wrapper.find('Voting')).to.have.lengthOf(1);
});
});
39 changes: 39 additions & 0 deletions src/components/voting/voteCheckbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import Checkbox from 'react-toolbox/lib/checkbox';
import { connect } from 'react-redux';
import { addedToVoteList, removedFromVoteList } from '../../actions/voting';
import Spinner from '../spinner';

export class VoteCheckbox extends React.Component {
/**
* change status of selected row
* @param {Number} index - index of row that we want to change status of that
* @param {Boolean} value - value of checkbox
*/
toggle(delegate, value) {
if (value) {
this.props.addToVoteList(delegate);
} else {
this.props.removeFromVoteList(delegate);
}
}

render() {
const template = this.props.pending ?
<Spinner /> :
<Checkbox
className={this.props.styles.field}
checked={this.props.value}
onChange={this.toggle.bind(this, this.props.data)}
/>;
return template;
}
}

const mapDispatchToProps = dispatch => ({
addToVoteList: data => dispatch(addedToVoteList(data)),
removeFromVoteList: data => dispatch(removedFromVoteList(data)),
});

export default connect(null, mapDispatchToProps)(VoteCheckbox);

Loading

0 comments on commit 2810a4c

Please sign in to comment.