From 42963b1eace4e5b0f8de4f5cd8d74a0640ffe772 Mon Sep 17 00:00:00 2001 From: reyraa Date: Mon, 11 Sep 2017 19:00:40 +0200 Subject: [PATCH 01/18] Refactor voting reducer --- src/constants/actions.js | 5 +- src/store/reducers/voting.js | 111 ++++++++---------- src/store/reducers/voting.test.js | 182 ++++++++++++++++-------------- 3 files changed, 150 insertions(+), 148 deletions(-) diff --git a/src/constants/actions.js b/src/constants/actions.js index 3b240d885..b4681a093 100644 --- a/src/constants/actions.js +++ b/src/constants/actions.js @@ -12,9 +12,10 @@ const actionTypes = { forgingStatsUpdated: 'FORGING_STATS_UPDATED', forgingReset: 'FORGING_RESET', VotePlaced: 'VOTE_PLACED', - addedToVoteList: 'ADDED_TO_VOTE_LIST', - removedFromVoteList: 'REMOVEd_FROM_VOTE_LIST', + voteToggled: 'VOTE_TOGGLED', + votesAdded: 'VOTES_ADDED', votesCleared: 'VOTES_CLEARED', + delegatesAdded: 'DELEGATES_ADDED', pendingVotesAdded: 'PENDING_VOTES_ADDED', toastDisplayed: 'TOAST_DISPLAYED', toastHidden: 'TOAST_HIDDEN', diff --git a/src/store/reducers/voting.js b/src/store/reducers/voting.js index 4894e6e8f..124336a98 100644 --- a/src/store/reducers/voting.js +++ b/src/store/reducers/voting.js @@ -1,89 +1,74 @@ import actionTypes from '../../constants/actions'; -/** - * remove a gelegate from list of delegates - * - * @param {array} list - list for delegates - * @param {object} item - a delegates that we want to remove it - */ -const removeFromList = (list, item) => { - const address = item.address; - return list.filter(delegate => delegate.address !== address); -}; -/** - * find index of a gelegate in list of delegates - * - * @param {array} list - list for delegates - * @param {object} item - a delegates that we want to find its index - */ -const findItemInList = (list, item) => { - const address = item.address; - let idx = -1; - list.forEach((delegate, index) => { - if (delegate.address === address) { - idx = index; - } - }); - return idx; -}; + /** * voting reducer * * @param {Object} state * @param {Object} action */ -const voting = (state = { votedList: [], unvotedList: [] }, action) => { +const voting = (state = { votes: {}, delegates: [] }, action) => { switch (action.type) { - case actionTypes.addedToVoteList: - if (action.data.voted) { - return Object.assign({}, state, { - refresh: false, - unvotedList: [...removeFromList(state.unvotedList, action.data)], - }); - } - if (findItemInList(state.votedList, action.data) > -1) { - return state; - } + case actionTypes.votesAdded: return Object.assign({}, state, { - refresh: false, - votedList: [ - ...state.votedList, - Object.assign(action.data, { selected: true, dirty: true }), - ], + votes: action.data.list + .reduce((votesDict, delegate) => { + votesDict[delegate.username] = { confirmed: true, unconfirmed: true }; + return votesDict; + }, {}), + delegates: action.data.list, }); - case actionTypes.removedFromVoteList: - if (!action.data.voted) { - return Object.assign({}, state, { - refresh: false, - votedList: [...removeFromList(state.votedList, action.data)], - }); - } - if (findItemInList(state.unvotedList, action.data) > -1) { - return state; - } + + case actionTypes.delegatesAdded: return Object.assign({}, state, { - refresh: false, - unvotedList: [ - ...state.unvotedList, - Object.assign(action.data, { selected: false, dirty: true }), - ], + delegates: action.data.list, }); + + case actionTypes.voteToggled: + return Object.assign({}, state, { + votes: Object.assign({}, state.votes, { + [action.data]: { + confirmed: state.votes[action.data] ? state.votes[action.data].confirmed : false, + unconfirmed: state.votes[action.data] ? !state.votes[action.data].confirmed : true, + }, + }), + }); + + case actionTypes.accountLoggedOut: return Object.assign({}, state, { - votedList: [], - unvotedList: [], + votes: {}, + delegates: [], refresh: true, }); + case actionTypes.votesCleared: return Object.assign({}, state, { - votedList: state.votedList.filter(item => !item.pending), - unvotedList: state.unvotedList.filter(item => !item.pending), + votes: Object.keys(state.votes).reduce((votesDict, username) => { + votesDict[username] = { + confirmed: state.votes[username].confirmed, + unconfirmed: state.votes[username].confirmed, + pending: false, + }; + return votesDict; + }, {}), refresh: true, }); + case actionTypes.pendingVotesAdded: return Object.assign({}, state, { - votedList: state.votedList.map(item => Object.assign(item, { pending: true })), - unvotedList: state.unvotedList.map(item => Object.assign(item, { pending: true })), + votes: Object.keys(state.votes).reduce((votesDict, username) => { + const pending = state.votes[username].confirmed !== state.votes[username].unconfirmed; + const { confirmed, unconfirmed } = state.votes[username]; + + votesDict[username] = { + confirmed: pending ? !confirmed : confirmed, + unconfirmed, + pending, + }; + return votesDict; + }, {}), }); + default: return state; } diff --git a/src/store/reducers/voting.test.js b/src/store/reducers/voting.test.js index 57a63abb5..b418d06b9 100644 --- a/src/store/reducers/voting.test.js +++ b/src/store/reducers/voting.test.js @@ -2,139 +2,155 @@ import { expect } from 'chai'; import actionTypes from '../../constants/actions'; import voting from './voting'; -describe('Reducer: voting(state, action)', () => { +describe.only('Reducer: voting(state, action)', () => { const state = { - votedList: [ - { - address: 'voted address1', - }, - { - address: 'voted address2', - }, - ], - unvotedList: [ - { - address: 'unvoted address1', - }, - { - address: 'unvoted address2', - }, - ], + votes: { + username1: { confirmed: true, unconfirmed: true }, + username2: { confirmed: false, unconfirmed: false }, + }, }; - it('should render default state', () => { + it('should return default state if action does not match', () => { const action = { type: '', }; const changedState = voting(state, action); + expect(changedState).to.be.equal(state); }); - it('should be 1 items in state.unvotedList', () => { + + it('should clean up with action: accountLoggedOut', () => { const action = { - type: actionTypes.addedToVoteList, - data: { - voted: true, - address: 'unvoted address1', - }, + type: actionTypes.accountLoggedOut, }; const changedState = voting(state, action); - expect(changedState.unvotedList).to.have.lengthOf(1); + const expectedState = { votes: {}, delegates: [], refresh: true }; + + expect(changedState).to.be.deep.equal(expectedState); }); - it('should return state if action.data existed in votedList before', () => { + it('should fill delegates list with action: votesAdded', () => { const action = { - type: actionTypes.addedToVoteList, + type: actionTypes.votesAdded, data: { - address: 'voted address1', + list: [ + { username: 'username1', id: '123HGJ123234L' }, + ], }, }; - const changedState = voting(state, action); - expect(changedState).to.be.deep.equal(state); - }); - - it('should be 3 items in state.votedList', () => { - const action = { - type: actionTypes.addedToVoteList, - data: { - address: 'voted address3', + const expectedState = { + votes: { + username1: { confirmed: true, unconfirmed: true }, }, + delegates: [{ username: 'username1', id: '123HGJ123234L' }], }; - const changedState = voting(state, action); - expect(changedState.votedList).to.have.lengthOf(3); + const oldState = { votes: {}, delegates: [] }; + const changedState = voting(oldState, action); + + expect(changedState).to.be.deep.equal(expectedState); }); - it('should be 1 items in state.votedList', () => { + it('should fill delegates list with action: delegatesAdded', () => { const action = { - type: actionTypes.removedFromVoteList, + type: actionTypes.delegatesAdded, data: { - voted: false, - address: 'voted address1', + list: [ + { username: 'username1', id: '123HGJ123234L' }, + ], + }, + }; + const expectedState = { + votes: { + username1: { confirmed: true, unconfirmed: true }, + username2: { confirmed: false, unconfirmed: false }, }, + delegates: [{ username: 'username1', id: '123HGJ123234L' }], }; const changedState = voting(state, action); - expect(changedState.votedList).to.have.lengthOf(1); + + expect(changedState).to.be.deep.equal(expectedState); }); - it('should return state if action.data existed in unvotedList before', () => { + it('should toggle unconfirmed state, with action: voteToggled', () => { const action = { - type: actionTypes.removedFromVoteList, - data: { - voted: true, - address: 'unvoted address2', + type: actionTypes.voteToggled, + data: 'username1', + }; + const expectedState = { + votes: { + username1: { confirmed: true, unconfirmed: false }, + username2: { confirmed: false, unconfirmed: false }, }, }; const changedState = voting(state, action); - expect(changedState).to.be.deep.equal(state); + + expect(changedState).to.be.deep.equal(expectedState); }); - it('should be 3 items in state.unvotedList', () => { + it('should add to votes dictionary in not exist, with action: voteToggled', () => { const action = { - type: actionTypes.removedFromVoteList, - data: { - voted: true, - address: 'unvoted address3', + type: actionTypes.voteToggled, + data: 'username3', + }; + const expectedState = { + votes: { + username1: { confirmed: true, unconfirmed: true }, + username2: { confirmed: false, unconfirmed: false }, + username3: { confirmed: false, unconfirmed: true }, }, }; const changedState = voting(state, action); - expect(changedState.unvotedList).to.have.lengthOf(3); + + expect(changedState).to.be.deep.equal(expectedState); }); - it('should add pending to all items in votedList and unvotedList', () => { + it('should mark the toggles votes as pending, with action: pendingVotesAdded ', () => { const action = { type: actionTypes.pendingVotesAdded, }; + const oldState = { + votes: { + username1: { confirmed: true, unconfirmed: false }, + username2: { confirmed: false, unconfirmed: true }, + username3: { confirmed: true, unconfirmed: true }, + username4: { confirmed: false, unconfirmed: false }, + }, + }; const expectedState = { - votedList: [ - { - address: 'voted address1', - pending: true, - }, - { - address: 'voted address2', - pending: true, - }, - ], - unvotedList: [ - { - address: 'unvoted address1', - pending: true, - }, - { - address: 'unvoted address2', - pending: true, - }, - ], + votes: { + username1: { confirmed: false, unconfirmed: false, pending: true }, + username2: { confirmed: true, unconfirmed: true, pending: true }, + username3: { confirmed: true, unconfirmed: true, pending: false }, + username4: { confirmed: false, unconfirmed: false, pending: false }, + }, }; - const changedState = voting(state, action); + const changedState = voting(oldState, action); + expect(changedState).to.be.deep.equal(expectedState); }); - it('should remove all pending in votedList and unvotedList', () => { + it('should remove all pending flags from votes, with action: votesCleared', () => { const action = { type: actionTypes.votesCleared, }; - const changedState = voting(state, action); - expect(changedState.unvotedList).to.have.lengthOf(0); - expect(changedState.votedList).to.have.lengthOf(0); - expect(changedState.refresh).to.be.equal(true); + const oldState = { + votes: { + username1: { confirmed: true, unconfirmed: false }, + username2: { confirmed: false, unconfirmed: true }, + username3: { confirmed: true, unconfirmed: true }, + username4: { confirmed: false, unconfirmed: false }, + }, + }; + const expectedState = { + votes: { + username1: { confirmed: true, unconfirmed: true, pending: false }, + username2: { confirmed: false, unconfirmed: false, pending: false }, + username3: { confirmed: true, unconfirmed: true, pending: false }, + username4: { confirmed: false, unconfirmed: false, pending: false }, + }, + refresh: true, + }; + const changedState = voting(oldState, action); + + expect(changedState).to.be.deep.equal(expectedState); }); }); From 7e5ded5b761189fc41159996531f4b202080e0f4 Mon Sep 17 00:00:00 2001 From: reyraa Date: Tue, 12 Sep 2017 12:37:39 +0200 Subject: [PATCH 02/18] Replace addToVotelsi and removeFromVotes with voteToggled --- src/actions/voting.js | 43 +++++++++++++++--------- src/actions/voting.test.js | 67 +++++++++++++++++++++++++++++--------- 2 files changed, 80 insertions(+), 30 deletions(-) diff --git a/src/actions/voting.js b/src/actions/voting.js index 670da839c..eb9fb9cd2 100644 --- a/src/actions/voting.js +++ b/src/actions/voting.js @@ -1,5 +1,5 @@ import actionTypes from '../constants/actions'; -import { vote } from '../utils/api/delegate'; +import { vote, listAccountDelegates, listDelegates } from '../utils/api/delegate'; import { transactionAdded } from './transactions'; import { errorAlertDialogDisplayed } from './dialog'; import Fees from '../constants/fees'; @@ -20,7 +20,25 @@ export const clearVoteLists = () => ({ }); /** - * + * Add data to the list of voted delegates + */ +export const votesAdded = data => ({ + type: actionTypes.votesAdded, + data, +}); + +/** + * Toggles account's vote for the given delegate + */ +export const voteToggled = data => ({ + type: actionTypes.voteToggled, + data, +}); + +/** + * Makes Api call to register votes + * Adds pending state and then after the duration of one round + * cleans the pending state */ export const votePlaced = ({ activePeer, account, votedList, unvotedList, secondSecret }) => (dispatch) => { @@ -59,17 +77,12 @@ export const votePlaced = ({ activePeer, account, votedList, unvotedList, second }; /** - * Add data to the list of voted delegates - */ -export const addedToVoteList = data => ({ - type: actionTypes.addedToVoteList, - data, -}); - -/** - * Remove data from the list of voted delegates + * Gets the list of delegates current account has voted for + * */ -export const removedFromVoteList = data => ({ - type: actionTypes.removedFromVoteList, - data, -}); +export const votesFetched = ({ activePeer, address }) => + (dispatch) => { + listAccountDelegates(activePeer, address).then(({ delegates }) => { + dispatch(votesAdded({ list: delegates })); + }); + }; diff --git a/src/actions/voting.test.js b/src/actions/voting.test.js index 4ca6b18c4..3c08c7de8 100644 --- a/src/actions/voting.test.js +++ b/src/actions/voting.test.js @@ -2,11 +2,12 @@ import { expect } from 'chai'; import sinon from 'sinon'; import actionTypes from '../constants/actions'; import { - addedToVoteList, - removedFromVoteList, - clearVoteLists, - pendingVotesAdded, - votePlaced, + pendingVotesAdded, + clearVoteLists, + votesAdded, + voteToggled, + votePlaced, + votesFetched, } from './voting'; import Fees from '../constants/fees'; import { transactionAdded } from './transactions'; @@ -14,31 +15,32 @@ import { errorAlertDialogDisplayed } from './dialog'; import * as delegateApi from '../utils/api/delegate'; describe('actions: voting', () => { - describe('addedToVoteList', () => { - it('should create an action to add data to vote list', () => { + describe('voteToggled', () => { + it('should create an action to add data to toggle the vote status for any given delegate', () => { const data = { label: 'dummy', }; const expectedAction = { data, - type: actionTypes.addedToVoteList, + type: actionTypes.voteToggled, }; - expect(addedToVoteList(data)).to.be.deep.equal(expectedAction); + expect(voteToggled(data)).to.be.deep.equal(expectedAction); }); }); - describe('removedFromVoteList', () => { + describe('votesAdded', () => { it('should create an action to remove data from vote list', () => { - const data = { - label: 'dummy', - }; + const data = [ + { username: 'username1', id: '123HG3452245L' }, + { username: 'username2', id: '123HG3522345L' }, + ]; const expectedAction = { data, - type: actionTypes.removedFromVoteList, + type: actionTypes.votesAdded, }; - expect(removedFromVoteList(data)).to.be.deep.equal(expectedAction); + expect(votesAdded(data)).to.be.deep.equal(expectedAction); }); }); @@ -130,4 +132,39 @@ describe('actions: voting', () => { expect(dispatch).to.have.been.calledWith(expectedAction); }); }); + + describe('votesFetched', () => { + let delegateApiMock; + const data = { + activePeer: {}, + address: '8096217735672704724L', + }; + const votesList = [ + { username: 'username1', id: '80962134535672704724L' }, + { username: 'username2', id: '80962134535672704725L' }, + ]; + const actionFunction = votesFetched(data); + let dispatch; + + beforeEach(() => { + delegateApiMock = sinon.stub(delegateApi, 'setSecondPassphrase'); + dispatch = sinon.spy(); + }); + + afterEach(() => { + delegateApiMock.restore(); + }); + + it('should create an action function', () => { + expect(typeof actionFunction).to.be.deep.equal('function'); + }); + + it('should dispatch votesAdded action if resolved', () => { + delegateApiMock.returnsPromise().resolves(votesList); + const expectedAction = { list: votesList }; + + actionFunction(dispatch); + expect(dispatch).to.have.been.calledWith(transactionAdded(expectedAction)); + }); + }); }); From c0838c09e4fabf55e1913804b6b3ba2923c570dd Mon Sep 17 00:00:00 2001 From: reyraa Date: Tue, 12 Sep 2017 12:38:27 +0200 Subject: [PATCH 03/18] Minor fixings in reducer. tests updated --- src/store/reducers/voting.js | 4 ++-- src/store/reducers/voting.test.js | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/store/reducers/voting.js b/src/store/reducers/voting.js index 124336a98..402c24e90 100644 --- a/src/store/reducers/voting.js +++ b/src/store/reducers/voting.js @@ -20,7 +20,7 @@ const voting = (state = { votes: {}, delegates: [] }, action) => { case actionTypes.delegatesAdded: return Object.assign({}, state, { - delegates: action.data.list, + delegates: [...state.delegates, ...action.data.list], }); case actionTypes.voteToggled: @@ -28,7 +28,7 @@ const voting = (state = { votes: {}, delegates: [] }, action) => { votes: Object.assign({}, state.votes, { [action.data]: { confirmed: state.votes[action.data] ? state.votes[action.data].confirmed : false, - unconfirmed: state.votes[action.data] ? !state.votes[action.data].confirmed : true, + unconfirmed: state.votes[action.data] ? !state.votes[action.data].unconfirmed : true, }, }), }); diff --git a/src/store/reducers/voting.test.js b/src/store/reducers/voting.test.js index b418d06b9..989dd9cd3 100644 --- a/src/store/reducers/voting.test.js +++ b/src/store/reducers/voting.test.js @@ -2,7 +2,7 @@ import { expect } from 'chai'; import actionTypes from '../../constants/actions'; import voting from './voting'; -describe.only('Reducer: voting(state, action)', () => { +describe('Reducer: voting(state, action)', () => { const state = { votes: { username1: { confirmed: true, unconfirmed: true }, @@ -58,14 +58,16 @@ describe.only('Reducer: voting(state, action)', () => { ], }, }; + const oldState = { + delegates: [{ username: 'username2', id: '123HGJ123235L' }], + }; const expectedState = { - votes: { - username1: { confirmed: true, unconfirmed: true }, - username2: { confirmed: false, unconfirmed: false }, - }, - delegates: [{ username: 'username1', id: '123HGJ123234L' }], + delegates: [ + { username: 'username2', id: '123HGJ123235L' }, + { username: 'username1', id: '123HGJ123234L' }, + ], }; - const changedState = voting(state, action); + const changedState = voting(oldState, action); expect(changedState).to.be.deep.equal(expectedState); }); From dbf1cc0801019c8d1ea8ad994fbb4ed611cb0104 Mon Sep 17 00:00:00 2001 From: reyraa Date: Tue, 12 Sep 2017 12:39:21 +0200 Subject: [PATCH 04/18] Update voting components according to changes in actions and reducers --- src/components/voting/index.js | 11 ++-- src/components/voting/voteCheckbox.js | 35 ++++------- src/components/voting/voting.css | 4 ++ src/components/voting/voting.js | 84 +++++++++++++-------------- src/components/voting/votingHeader.js | 29 ++++----- src/components/voting/votingRow.js | 7 +-- 6 files changed, 79 insertions(+), 91 deletions(-) diff --git a/src/components/voting/index.js b/src/components/voting/index.js index 002386468..e9fa062a1 100644 --- a/src/components/voting/index.js +++ b/src/components/voting/index.js @@ -1,23 +1,22 @@ import { connect } from 'react-redux'; import { dialogDisplayed } from '../../actions/dialog'; -import { removedFromVoteList, addedToVoteList } from '../../actions/voting'; +import { voteToggled, votesFetched } from '../../actions/voting'; import { transactionAdded } from '../../actions/transactions'; import Voting from './voting'; const mapStateToProps = state => ({ address: state.account.address, activePeer: state.peers.data, - votedList: state.voting.votedList, - unvotedList: state.voting.unvotedList, + votes: state.voting.votes, + delegates: state.voting.delegates, refreshDelegates: state.voting.refresh, }); const mapDispatchToProps = dispatch => ({ setActiveDialog: data => dispatch(dialogDisplayed(data)), - addToUnvoted: data => dispatch(addedToVoteList(data)), - addToVoteList: data => dispatch(addedToVoteList(data)), - removeFromVoteList: data => dispatch(removedFromVoteList(data)), + voteToggled: data => dispatch(voteToggled(data)), addTransaction: data => dispatch(transactionAdded(data)), + votesFetched: data => dispatch(votesFetched(data)), }); export default connect(mapStateToProps, mapDispatchToProps)(Voting); diff --git a/src/components/voting/voteCheckbox.js b/src/components/voting/voteCheckbox.js index e8ec9f873..78bf5acf2 100644 --- a/src/components/voting/voteCheckbox.js +++ b/src/components/voting/voteCheckbox.js @@ -2,28 +2,15 @@ import React from 'react'; import Checkbox from 'react-toolbox/lib/checkbox'; import Spinner from '../spinner'; -export default 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); - } - } +const VoteCheckbox = (props) => { + const template = props.status && props.status.pending ? + : + ; + return template; +}; - render() { - const template = this.props.pending ? - : - ; - return template; - } -} +export default VoteCheckbox; diff --git a/src/components/voting/voting.css b/src/components/voting/voting.css index e0483d5f9..f5c4ac255 100644 --- a/src/components/voting/voting.css +++ b/src/components/voting/voting.css @@ -90,3 +90,7 @@ margin-top: 18px; margin-right: 16px; } + +.rotated { + transform: rotate(45deg); +} diff --git a/src/components/voting/voting.js b/src/components/voting/voting.js index 1da3eb086..5e3085d6b 100644 --- a/src/components/voting/voting.js +++ b/src/components/voting/voting.js @@ -5,7 +5,7 @@ import { tableFactory } from 'react-toolbox/lib/table/Table'; import { TableHead, TableCell } from 'react-toolbox/lib/table'; import TableTheme from 'react-toolbox/lib/table/theme.css'; import Waypoint from 'react-waypoint'; -import { listAccountDelegates, listDelegates } from '../../utils/api/delegate'; +import { listDelegates } from '../../utils/api/delegate'; import Header from './votingHeader'; import VotingRow from './votingRow'; @@ -17,7 +17,6 @@ class Voting extends React.Component { super(); this.state = { delegates: [], - votedDelegates: [], selected: [], offset: 0, loadMore: false, @@ -28,16 +27,18 @@ class Voting extends React.Component { } componentWillReceiveProps() { - setTimeout(() => { - if (this.props.refreshDelegates) { - this.loadVotedDelegates(true); - } else { - const delegates = this.state.delegates.map(delegate => this.setStatus(delegate)); - this.setState({ - delegates, - }); - } - }, 1); + // setTimeout(() => { + // if (this.props.refreshDelegates) { + // console.log('load voted'); + // this.loadVotedDelegates(true); + // } else { + // console.log('load the rest'); + // const delegates = this.state.delegates.map(delegate => this.setStatus(delegate)); + // this.setState({ + // delegates, + // }); + // } + // }, 1); } componentDidMount() { @@ -45,28 +46,24 @@ class Voting extends React.Component { } loadVotedDelegates(refresh) { - listAccountDelegates(this.props.activePeer, this.props.address).then((res) => { - if (res.delegates) { - const votedDelegates = res.delegates - .map(delegate => Object.assign({}, delegate, { voted: true })); - this.setState({ - votedDelegates, - }); - } - if (refresh) { - setTimeout(() => { - const delegates = this.state.delegates.map(delegate => this.setStatus(delegate)); - this.setState({ - delegates, - }); - }, 10); - } else { - this.loadDelegates(this.query); - } - }) - .catch(() => { - this.loadDelegates(this.query); - }); + this.props.votesFetched({ activePeer: this.props.activePeer, + address: this.props.address }); + + // listAccountDelegates(this.props.activePeer, this.props.address).then((res) => { + // if (refresh) { + // setTimeout(() => { + // const delegates = this.state.delegates.map(delegate => this.setStatus(delegate)); + // this.setState({ + // delegates, + // }); + // }, 10); + // } else { + // this.loadDelegates(this.query); + // } + // }) + // .catch(() => { + // this.loadDelegates(this.query); + // }); } /** @@ -90,7 +87,7 @@ class Voting extends React.Component { * Fetches a list of delegates * * @method loadDelegates - * @param {String} search - The search phrase to match with the delegate name + * @param {String} query - The search phrase to match with the delegate name * should replace the old delegates list * @param {Number} limit - The maximum number of results */ @@ -138,13 +135,14 @@ class Voting extends React.Component { if (delegateExisted) { return delegateExisted; } - const voted = this.state.votedDelegates + + const voted = this.props.confirmedVotedList .filter(row => row.username === delegate.username).length > 0; return Object.assign(delegate, { voted }, { selected: voted }, { pending: false }); } /** - * load more data when scroll bar reachs end of the page + * load more data when scroll bar reaches end of the page */ loadMore() { if (this.state.loadMore && this.state.length > this.state.offset) { @@ -157,11 +155,9 @@ class Voting extends React.Component {
this.search(value) } />
@@ -174,10 +170,10 @@ class Voting extends React.Component { Uptime Approval - {this.state.delegates.map(item => ( + {this.props.delegates.map(item => ( ))} diff --git a/src/components/voting/votingHeader.js b/src/components/voting/votingHeader.js index d133b3027..f4d81d20e 100644 --- a/src/components/voting/votingHeader.js +++ b/src/components/voting/votingHeader.js @@ -13,6 +13,7 @@ class VotingHeader extends React.Component { this.state = { query: '', searchIcon: 'search', + votesList: [], }; } @@ -38,8 +39,12 @@ class VotingHeader extends React.Component { confirmVoteText() { let info = 'VOTE'; - const voted = this.props.votedList.filter(item => !item.pending).length; - const unvoted = this.props.unvotedList.filter(item => !item.pending).length; + const { votes } = this.props; + const votesList = Object.keys(votes); + const voted = votesList.filter(item => + !votes[item].confirmed && votes[item].unconfirmed).length; + const unvoted = votesList.filter(item => + votes[item].confirmed && !votes[item].unconfirmed).length; if (voted > 0 || unvoted > 0) { const seprator = (voted > 0 && unvoted > 0) ? ' / ' : ''; // eslint-disable-line const votedHtml = voted > 0 ? +{voted} : ''; @@ -50,10 +55,12 @@ class VotingHeader extends React.Component { } render() { - const theme = this.props.votedDelegates.length === 0 ? disableStyle : styles; + const { votes } = this.props; + const votesList = Object.keys(votes); + const theme = votesList.length === 0 ? disableStyle : styles; const button =
visibility - my votes ({this.props.votedDelegates.length}) + my votes ({votesList.length})
; return (
@@ -71,23 +78,19 @@ class VotingHeader extends React.Component {
- {this.props.votedDelegates.map(delegate => + {votesList.map(delegate => )} + key={delegate} + caption={delegate} + icon={(votes[delegate].confirmed === votes[delegate].unconfirmed) ? 'clear' : 'add'} + onClick={this.props.voteToggled.bind(this, delegate)} />)}
diff --git a/src/components/voting/votingRow.js b/src/components/voting/votingRow.js index 69ced7c6e..f4e6f0018 100644 --- a/src/components/voting/votingRow.js +++ b/src/components/voting/votingRow.js @@ -15,7 +15,7 @@ const setRowClass = ({ pending, selected, voted }) => { class VotingRow extends React.Component { // eslint-disable-next-line class-methods-use-this shouldComponentUpdate(nextProps) { - return !!nextProps.data.dirty; + return nextProps.voteStatus.unconfirmed !== this.props.voteStatus.unconfirmed; } render() { @@ -24,10 +24,9 @@ class VotingRow extends React.Component { return ( From 8e4b6e33a38a880409a0a353fe2e1a6ecde18c8b Mon Sep 17 00:00:00 2001 From: reyraa Date: Fri, 15 Sep 2017 09:32:26 +0200 Subject: [PATCH 05/18] Add delegatesFetched action. - Move votes dict normalization to actions. -Adapt unit tests --- src/actions/voting.js | 38 ++++++++++++++++++++++++++++++++++++-- src/actions/voting.test.js | 17 ++++++++--------- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/actions/voting.js b/src/actions/voting.js index eb9fb9cd2..8502be1c5 100644 --- a/src/actions/voting.js +++ b/src/actions/voting.js @@ -27,6 +27,14 @@ export const votesAdded = data => ({ data, }); +/** + * Add data to the list of all delegates + */ +export const delegatesAdded = data => ({ + type: actionTypes.delegatesAdded, + data, +}); + /** * Toggles account's vote for the given delegate */ @@ -40,9 +48,19 @@ export const voteToggled = data => ({ * Adds pending state and then after the duration of one round * cleans the pending state */ -export const votePlaced = ({ activePeer, account, votedList, unvotedList, secondSecret }) => +export const votePlaced = ({ activePeer, account, votes, secondSecret }) => (dispatch) => { - // Make the Api call + const votedList = []; + const unvotedList = []; + + Object.keys(votes).forEach((username) => { + if (!votes[username].confirmed && votes[username].unconfirmed) { + votedList.push(votes[username].publicKey); + } else if (votes[username].confirmed && !votes[username].unconfirmed) { + unvotedList.push(votes[username].publicKey); + } + }); + vote( activePeer, account.passphrase, @@ -86,3 +104,19 @@ export const votesFetched = ({ activePeer, address }) => dispatch(votesAdded({ list: delegates })); }); }; + +/** + * Gets list of all delegates + */ +export const delegatesFetched = ({ activePeer, q, offset, refresh }) => + (dispatch) => { + listDelegates( + activePeer, { + offset, + limit: '100', + q, + }, + ).then(({ delegates, totalCount }) => { + dispatch(delegatesAdded({ list: delegates, totalDelegates: totalCount, refresh })); + }); + }; diff --git a/src/actions/voting.test.js b/src/actions/voting.test.js index 3c08c7de8..e01066d0a 100644 --- a/src/actions/voting.test.js +++ b/src/actions/voting.test.js @@ -14,7 +14,7 @@ import { transactionAdded } from './transactions'; import { errorAlertDialogDisplayed } from './dialog'; import * as delegateApi from '../utils/api/delegate'; -describe('actions: voting', () => { +describe.only('actions: voting', () => { describe('voteToggled', () => { it('should create an action to add data to toggle the vote status for any given delegate', () => { const data = { @@ -32,8 +32,8 @@ describe('actions: voting', () => { describe('votesAdded', () => { it('should create an action to remove data from vote list', () => { const data = [ - { username: 'username1', id: '123HG3452245L' }, - { username: 'username2', id: '123HG3522345L' }, + { username: 'username1', publicKey: '123HG3452245L' }, + { username: 'username2', publicKey: '123HG3522345L' }, ]; const expectedAction = { data, @@ -69,12 +69,11 @@ describe('actions: voting', () => { address: 'test_address', }; const activePeer = {}; - const votedList = []; - const unvotedList = []; const secondSecret = null; + const votes = {}; const actionFunction = votePlaced({ - activePeer, account, votedList, unvotedList, secondSecret, + activePeer, account, votes, secondSecret, }); let dispatch; @@ -140,14 +139,14 @@ describe('actions: voting', () => { address: '8096217735672704724L', }; const votesList = [ - { username: 'username1', id: '80962134535672704724L' }, - { username: 'username2', id: '80962134535672704725L' }, + { username: 'username1', publicKey: '80962134535672704724L' }, + { username: 'username2', publicKey: '80962134535672704725L' }, ]; const actionFunction = votesFetched(data); let dispatch; beforeEach(() => { - delegateApiMock = sinon.stub(delegateApi, 'setSecondPassphrase'); + delegateApiMock = sinon.stub(delegateApi, 'listAccountDelegates'); dispatch = sinon.spy(); }); From 9574797f663086ff58929b2de3c94d278e06832f Mon Sep 17 00:00:00 2001 From: reyraa Date: Fri, 15 Sep 2017 09:36:32 +0200 Subject: [PATCH 06/18] Pass the list of delegates of votes to votingDialog using connect --- src/components/voteDialog/index.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/voteDialog/index.js b/src/components/voteDialog/index.js index 28f3c89eb..350c697a9 100644 --- a/src/components/voteDialog/index.js +++ b/src/components/voteDialog/index.js @@ -1,18 +1,19 @@ import { connect } from 'react-redux'; -import { votePlaced, addedToVoteList, removedFromVoteList } from '../../actions/voting'; +import { votePlaced, voteToggled } from '../../actions/voting'; +import { transactionAdded } from '../../actions/transactions'; import VoteDialog from './voteDialog'; const mapStateToProps = state => ({ - votedList: state.voting.votedList, - unvotedList: state.voting.unvotedList, + votes: state.voting.votes, + delegates: state.voting.delegates, account: state.account, activePeer: state.peers.data, }); const mapDispatchToProps = dispatch => ({ votePlaced: data => dispatch(votePlaced(data)), - addedToVoteList: data => dispatch(addedToVoteList(data)), - removedFromVoteList: data => dispatch(removedFromVoteList(data)), + voteToggled: data => dispatch(voteToggled(data)), + addTransaction: data => dispatch(transactionAdded(data)), }); export default connect(mapStateToProps, mapDispatchToProps)(VoteDialog); From d961ed96ce71328950987f1e0c9b9debf5811bf1 Mon Sep 17 00:00:00 2001 From: reyraa Date: Fri, 15 Sep 2017 09:38:23 +0200 Subject: [PATCH 07/18] Use new actions and delegtes/vote lists in votingDialog --- src/components/voteDialog/voteAutocomplete.js | 41 +++++++++++++------ src/components/voteDialog/voteDialog.js | 18 ++++---- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/components/voteDialog/voteAutocomplete.js b/src/components/voteDialog/voteAutocomplete.js index bccafb528..9203604b8 100644 --- a/src/components/voteDialog/voteAutocomplete.js +++ b/src/components/voteDialog/voteAutocomplete.js @@ -37,7 +37,7 @@ export default class VoteAutocomplete extends React.Component { this.timeout = setTimeout(() => { if (value.length > 0) { if (name === 'votedListSearch') { - voteAutocomplete(this.props.activePeer, value, this.props.voted) + voteAutocomplete(this.props.activePeer, value, this.props.votes) .then((res) => { this.setState({ votedResult: res, @@ -45,7 +45,7 @@ export default class VoteAutocomplete extends React.Component { }); }); } else { - unvoteAutocomplete(value, this.props.voted) + unvoteAutocomplete(value, this.props.votes) .then((res) => { this.setState({ unvotedResult: res, @@ -136,14 +136,16 @@ export default class VoteAutocomplete extends React.Component { this.setState({ [name]: list }); } addToVoted(item) { - this.props.addedToVoteList(item); + const { username, publicKey } = item; + this.props.voteToggled({ username, publicKey }); this.setState({ votedListSearch: '', votedSuggestionClass: styles.hidden, }); } removeFromVoted(item) { - this.props.removedFromVoteList(item); + const { username, publicKey } = item; + this.props.voteToggled({ username, publicKey }); this.setState({ unvotedListSearch: '', unvotedSuggestionClass: styles.hidden, @@ -151,15 +153,29 @@ export default class VoteAutocomplete extends React.Component { } render() { + const { votes } = this.props; + const votedList = []; + const unvotedList = []; + + Object.keys(votes).forEach((delegate) => { + if (!votes[delegate].confirmed && votes[delegate].unconfirmed) { + votedList.push(delegate); + } else if (votes[delegate].confirmed && !votes[delegate].unconfirmed) { + unvotedList.push(delegate); + } + }); + + return (

Add vote to

- {this.props.votedList.map( - item => - {item.username} + onDeleteClick={this.props.voteToggled.bind(this, + { username: item, publicKey: votes[item].publicKey })}> + {item} , )}
@@ -186,11 +202,12 @@ export default class VoteAutocomplete extends React.Component {

Remove vote from

- {this.props.unvotedList.map( - item => - {item.username} + onDeleteClick={this.props.voteToggled.bind(this, + { username: item, publicKey: votes[item].publicKey })}> + {item} , )}
diff --git a/src/components/voteDialog/voteDialog.js b/src/components/voteDialog/voteDialog.js index f1ed4b529..8629893f9 100644 --- a/src/components/voteDialog/voteDialog.js +++ b/src/components/voteDialog/voteDialog.js @@ -26,11 +26,11 @@ export default class VoteDialog extends React.Component { this.props.votePlaced({ activePeer: this.props.activePeer, account: this.props.account, - votedList: this.props.votedList, - unvotedList: this.props.unvotedList, + votes: this.props.votes, secondSecret, }); } + setSecondPass(name, value, error) { this.setState({ [name]: { @@ -41,14 +41,15 @@ export default class VoteDialog extends React.Component { } render() { + const { votes } = this.props; + const votesList = Object.keys(votes).filter(delegate => + votes[delegate].confirmed !== votes[delegate].unconfirmed); return (
Date: Fri, 15 Sep 2017 09:54:10 +0200 Subject: [PATCH 08/18] Improvements in voting reducer to reduce the number of iterations required to keep the voting list update --- src/store/reducers/voting.js | 30 ++++-- src/store/reducers/voting.test.js | 156 +++++++++++++++++------------- 2 files changed, 113 insertions(+), 73 deletions(-) diff --git a/src/store/reducers/voting.js b/src/store/reducers/voting.js index 402c24e90..36d88f865 100644 --- a/src/store/reducers/voting.js +++ b/src/store/reducers/voting.js @@ -6,29 +6,40 @@ import actionTypes from '../../constants/actions'; * @param {Object} state * @param {Object} action */ -const voting = (state = { votes: {}, delegates: [] }, action) => { +const voting = (state = { votes: {}, delegates: [], totalDelegates: 0 }, action) => { switch (action.type) { case actionTypes.votesAdded: return Object.assign({}, state, { votes: action.data.list .reduce((votesDict, delegate) => { - votesDict[delegate.username] = { confirmed: true, unconfirmed: true }; + votesDict[delegate.username] = { + confirmed: true, + unconfirmed: true, + publicKey: delegate.publicKey, + }; return votesDict; }, {}), - delegates: action.data.list, + refresh: false, }); case actionTypes.delegatesAdded: return Object.assign({}, state, { - delegates: [...state.delegates, ...action.data.list], + delegates: action.data.refresh ? action.data.list : + [...state.delegates, ...action.data.list], + totalDelegates: action.data.totalCount, + refresh: true, }); case actionTypes.voteToggled: return Object.assign({}, state, { + refresh: false, votes: Object.assign({}, state.votes, { - [action.data]: { - confirmed: state.votes[action.data] ? state.votes[action.data].confirmed : false, - unconfirmed: state.votes[action.data] ? !state.votes[action.data].unconfirmed : true, + [action.data.username]: { + confirmed: state.votes[action.data.username] ? + state.votes[action.data.username].confirmed : false, + unconfirmed: state.votes[action.data.username] ? + !state.votes[action.data.username].unconfirmed : true, + publicKey: action.data.publicKey, }, }), }); @@ -47,6 +58,7 @@ const voting = (state = { votes: {}, delegates: [] }, action) => { votesDict[username] = { confirmed: state.votes[username].confirmed, unconfirmed: state.votes[username].confirmed, + publicKey: state.votes[username].publicKey, pending: false, }; return votesDict; @@ -56,14 +68,16 @@ const voting = (state = { votes: {}, delegates: [] }, action) => { case actionTypes.pendingVotesAdded: return Object.assign({}, state, { + refresh: false, votes: Object.keys(state.votes).reduce((votesDict, username) => { const pending = state.votes[username].confirmed !== state.votes[username].unconfirmed; - const { confirmed, unconfirmed } = state.votes[username]; + const { confirmed, unconfirmed, publicKey } = state.votes[username]; votesDict[username] = { confirmed: pending ? !confirmed : confirmed, unconfirmed, pending, + publicKey, }; return votesDict; }, {}), diff --git a/src/store/reducers/voting.test.js b/src/store/reducers/voting.test.js index 989dd9cd3..e351d06a4 100644 --- a/src/store/reducers/voting.test.js +++ b/src/store/reducers/voting.test.js @@ -3,16 +3,42 @@ import actionTypes from '../../constants/actions'; import voting from './voting'; describe('Reducer: voting(state, action)', () => { - const state = { - votes: { - username1: { confirmed: true, unconfirmed: true }, - username2: { confirmed: false, unconfirmed: false }, - }, + const initialState = { votes: {}, delegates: [], refresh: true }; + const cleanVotes = { + username1: { confirmed: false, unconfirmed: false, publicKey: 'sample_key' }, + username2: { confirmed: true, unconfirmed: true, publicKey: 'sample_key' }, + username3: { confirmed: false, unconfirmed: false, publicKey: 'sample_key' }, }; + const dirtyVotes = { + username1: { confirmed: false, unconfirmed: true, publicKey: 'sample_key' }, + username2: { confirmed: true, unconfirmed: true, publicKey: 'sample_key' }, + username3: { confirmed: false, unconfirmed: false, publicKey: 'sample_key' }, + }; + const pendingVotes = { + username1: { confirmed: true, unconfirmed: true, pending: true, publicKey: 'sample_key' }, + username2: { confirmed: true, unconfirmed: true, pending: false, publicKey: 'sample_key' }, + username3: { confirmed: false, unconfirmed: false, pending: false, publicKey: 'sample_key' }, + }; + const restoredVotes = { + username1: { confirmed: false, unconfirmed: false, pending: false, publicKey: 'sample_key' }, + username2: { confirmed: true, unconfirmed: true, pending: false, publicKey: 'sample_key' }, + username3: { confirmed: false, unconfirmed: false, pending: false, publicKey: 'sample_key' }, + }; + const delegates1 = [ + { username: 'username1', publicKey: 'sample_key' }, + { username: 'username2', publicKey: 'sample_key' }, + ]; + const delegates2 = [ + { username: 'username3', publicKey: 'sample_key' }, + { username: 'username4', publicKey: 'sample_key' }, + ]; + const fullDelegates = [...delegates1, ...delegates2]; + it('should return default state if action does not match', () => { const action = { type: '', }; + const state = { votes: cleanVotes }; const changedState = voting(state, action); expect(changedState).to.be.equal(state); @@ -22,52 +48,72 @@ describe('Reducer: voting(state, action)', () => { const action = { type: actionTypes.accountLoggedOut, }; + const state = { votes: cleanVotes, delegates: fullDelegates, refresh: false }; const changedState = voting(state, action); - const expectedState = { votes: {}, delegates: [], refresh: true }; - expect(changedState).to.be.deep.equal(expectedState); + expect(changedState).to.be.deep.equal(initialState); }); - it('should fill delegates list with action: votesAdded', () => { + it('should fill votes object with action: votesAdded', () => { const action = { type: actionTypes.votesAdded, data: { - list: [ - { username: 'username1', id: '123HGJ123234L' }, - ], + list: delegates1, }, }; const expectedState = { votes: { - username1: { confirmed: true, unconfirmed: true }, + username1: { confirmed: true, unconfirmed: true, publicKey: 'sample_key' }, + username2: { confirmed: true, unconfirmed: true, publicKey: 'sample_key' }, }, - delegates: [{ username: 'username1', id: '123HGJ123234L' }], + delegates: [], + refresh: false, }; - const oldState = { votes: {}, delegates: [] }; - const changedState = voting(oldState, action); + const changedState = voting(initialState, action); expect(changedState).to.be.deep.equal(expectedState); }); - it('should fill delegates list with action: delegatesAdded', () => { + it('should append to delegates list with action: delegatesAdded, refresh: false', () => { const action = { type: actionTypes.delegatesAdded, data: { - list: [ - { username: 'username1', id: '123HGJ123234L' }, - ], + list: delegates2, + totalCount: 100, + refresh: false, }, }; - const oldState = { - delegates: [{ username: 'username2', id: '123HGJ123235L' }], + const state = { + delegates: delegates1, }; const expectedState = { - delegates: [ - { username: 'username2', id: '123HGJ123235L' }, - { username: 'username1', id: '123HGJ123234L' }, - ], + delegates: fullDelegates, + refresh: true, + totalDelegates: 100, }; - const changedState = voting(oldState, action); + const changedState = voting(state, action); + + expect(changedState.delegates).to.be.deep.equal(expectedState.delegates); + }); + + it('should replace to delegates list with action: delegatesAdded, refresh: true', () => { + const action = { + type: actionTypes.delegatesAdded, + data: { + list: delegates1, + totalCount: 100, + refresh: true, + }, + }; + const state = { + delegates: delegates2, + }; + const expectedState = { + delegates: delegates1, + refresh: true, + totalDelegates: 100, + }; + const changedState = voting(state, action); expect(changedState).to.be.deep.equal(expectedState); }); @@ -75,13 +121,12 @@ describe('Reducer: voting(state, action)', () => { it('should toggle unconfirmed state, with action: voteToggled', () => { const action = { type: actionTypes.voteToggled, - data: 'username1', + data: delegates1[0], }; + const state = { votes: cleanVotes }; const expectedState = { - votes: { - username1: { confirmed: true, unconfirmed: false }, - username2: { confirmed: false, unconfirmed: false }, - }, + votes: dirtyVotes, + refresh: false, }; const changedState = voting(state, action); @@ -91,16 +136,16 @@ describe('Reducer: voting(state, action)', () => { it('should add to votes dictionary in not exist, with action: voteToggled', () => { const action = { type: actionTypes.voteToggled, - data: 'username3', + data: delegates1[0], }; const expectedState = { votes: { - username1: { confirmed: true, unconfirmed: true }, - username2: { confirmed: false, unconfirmed: false }, - username3: { confirmed: false, unconfirmed: true }, + [delegates1[0].username]: dirtyVotes[delegates1[0].username], }, + delegates: [], + refresh: false, }; - const changedState = voting(state, action); + const changedState = voting(initialState, action); expect(changedState).to.be.deep.equal(expectedState); }); @@ -109,24 +154,14 @@ describe('Reducer: voting(state, action)', () => { const action = { type: actionTypes.pendingVotesAdded, }; - const oldState = { - votes: { - username1: { confirmed: true, unconfirmed: false }, - username2: { confirmed: false, unconfirmed: true }, - username3: { confirmed: true, unconfirmed: true }, - username4: { confirmed: false, unconfirmed: false }, - }, + const state = { + votes: dirtyVotes, }; const expectedState = { - votes: { - username1: { confirmed: false, unconfirmed: false, pending: true }, - username2: { confirmed: true, unconfirmed: true, pending: true }, - username3: { confirmed: true, unconfirmed: true, pending: false }, - username4: { confirmed: false, unconfirmed: false, pending: false }, - }, + votes: pendingVotes, + refresh: false, }; - const changedState = voting(oldState, action); - + const changedState = voting(state, action); expect(changedState).to.be.deep.equal(expectedState); }); @@ -134,24 +169,15 @@ describe('Reducer: voting(state, action)', () => { const action = { type: actionTypes.votesCleared, }; - const oldState = { - votes: { - username1: { confirmed: true, unconfirmed: false }, - username2: { confirmed: false, unconfirmed: true }, - username3: { confirmed: true, unconfirmed: true }, - username4: { confirmed: false, unconfirmed: false }, - }, + const state = { + votes: dirtyVotes, }; + const expectedState = { - votes: { - username1: { confirmed: true, unconfirmed: true, pending: false }, - username2: { confirmed: false, unconfirmed: false, pending: false }, - username3: { confirmed: true, unconfirmed: true, pending: false }, - username4: { confirmed: false, unconfirmed: false, pending: false }, - }, + votes: restoredVotes, refresh: true, }; - const changedState = voting(oldState, action); + const changedState = voting(state, action); expect(changedState).to.be.deep.equal(expectedState); }); From 1b0efbcad0d4bc466121ff8c305df9b9197fce53 Mon Sep 17 00:00:00 2001 From: reyraa Date: Fri, 15 Sep 2017 14:40:46 +0200 Subject: [PATCH 09/18] Update utitilies and adapt its unit tests --- src/utils/api/delegate.js | 11 +++++++---- src/utils/api/delegate.test.js | 10 +++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/utils/api/delegate.js b/src/utils/api/delegate.js index 5cc5325e3..3ee464acd 100644 --- a/src/utils/api/delegate.js +++ b/src/utils/api/delegate.js @@ -14,8 +14,8 @@ export const vote = (activePeer, secret, publicKey, voteList, unvoteList, second requestToActivePeer(activePeer, 'accounts/delegates', { secret, publicKey, - delegates: voteList.map(delegate => `+${delegate.publicKey}`).concat( - unvoteList.map(delegate => `-${delegate.publicKey}`), + delegates: voteList.map(delegate => `+${delegate}`).concat( + unvoteList.map(delegate => `-${delegate}`), ), secondSecret, }); @@ -27,7 +27,7 @@ export const voteAutocomplete = (activePeer, username, votedList) => { listDelegates(activePeer, options) .then((response) => { resolve(response.delegates.filter(delegate => - votedList.filter(item => item.username === delegate.username).length === 0, + Object.keys(votedList).filter(item => item === delegate.username).length === 0, )); }) .catch(reject), @@ -36,7 +36,10 @@ export const voteAutocomplete = (activePeer, username, votedList) => { export const unvoteAutocomplete = (username, votedList) => new Promise((resolve) => { - resolve(votedList.filter(delegate => delegate.username.indexOf(username) !== -1)); + resolve( + Object.keys(votedList) + .filter(delegate => delegate.indexOf(username) !== -1) + .map(element => ({ username: element, publicKey: votedList[element].publicKey }))); }); export const registerDelegate = (activePeer, username, secret, secondSecret = null) => { diff --git a/src/utils/api/delegate.test.js b/src/utils/api/delegate.test.js index c86980ac1..97e6f8370 100644 --- a/src/utils/api/delegate.test.js +++ b/src/utils/api/delegate.test.js @@ -70,15 +70,19 @@ describe('Utils: Delegate', () => { describe('unvoteAutocomplete', () => { it('should return a promise', () => { - const votedList = ['genesis_1', 'genesis_2', 'genesis_3']; + const voteList = { + genesis_1: { confirmed: true, unconfirmed: false, publicKey: 'sample_key' }, + genesis_2: { confirmed: true, unconfirmed: false, publicKey: 'sample_key' }, + genesis_3: { confirmed: true, unconfirmed: false, publicKey: 'sample_key' }, + }; const nonExistingUsername = 'genesis_4'; - const promise = unvoteAutocomplete(username, votedList); + const promise = unvoteAutocomplete(username, voteList); expect(typeof promise.then).to.be.equal('function'); promise.then((result) => { expect(result).to.be.equal(true); }); - unvoteAutocomplete(nonExistingUsername, votedList).then((result) => { + unvoteAutocomplete(nonExistingUsername, voteList).then((result) => { expect(result).to.be.equal(false); }); }); From eb5e7e594661f2108b621f599d2d9e9303ea176a Mon Sep 17 00:00:00 2001 From: reyraa Date: Fri, 15 Sep 2017 14:42:09 +0200 Subject: [PATCH 10/18] Adapt actions unit tests --- src/actions/voting.js | 1 + src/actions/voting.test.js | 54 +++++++++++++++++++++++++++++--------- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/actions/voting.js b/src/actions/voting.js index 8502be1c5..0757c31d3 100644 --- a/src/actions/voting.js +++ b/src/actions/voting.js @@ -54,6 +54,7 @@ export const votePlaced = ({ activePeer, account, votes, secondSecret }) => const unvotedList = []; Object.keys(votes).forEach((username) => { + /* istanbul ignore else */ if (!votes[username].confirmed && votes[username].unconfirmed) { votedList.push(votes[username].publicKey); } else if (votes[username].confirmed && !votes[username].unconfirmed) { diff --git a/src/actions/voting.test.js b/src/actions/voting.test.js index e01066d0a..6261d33f4 100644 --- a/src/actions/voting.test.js +++ b/src/actions/voting.test.js @@ -8,13 +8,15 @@ import { voteToggled, votePlaced, votesFetched, + delegatesFetched, + delegatesAdded, } from './voting'; import Fees from '../constants/fees'; import { transactionAdded } from './transactions'; import { errorAlertDialogDisplayed } from './dialog'; import * as delegateApi from '../utils/api/delegate'; -describe.only('actions: voting', () => { +describe('actions: voting', () => { describe('voteToggled', () => { it('should create an action to add data to toggle the vote status for any given delegate', () => { const data = { @@ -70,7 +72,10 @@ describe.only('actions: voting', () => { }; const activePeer = {}; const secondSecret = null; - const votes = {}; + const votes = { + username1: { publicKey: 'sample_key', confirmed: true, unconfirmed: false }, + username2: { publicKey: 'sample_key', confirmed: false, unconfirmed: true }, + }; const actionFunction = votePlaced({ activePeer, account, votes, secondSecret, @@ -133,37 +138,60 @@ describe.only('actions: voting', () => { }); describe('votesFetched', () => { - let delegateApiMock; const data = { activePeer: {}, address: '8096217735672704724L', }; - const votesList = [ + const delegates = [ { username: 'username1', publicKey: '80962134535672704724L' }, { username: 'username2', publicKey: '80962134535672704725L' }, ]; const actionFunction = votesFetched(data); - let dispatch; - beforeEach(() => { - delegateApiMock = sinon.stub(delegateApi, 'listAccountDelegates'); - dispatch = sinon.spy(); + it('should create an action function', () => { + expect(typeof actionFunction).to.be.deep.equal('function'); }); - afterEach(() => { + it.skip('should dispatch votesAdded action if resolved', () => { + const delegateApiMock = sinon.stub(delegateApi, 'listAccountDelegates'); + const dispatch = sinon.spy(); + + delegateApiMock.returnsPromise().resolves({ delegates }); + const expectedAction = { list: delegates }; + + actionFunction(dispatch); + expect(dispatch).to.have.been.calledWith(votesAdded(expectedAction)); delegateApiMock.restore(); }); + }); + + describe('delegatesFetched', () => { + const data = { + activePeer: {}, + q: '', + offset: 0, + refresh: true, + }; + const delegates = [ + { username: 'username1', publicKey: '80962134535672704724L' }, + { username: 'username2', publicKey: '80962134535672704725L' }, + ]; + const actionFunction = delegatesFetched(data); it('should create an action function', () => { expect(typeof actionFunction).to.be.deep.equal('function'); }); - it('should dispatch votesAdded action if resolved', () => { - delegateApiMock.returnsPromise().resolves(votesList); - const expectedAction = { list: votesList }; + it('should dispatch delegatesAdded action if resolved', () => { + const delegateApiMock = sinon.stub(delegateApi, 'listDelegates'); + const dispatch = sinon.spy(); + + delegateApiMock.returnsPromise().resolves({ delegates, totalCount: 10 }); + const expectedAction = { list: delegates, totalDelegates: 10, refresh: true }; actionFunction(dispatch); - expect(dispatch).to.have.been.calledWith(transactionAdded(expectedAction)); + expect(dispatch).to.have.been.calledWith(delegatesAdded(expectedAction)); + delegateApiMock.restore(); }); }); }); From 092e61895eef25556ed569dcea60882497ad371c Mon Sep 17 00:00:00 2001 From: reyraa Date: Fri, 15 Sep 2017 14:43:17 +0200 Subject: [PATCH 11/18] - Store the list of delegates and votes on store. -Update component accordingly. - Adapt unit tests --- src/components/voting/index.js | 6 +- src/components/voting/index.test.js | 8 +- src/components/voting/voteCheckbox.js | 11 +- src/components/voting/voteCheckbox.test.js | 62 +++++---- src/components/voting/voting.js | 125 ++++++------------ src/components/voting/voting.test.js | 119 ++++++----------- src/components/voting/votingHeader.js | 17 ++- src/components/voting/votingHeader.test.js | 142 +++++++++++++-------- src/components/voting/votingRow.js | 30 +++-- src/components/voting/votingRow.test.js | 54 ++++---- 10 files changed, 273 insertions(+), 301 deletions(-) diff --git a/src/components/voting/index.js b/src/components/voting/index.js index e9fa062a1..e7a26e55c 100644 --- a/src/components/voting/index.js +++ b/src/components/voting/index.js @@ -1,7 +1,6 @@ import { connect } from 'react-redux'; import { dialogDisplayed } from '../../actions/dialog'; -import { voteToggled, votesFetched } from '../../actions/voting'; -import { transactionAdded } from '../../actions/transactions'; +import { voteToggled, votesFetched, delegatesFetched } from '../../actions/voting'; import Voting from './voting'; const mapStateToProps = state => ({ @@ -9,14 +8,15 @@ const mapStateToProps = state => ({ activePeer: state.peers.data, votes: state.voting.votes, delegates: state.voting.delegates, + totalDelegates: state.voting.totalDelegates, refreshDelegates: state.voting.refresh, }); const mapDispatchToProps = dispatch => ({ setActiveDialog: data => dispatch(dialogDisplayed(data)), voteToggled: data => dispatch(voteToggled(data)), - addTransaction: data => dispatch(transactionAdded(data)), votesFetched: data => dispatch(votesFetched(data)), + delegatesFetched: data => dispatch(delegatesFetched(data)), }); export default connect(mapStateToProps, mapDispatchToProps)(Voting); diff --git a/src/components/voting/index.test.js b/src/components/voting/index.test.js index 9ab8859d6..ae071607b 100644 --- a/src/components/voting/index.test.js +++ b/src/components/voting/index.test.js @@ -16,8 +16,12 @@ describe('VotingHOC', () => { confirmed: [], }, voting: { - votedList: [], - unvotedList: [], + delegates: [ + { username: 'username1', publicKey: 'sample_key' }, + ], + votes: { + username1: { confirmed: true, unconfirmed: true, publicKey: 'sample_key' }, + }, }, account: {}, }); diff --git a/src/components/voting/voteCheckbox.js b/src/components/voting/voteCheckbox.js index 78bf5acf2..ce78d4de1 100644 --- a/src/components/voting/voteCheckbox.js +++ b/src/components/voting/voteCheckbox.js @@ -2,13 +2,14 @@ import React from 'react'; import Checkbox from 'react-toolbox/lib/checkbox'; import Spinner from '../spinner'; -const VoteCheckbox = (props) => { - const template = props.status && props.status.pending ? +const VoteCheckbox = ({ data, status, styles, toggle }) => { + const { username, publicKey } = data; + const template = status && status.pending ? : ; return template; }; diff --git a/src/components/voting/voteCheckbox.test.js b/src/components/voting/voteCheckbox.test.js index 998ce0fd3..5791739b9 100644 --- a/src/components/voting/voteCheckbox.test.js +++ b/src/components/voting/voteCheckbox.test.js @@ -2,47 +2,61 @@ import React from 'react'; import { expect } from 'chai'; import sinon from 'sinon'; import { mount } from 'enzyme'; -import configureStore from 'redux-mock-store'; import VoteCheckbox from './voteCheckbox'; import styles from './voting.css'; -const mockStore = configureStore(); - describe('VoteCheckbox', () => { - let wrapper; const props = { - store: mockStore({ runtime: {} }), data: { username: 'yashar', - address: 'address 1', + publicKey: 'address 1', }, styles, - pending: false, - value: true, - addToVoteList: sinon.spy(), - removeFromVoteList: sinon.spy(), + toggle: sinon.spy(), }; + const voteStatus = { confirmed: false, unconfirmed: true }; + const unvoteStatus = { confirmed: true, unconfirmed: false }; + const pendingStatus = { confirmed: true, unconfirmed: true, pending: true }; - beforeEach(() => { - wrapper = mount(); - }); + describe('General', () => { + let wrapper; + beforeEach(() => { + wrapper = mount(); + }); - it('should render a Spinner When pending is true', () => { - wrapper.setProps({ pending: true }); - expect(wrapper.find('Spinner').exists()).to.be.equal(true); + it('should render a Checkbox', () => { + expect(wrapper.find('Checkbox').exists()).to.be.equal(true); + }); + + it('should Checkbox change event should call props.toggle', () => { + wrapper.find('input').simulate('click'); + expect(props.toggle).to.have.been.calledWith(props.data); + }); }); - it('should render a Checkbox is false', () => { - expect(wrapper.find('Checkbox').exists()).to.be.equal(true); + describe('To show vote', () => { + it('should render a Checkbox', () => { + const wrapper = mount(); + expect(wrapper.find('input').props().checked).to.equal(true); + }); }); - it('should Checkbox change event should call this.props.addToVoteList when value is true', () => { - wrapper.instance().toggle(props.data, true); - expect(props.addToVoteList).to.have.been.calledWith(props.data); + describe('To show unvote', () => { + it('should render a Checkbox', () => { + const wrapper = mount(); + expect(wrapper.find('input').props().checked).to.equal(false); + }); + + it('should render a Checkbox even if status is not passed', () => { + const wrapper = mount(); + expect(wrapper.find('input').props().checked).to.equal(false); + }); }); - it('should Checkbox change event should call this.props.removeFromVoteList when value is false', () => { - wrapper.instance().toggle(props.data, false); - expect(props.removeFromVoteList).to.have.been.calledWith(props.data); + describe('To show pending', () => { + it('should render a Spinner When pending is true', () => { + const wrapper = mount(); + expect(wrapper.find('Spinner').exists()).to.be.equal(true); + }); }); }); diff --git a/src/components/voting/voting.js b/src/components/voting/voting.js index 5e3085d6b..780291f17 100644 --- a/src/components/voting/voting.js +++ b/src/components/voting/voting.js @@ -5,7 +5,6 @@ import { tableFactory } from 'react-toolbox/lib/table/Table'; import { TableHead, TableCell } from 'react-toolbox/lib/table'; import TableTheme from 'react-toolbox/lib/table/theme.css'; import Waypoint from 'react-waypoint'; -import { listDelegates } from '../../utils/api/delegate'; import Header from './votingHeader'; import VotingRow from './votingRow'; @@ -18,52 +17,41 @@ class Voting extends React.Component { this.state = { delegates: [], selected: [], - offset: 0, - loadMore: false, length: 1, notFound: '', }; + this.freezeLoading = false; + this.offset = -1; this.query = ''; } - componentWillReceiveProps() { - // setTimeout(() => { - // if (this.props.refreshDelegates) { - // console.log('load voted'); - // this.loadVotedDelegates(true); - // } else { - // console.log('load the rest'); - // const delegates = this.state.delegates.map(delegate => this.setStatus(delegate)); - // this.setState({ - // delegates, - // }); - // } - // }, 1); + componentWillUpdate(nextProps) { + setTimeout(() => { + if (this.props.refreshDelegates) { + this.loadVotedDelegates(true); + } + }, 1); + if (this.props.delegates.length < nextProps.delegates.length) { + setTimeout(() => { + this.freezeLoading = false; + this.offset = nextProps.delegates.length; + }, 5); + } } componentDidMount() { - this.loadVotedDelegates(); + this.loadVotedDelegates(true); } loadVotedDelegates(refresh) { - this.props.votesFetched({ activePeer: this.props.activePeer, - address: this.props.address }); - - // listAccountDelegates(this.props.activePeer, this.props.address).then((res) => { - // if (refresh) { - // setTimeout(() => { - // const delegates = this.state.delegates.map(delegate => this.setStatus(delegate)); - // this.setState({ - // delegates, - // }); - // }, 10); - // } else { - // this.loadDelegates(this.query); - // } - // }) - // .catch(() => { - // this.loadDelegates(this.query); - // }); + /* istanbul-ignore-else */ + if (!this.freezeLoading) { + this.props.votesFetched({ + activePeer: this.props.activePeer, + address: this.props.address, + }); + this.loadDelegates('', refresh); + } } /** @@ -76,8 +64,8 @@ class Voting extends React.Component { offset: 0, delegates: [], length: 1, - loadMore: false, }); + this.freezeLoading = false; setTimeout(() => { this.loadDelegates(this.query); }, 1); @@ -91,66 +79,29 @@ class Voting extends React.Component { * should replace the old delegates list * @param {Number} limit - The maximum number of results */ - loadDelegates(search, limit = 100) { - this.setState({ loadMore: false }); - - listDelegates( - this.props.activePeer, { - offset: this.state.offset, - limit: limit.toString(), - q: search, - }, - ).then((res) => { - const delegatesList = res.delegates - .map(delegate => this.setStatus(delegate)); - this.setState({ - delegates: [...this.state.delegates, ...delegatesList], - offset: this.state.offset + delegatesList.length, - length: parseInt(res.totalCount, 10), - loadMore: true, - notFound: delegatesList.length > 0 ? '' :
No delegates found
, - }); + loadDelegates(search = '', refresh) { + this.freezeLoading = true; + this.offset = refresh ? -1 : this.offset; + this.props.delegatesFetched({ + activePeer: this.props.activePeer, + offset: this.offset > -1 ? this.offset : 0, + q: search || '', + refresh, }); } - /** - * Sets delegate.status to be always the same object for given delegate.address - */ - setStatus(delegate) { - let delegateExisted = false; - if (this.props.unvotedList.length > 0) { - this.props.unvotedList.forEach((row) => { - if (row.address === delegate.address) { - delegateExisted = row; - } - }); - } - if (this.props.votedList.length > 0) { - this.props.votedList.forEach((row) => { - if (row.address === delegate.address) { - delegateExisted = row; - } - }); - } - if (delegateExisted) { - return delegateExisted; - } - - const voted = this.props.confirmedVotedList - .filter(row => row.username === delegate.username).length > 0; - return Object.assign(delegate, { voted }, { selected: voted }, { pending: false }); - } - /** * load more data when scroll bar reaches end of the page */ loadMore() { - if (this.state.loadMore && this.state.length > this.state.offset) { + /* istanbul-ignore-else */ + if (!this.freezeLoading && this.props.totalDelegates > this.offset) { this.loadDelegates(this.query); } } render() { + // .log(this.props.votes.cc001); return (
{this.state.notFound} + scrollableAncestor={window} + key={this.state.delegates.length} + onEnter={this.loadMore.bind(this)}>
); } diff --git a/src/components/voting/voting.test.js b/src/components/voting/voting.test.js index 713a0f031..25f70984f 100644 --- a/src/components/voting/voting.test.js +++ b/src/components/voting/voting.test.js @@ -5,47 +5,39 @@ import PropTypes from 'prop-types'; import sinon from 'sinon'; import Voting from './voting'; import store from '../../store'; -import * as delegateApi from '../../utils/api/delegate'; describe('Voting', () => { let wrapper; - const listAccountDelegatesMock = sinon.stub(delegateApi, 'listAccountDelegates'); - let listDelegatesMock; - listAccountDelegatesMock.returnsPromise().resolves({ - delegates: [ - { - address: 'address 1', - }, - { - address: 'address 1', - }, - ], - }); + const delegates = [ + { + address: 'address 1', + username: 'username1', + publicKey: 'sample_key', + }, + { + address: 'address 2', + username: 'username2', + publicKey: 'sample_key', + }, + ]; + const votes = { + username1: { confirmed: true, unconfirmed: true, publicKey: 'sample_key' }, + }; const props = { refreshDelegates: false, + delegates, + totalDelegates: 10, + votes, activePeer: {}, address: '16313739661670634666L', - votedList: [], - unvotedList: [], - addToUnvoted: sinon.spy(), + setActiveDialog: sinon.spy(), + voteToggled: sinon.spy(), + addTransaction: sinon.spy(), + votesFetched: sinon.spy(), + delegatesFetched: sinon.spy(), }; beforeEach(() => { - sinon.spy(Voting.prototype, 'loadVotedDelegates'); - sinon.spy(Voting.prototype, 'loadDelegates'); - sinon.spy(Voting.prototype, 'setStatus'); - listDelegatesMock = sinon.stub(delegateApi, 'listDelegates'); - listDelegatesMock.returnsPromise().resolves({ - delegates: [ - { - address: 'address 1', - }, - { - address: 'address 1', - }, - ], - totalCount: 110, - }); wrapper = mount(, { context: { store }, @@ -55,63 +47,32 @@ describe('Voting', () => { }); afterEach(() => { - Voting.prototype.loadVotedDelegates.restore(); - Voting.prototype.loadDelegates.restore(); - Voting.prototype.setStatus.restore(); - listDelegatesMock.restore(); + // Voting.prototype.setStatus.restore(); }); - it('should call "loadVotedDelegates" after component did mount', () => { - expect(Voting.prototype.loadVotedDelegates).to.have.property('callCount', 1); - expect(wrapper.state('votedDelegates')).to.have.lengthOf(2); - expect(Voting.prototype.loadDelegates).to.have.property('callCount', 1); - expect(Voting.prototype.setStatus).to.have.property('callCount', 2); + it('should render VotingHeader', () => { + expect(wrapper.find('VotingHeader')).to.have.lengthOf(1); }); - - it('should call "loadVotedDelegates" twice when "refreshDelegates" is changed to true', () => { - const clock = sinon.useFakeTimers(); - clock.tick(100); - wrapper.setProps({ refreshDelegates: true }); - // it should triger 'wrapper.loadDelegates' after 1 ms - clock.tick(1); - - expect(Voting.prototype.loadVotedDelegates).to.have.property('callCount', 2); - clock.tick(10); - expect(Voting.prototype.loadDelegates).to.have.property('callCount', 1); + it('should render VotingRow', () => { + expect(wrapper.find('VotingRow')).to.have.lengthOf(delegates.length); }); - - it('should call "loadVotedDelegates" once when "refreshDelegates" is not changed', () => { - const clock = sinon.useFakeTimers(); - clock.tick(100); - wrapper.setProps({ votedList: [] }); - // it should triger 'wrapper.loadDelegates' after 1 ms - clock.tick(1); - - expect(Voting.prototype.loadVotedDelegates).to.have.property('callCount', 1); - clock.tick(10); - expect(Voting.prototype.setStatus).to.have.property('callCount', 4); - }); - - it('should "loadMore" calls "loadDelegates" when state.loadMore is true', () => { - wrapper.instance().loadMore(); - expect(Voting.prototype.loadDelegates).to.have.property('callCount', 2); + it('should render Table', () => { + expect(wrapper.find('Table')).to.have.lengthOf(1); }); - it('should "search" function call "loadDelegates"', () => { - wrapper.instance().search('query'); + it('should define search method to reload delegates based on given query', () => { const clock = sinon.useFakeTimers(); - // it should triger 'wrapper.loadDelegates' after 1 ms - clock.tick(100); - expect(wrapper.instance().query).to.be.equal('query'); - }); - it('should render VotingHeader', () => { - expect(wrapper.find('VotingHeader')).to.have.lengthOf(1); - }); - - it('should render Table', () => { - expect(wrapper.find('Table')).to.have.lengthOf(1); + wrapper.instance().search('query'); + clock.tick(2); + expect(props.delegatesFetched).to.be.calledWith({ + activePeer: props.activePeer, + offset: 0, + q: 'query', + refresh: undefined, + }); + clock.restore(); }); }); diff --git a/src/components/voting/votingHeader.js b/src/components/voting/votingHeader.js index f4d81d20e..4f702910c 100644 --- a/src/components/voting/votingHeader.js +++ b/src/components/voting/votingHeader.js @@ -46,10 +46,10 @@ class VotingHeader extends React.Component { const unvoted = votesList.filter(item => votes[item].confirmed && !votes[item].unconfirmed).length; if (voted > 0 || unvoted > 0) { - const seprator = (voted > 0 && unvoted > 0) ? ' / ' : ''; // eslint-disable-line + const separator = (voted > 0 && unvoted > 0) ? ' / ' : ''; // eslint-disable-line const votedHtml = voted > 0 ? +{voted} : ''; const unvotedHtml = unvoted > 0 ? -{unvoted} : ''; - info = VOTE ({votedHtml}{seprator}{unvotedHtml}); + info = VOTE ({votedHtml}{separator}{unvotedHtml}); } return info; } @@ -78,13 +78,16 @@ class VotingHeader extends React.Component {
- {votesList.map(delegate => + {votesList.map(username => )} + key={username} + caption={username} + icon={(votes[username].confirmed === votes[username].unconfirmed) ? 'clear' : 'add'} + onClick={this.props.voteToggled.bind(this, { + username, + publicKey: votes[username].publicKey, + })} />)}
); diff --git a/src/components/voting/voting.test.js b/src/components/voting/voting.test.js index 25f70984f..64a7592e8 100644 --- a/src/components/voting/voting.test.js +++ b/src/components/voting/voting.test.js @@ -64,13 +64,14 @@ describe('Voting', () => { it('should define search method to reload delegates based on given query', () => { const clock = sinon.useFakeTimers(); + props.delegatesFetched.reset(); wrapper.instance().search('query'); clock.tick(2); expect(props.delegatesFetched).to.be.calledWith({ activePeer: props.activePeer, offset: 0, q: 'query', - refresh: undefined, + refresh: true, }); clock.restore(); }); From 80e5d21b8898c342d4f8e17ad54cc23aa257a0bf Mon Sep 17 00:00:00 2001 From: reyraa Date: Mon, 18 Sep 2017 11:13:21 +0200 Subject: [PATCH 14/18] Fix a UI issue with autocomplete --- src/components/voteDialog/voteAutocomplete.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/voteDialog/voteAutocomplete.js b/src/components/voteDialog/voteAutocomplete.js index 9203604b8..d727d0277 100644 --- a/src/components/voteDialog/voteAutocomplete.js +++ b/src/components/voteDialog/voteAutocomplete.js @@ -86,13 +86,17 @@ export default class VoteAutocomplete extends React.Component { case 38: // 38 is keyCode of arrow up key in keyboard this.handleArrowUp(this.state[listName], listName); return false; - case 27 : // 27 is keyCode of enter key in keyboard + case 27 : // 27 is keyCode of escape key in keyboard this.setState({ [className]: styles.hidden, }); return false; - case 13 : // 27 is keyCode of escape key in keyboard + case 13 : // 13 is keyCode of enter key in keyboard if (selected.length > 0) { + selected[0].hovered = false; + this.setState({ + [listName]: this.state[listName], + }); this[selectFunc](selected[0]); } return false; From ce264a6aa99e8e3fbe4898a55e7c4c6252224cd5 Mon Sep 17 00:00:00 2001 From: reyraa Date: Mon, 18 Sep 2017 12:37:53 +0200 Subject: [PATCH 15/18] Rename some variables to reflect their new specifications --- src/store/reducers/voting.js | 2 +- src/utils/api/delegate.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/store/reducers/voting.js b/src/store/reducers/voting.js index 36d88f865..47c9eabf6 100644 --- a/src/store/reducers/voting.js +++ b/src/store/reducers/voting.js @@ -26,7 +26,7 @@ const voting = (state = { votes: {}, delegates: [], totalDelegates: 0 }, action) return Object.assign({}, state, { delegates: action.data.refresh ? action.data.list : [...state.delegates, ...action.data.list], - totalDelegates: action.data.totalCount, + totalDelegates: action.data.totalDelegates, refresh: true, }); diff --git a/src/utils/api/delegate.js b/src/utils/api/delegate.js index 3ee464acd..22f24a5d7 100644 --- a/src/utils/api/delegate.js +++ b/src/utils/api/delegate.js @@ -20,26 +20,26 @@ export const vote = (activePeer, secret, publicKey, voteList, unvoteList, second secondSecret, }); -export const voteAutocomplete = (activePeer, username, votedList) => { +export const voteAutocomplete = (activePeer, username, votedDict) => { const options = { q: username }; return new Promise((resolve, reject) => listDelegates(activePeer, options) .then((response) => { resolve(response.delegates.filter(delegate => - Object.keys(votedList).filter(item => item === delegate.username).length === 0, + Object.keys(votedDict).filter(item => item === delegate.username).length === 0, )); }) .catch(reject), ); }; -export const unvoteAutocomplete = (username, votedList) => +export const unvoteAutocomplete = (username, votedDict) => new Promise((resolve) => { resolve( - Object.keys(votedList) + Object.keys(votedDict) .filter(delegate => delegate.indexOf(username) !== -1) - .map(element => ({ username: element, publicKey: votedList[element].publicKey }))); + .map(element => ({ username: element, publicKey: votedDict[element].publicKey }))); }); export const registerDelegate = (activePeer, username, secret, secondSecret = null) => { From 17e302b52441ca01919d16f9d9eeb2d0161b0166 Mon Sep 17 00:00:00 2001 From: reyraa Date: Mon, 18 Sep 2017 18:16:37 +0200 Subject: [PATCH 16/18] replace clearedVoteLists with votesUpdates to update votes individually --- src/actions/voting.js | 13 +++++++++---- src/actions/voting.test.js | 32 +++++++++++++++----------------- src/constants/actions.js | 2 +- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/actions/voting.js b/src/actions/voting.js index 1449d8b0c..d6db1cbd7 100644 --- a/src/actions/voting.js +++ b/src/actions/voting.js @@ -14,8 +14,9 @@ export const pendingVotesAdded = () => ({ /** * Remove all data from the list of voted delegates and list of unvoted delegates */ -export const clearVoteLists = () => ({ - type: actionTypes.votesCleared, +export const votesUpdated = data => ({ + type: actionTypes.votesUpdated, + data, }); /** @@ -93,10 +94,14 @@ export const votePlaced = ({ activePeer, account, votes, secondSecret }) => * Gets the list of delegates current account has voted for * */ -export const votesFetched = ({ activePeer, address }) => +export const votesFetched = ({ activePeer, address, type }) => (dispatch) => { listAccountDelegates(activePeer, address).then(({ delegates }) => { - dispatch(votesAdded({ list: delegates })); + if (type === 'update') { + dispatch(votesUpdated({ list: delegates })); + } else { + dispatch(votesAdded({ list: delegates })); + } }); }; diff --git a/src/actions/voting.test.js b/src/actions/voting.test.js index 4a2637f91..9e6614b18 100644 --- a/src/actions/voting.test.js +++ b/src/actions/voting.test.js @@ -3,7 +3,7 @@ import sinon from 'sinon'; import actionTypes from '../constants/actions'; import { pendingVotesAdded, - clearVoteLists, + votesUpdated, votesAdded, voteToggled, votePlaced, @@ -16,6 +16,11 @@ import { transactionAdded } from './transactions'; import { errorAlertDialogDisplayed } from './dialog'; import * as delegateApi from '../utils/api/delegate'; +const delegateList = [ + { username: 'username1', publicKey: '123HG3452245L' }, + { username: 'username2', publicKey: '123HG3522345L' }, +]; + describe('actions: voting', () => { describe('voteToggled', () => { it('should create an action to add data to toggle the vote status for any given delegate', () => { @@ -33,10 +38,7 @@ describe('actions: voting', () => { describe('votesAdded', () => { it('should create an action to remove data from vote list', () => { - const data = [ - { username: 'username1', publicKey: '123HG3452245L' }, - { username: 'username2', publicKey: '123HG3522345L' }, - ]; + const data = delegateList; const expectedAction = { data, type: actionTypes.votesAdded, @@ -46,12 +48,14 @@ describe('actions: voting', () => { }); }); - describe('clearVoteLists', () => { - it('should create an action to remove all pending rows from vote list', () => { + describe('votesUpdated', () => { + it('should create an action to update the votes dictionary', () => { const expectedAction = { - type: actionTypes.votesCleared, + type: actionTypes.votesUpdated, + data: { list: delegateList }, }; - expect(clearVoteLists()).to.be.deep.equal(expectedAction); + const createdAction = votesUpdated({ list: delegateList }); + expect(createdAction).to.be.deep.equal(expectedAction); }); }); @@ -132,10 +136,7 @@ describe('actions: voting', () => { activePeer: {}, address: '8096217735672704724L', }; - const delegates = [ - { username: 'username1', publicKey: '80962134535672704724L' }, - { username: 'username2', publicKey: '80962134535672704725L' }, - ]; + const delegates = delegateList; const actionFunction = votesFetched(data); it('should create an action function', () => { @@ -162,10 +163,7 @@ describe('actions: voting', () => { offset: 0, refresh: true, }; - const delegates = [ - { username: 'username1', publicKey: '80962134535672704724L' }, - { username: 'username2', publicKey: '80962134535672704725L' }, - ]; + const delegates = delegateList; const actionFunction = delegatesFetched(data); it('should create an action function', () => { diff --git a/src/constants/actions.js b/src/constants/actions.js index b4681a093..3fe3994ff 100644 --- a/src/constants/actions.js +++ b/src/constants/actions.js @@ -14,7 +14,7 @@ const actionTypes = { VotePlaced: 'VOTE_PLACED', voteToggled: 'VOTE_TOGGLED', votesAdded: 'VOTES_ADDED', - votesCleared: 'VOTES_CLEARED', + votesUpdated: 'VOTES_UPDATED', delegatesAdded: 'DELEGATES_ADDED', pendingVotesAdded: 'PENDING_VOTES_ADDED', toastDisplayed: 'TOAST_DISPLAYED', From cffbe4fe18d0dfa7e7531bdc5b0340975fc943d6 Mon Sep 17 00:00:00 2001 From: reyraa Date: Mon, 18 Sep 2017 18:17:47 +0200 Subject: [PATCH 17/18] Compare new and old votes to keep the list update --- src/store/reducers/voting.js | 45 ++++++++++++++++++++++++++++--- src/store/reducers/voting.test.js | 4 +-- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/store/reducers/voting.js b/src/store/reducers/voting.js index 47c9eabf6..7578397c1 100644 --- a/src/store/reducers/voting.js +++ b/src/store/reducers/voting.js @@ -1,5 +1,36 @@ import actionTypes from '../../constants/actions'; +const mergeVotes = (newList, oldDict) => { + const newDict = newList.reduce((tempDict, delegate) => { + tempDict[delegate.username] = { + confirmed: true, + unconfirmed: true, + pending: false, + publicKey: delegate.publicKey, + }; + return tempDict; + }, {}); + + Object.keys(oldDict).forEach((username) => { + // By pendingVotesAdded, we set confirmed equal to unconfirmed, + // to recognize pending-not-voted items from pending-voted + // so here we just check unconfirmed flag. + const { confirmed, unconfirmed, pending } = oldDict[username]; + if (// we've voted but it's not in the new list + (pending && unconfirmed && newDict[username] === undefined) || + // we've un-voted but it still exists in the new list + (pending && !unconfirmed && newDict[username] !== undefined) || + // dirty, not voted for and not updated in other client + (!pending && unconfirmed !== confirmed && + (newDict[username] === undefined || confirmed === newDict[username].confirmed)) + ) { + newDict[username] = Object.assign({}, oldDict[username]); + } + }); + + return newDict; +}; + /** * voting reducer * @@ -66,17 +97,23 @@ const voting = (state = { votes: {}, delegates: [], totalDelegates: 0 }, action) refresh: true, }); + case actionTypes.votesUpdated: + return Object.assign({}, state, { + votes: mergeVotes(action.data.list, state.votes), + refresh: false, + }); + case actionTypes.pendingVotesAdded: return Object.assign({}, state, { refresh: false, votes: Object.keys(state.votes).reduce((votesDict, username) => { - const pending = state.votes[username].confirmed !== state.votes[username].unconfirmed; - const { confirmed, unconfirmed, publicKey } = state.votes[username]; + const { confirmed, unconfirmed, publicKey, pending } = state.votes[username]; + const nextPendingStatus = pending || (confirmed !== unconfirmed); votesDict[username] = { - confirmed: pending ? !confirmed : confirmed, + confirmed: nextPendingStatus ? !confirmed : confirmed, unconfirmed, - pending, + pending: nextPendingStatus, publicKey, }; return votesDict; diff --git a/src/store/reducers/voting.test.js b/src/store/reducers/voting.test.js index e351d06a4..129f31c24 100644 --- a/src/store/reducers/voting.test.js +++ b/src/store/reducers/voting.test.js @@ -96,12 +96,12 @@ describe('Reducer: voting(state, action)', () => { expect(changedState.delegates).to.be.deep.equal(expectedState.delegates); }); - it('should replace to delegates list with action: delegatesAdded, refresh: true', () => { + it('should replace delegates with the new delegates list with action: delegatesAdded, refresh: true', () => { const action = { type: actionTypes.delegatesAdded, data: { list: delegates1, - totalCount: 100, + totalDelegates: 100, refresh: true, }, }; From 5971aec1baf1f790e3349e1b495e1cb42f0a1607 Mon Sep 17 00:00:00 2001 From: reyraa Date: Mon, 18 Sep 2017 18:18:24 +0200 Subject: [PATCH 18/18] Use votesUpdated after new vote transaction confirmed --- src/store/middlewares/account.js | 11 +++++++++-- src/store/middlewares/account.test.js | 12 +++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/store/middlewares/account.js b/src/store/middlewares/account.js index 21f480a45..3bbdf0bd4 100644 --- a/src/store/middlewares/account.js +++ b/src/store/middlewares/account.js @@ -2,7 +2,7 @@ import { getAccountStatus, getAccount, transactions } from '../../utils/api/acco import { accountUpdated, accountLoggedIn } from '../../actions/account'; import { transactionsUpdated } from '../../actions/transactions'; import { activePeerUpdate } from '../../actions/peers'; -import { clearVoteLists } from '../../actions/voting'; +import { votesFetched } from '../../actions/voting'; import actionTypes from '../../constants/actions'; import { fetchAndUpdateForgedBlocks } from '../../actions/forging'; import { getDelegate } from '../../utils/api/delegate'; @@ -81,7 +81,14 @@ const votePlaced = (store, action) => { action.data.confirmed, transactionTypes.vote); if (voteTransaction) { - store.dispatch(clearVoteLists()); + const state = store.getState(); + const { peers, account } = state; + + store.dispatch(votesFetched({ + activePeer: peers.data, + address: account.address, + type: 'update', + })); } }; diff --git a/src/store/middlewares/account.test.js b/src/store/middlewares/account.test.js index 99676625f..875c82191 100644 --- a/src/store/middlewares/account.test.js +++ b/src/store/middlewares/account.test.js @@ -6,7 +6,7 @@ import * as delegateApi from '../../utils/api/delegate'; import actionTypes from '../../constants/actions'; import transactionTypes from '../../constants/transactionTypes'; import { SYNC_ACTIVE_INTERVAL, SYNC_INACTIVE_INTERVAL } from '../../constants/api'; -import { clearVoteLists } from '../../actions/voting'; +import * as votingActions from '../../actions/voting'; describe('Account middleware', () => { let store; @@ -49,6 +49,7 @@ describe('Account middleware', () => { }, account: { balance: 0, + address: 'sample_address', }, transactions: { pending: [{ @@ -164,10 +165,15 @@ describe('Account middleware', () => { delegateApiMock.restore(); }); - it(`should dispatch clearVoteLists action on ${actionTypes.transactionsUpdated} action if action.data.confirmed contains delegateRegistration transactions`, () => { + it(`should dispatch ${actionTypes.votesFetched} action on ${actionTypes.transactionsUpdated} action if action.data.confirmed contains delegateRegistration transactions`, () => { + const actionSpy = spy(votingActions, 'votesFetched'); transactionsUpdatedAction.data.confirmed[0].type = transactionTypes.vote; middleware(store)(next)(transactionsUpdatedAction); - expect(store.dispatch).to.have.been.calledWith(clearVoteLists()); + expect(actionSpy).to.have.been.calledWith({ + activePeer: state.peers.data, + address: state.account.address, + type: 'update', + }); }); });