This repository has been archived by the owner on Apr 15, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #593 from LiskHQ/492-MIGRATE-VOTING-FOR-DELEGATES-…
…ON-VOTING Migrate voting for delegates on voting component - Closes #492
- Loading branch information
Showing
22 changed files
with
1,093 additions
and
102 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
|
Oops, something went wrong.