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

Add delegate votes management - Closes #24 #115

Merged
merged 37 commits into from
Apr 20, 2017
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c51b652
An initial mockup for Delegate vote management
slaweet Apr 3, 2017
2b58196
Make delegates mockup more condensed
slaweet Apr 4, 2017
fdab0cb
Fix "no delegates found" state
slaweet Apr 10, 2017
582d972
Merge vote and unvote buttons
slaweet Apr 10, 2017
ab74136
Finish vote for delegates dialog
slaweet Apr 10, 2017
7d7ac3d
Add delegates infinite scroll
slaweet Apr 10, 2017
c792bd3
Label delegates checkbox column
slaweet Apr 10, 2017
801f296
Fix vote confirm button disable state
slaweet Apr 10, 2017
4776049
Make delegates search more visible and better
slaweet Apr 11, 2017
4c85954
Polish the voting UI
slaweet Apr 11, 2017
169f327
Remove delegates filter select
slaweet Apr 11, 2017
4f8b055
Delegates refactoring
slaweet Apr 12, 2017
72fbf98
Create the first unit test for delegates component
slaweet Apr 12, 2017
d63271c
Add support for voting with second passphrase
slaweet Apr 12, 2017
b459695
Fix delegates search clear icon action
slaweet Apr 12, 2017
eebd428
Delegates refactoring
slaweet Apr 12, 2017
22a78a8
Fix vote dialog remove icon action
slaweet Apr 12, 2017
32c5337
Remove no longer used boolFilter
slaweet Apr 12, 2017
d547236
Update delegates list when a peer is changed
slaweet Apr 12, 2017
251f548
Preserve changed delegate status
slaweet Apr 12, 2017
ffd4b58
Move voting dialog to a separate component
slaweet Apr 12, 2017
7e8e4cf
Create unit tests for delegates controller
slaweet Apr 12, 2017
0889a3e
Vote component refactoring
slaweet Apr 12, 2017
1854cbe
Create unit tests for vote component
slaweet Apr 12, 2017
65845d0
Wrap lisk-js sendRequest with our own function
slaweet Apr 13, 2017
339682c
Move clear votes from voting component to delegates
slaweet Apr 13, 2017
be7e13e
Refactor voting css
slaweet Apr 13, 2017
7cecbcc
Add "My votes" button
slaweet Apr 13, 2017
b804ad1
Create 'select list of delegates' feature
slaweet Apr 12, 2017
65cbdd1
Refactor delegates controller function names
slaweet Apr 13, 2017
ccf4612
Refactor delegates controller
slaweet Apr 13, 2017
25f6058
Create tests for the rest of delegates controller
slaweet Apr 13, 2017
85afa9d
Polish edge cases of delegates multi select feature
slaweet Apr 13, 2017
a89f7a6
Add pending state of delegates after voting
slaweet Apr 18, 2017
b177e16
Fix changed method name in tests
slaweet Apr 18, 2017
36679a2
Start refactoring sendRequest calls
slaweet Apr 18, 2017
fa3acf4
Merge branch 'development' into 24_delegate-vote-management
karmacoma Apr 20, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export default angular.module('app', [
'ngMaterial',
'ngAnimate',
'ngCookies',
'infinite-scroll',
'md.data.table',
]);
276 changes: 276 additions & 0 deletions src/app/components/delegates/delegates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
import './delegates.less';

const UPDATE_INTERVAL = 10000;

app.component('delegates', {
template: require('./delegates.pug')(),
bindings: {
account: '=',
passphrase: '<',
},
controller: class delegates {
constructor($scope, $peers, $mdDialog, $mdMedia, $mdToast, $timeout) {
this.$scope = $scope;
this.$peers = $peers;
this.$mdDialog = $mdDialog;
this.$mdMedia = $mdMedia;
this.$mdToast = $mdToast;
this.$timeout = $timeout;

this.$scope.search = '';
this.voteList = [];
this.votedDict = {};
this.votedList = [];
this.unvoteList = [];
this.loading = true;
this.usernameInput = '';
this.usernameSeparator = '\n';

this.updateAll();

this.$scope.$watch('search', (search, oldValue) => {
this.delegatesDisplayedCount = 20;
if (search || oldValue) {
this.loadDelegates(0, search, true);
}
});

this.$scope.$on('peerUpdate', () => {
this.updateAll();
});
}

updateAll() {
this.delegates = [];
this.delegatesDisplayedCount = 20;
this.$peers.listAccountDelegates({
address: this.account.address,
}).then((data) => {
this.votedList = data.delegates || [];
(this.votedList).forEach((delegate) => {
this.votedDict[delegate.username] = delegate;
});
this.loadDelegates(0, this.$scope.search);
});
}

loadDelegates(offset, search, replace) {
this.loading = true;
this.$peers.listDelegates({
offset,
limit: '100',
q: search,
}).then((data) => {
this.addDelegates(data, replace);
});
this.lastSearch = search;
}

addDelegates(data, replace) {
if (data.success) {
if (replace) {
this.delegates = [];
}
this.delegates = this.delegates.concat(data.delegates.map((delegate) => {
const voted = this.votedDict[delegate.username] !== undefined;
const changed = this.voteList.concat(this.unvoteList)
.map(d => d.username).indexOf(delegate.username) !== -1;
delegate.status = { // eslint-disable-line no-param-reassign
selected: (voted && !changed) || (!voted && changed),
voted,
changed,
};
return delegate;
}));
this.delegatesTotalCount = data.totalCount;
this.loading = false;
}
}

showMore() {
if (this.delegatesDisplayedCount < this.delegates.length) {
this.delegatesDisplayedCount += 20;
}
if (this.delegates.length - this.delegatesDisplayedCount <= 20 &&
this.delegates.length < this.delegatesTotalCount &&
!this.loading) {
this.loadDelegates(this.delegates.length, this.$scope.search);
}
}

selectionChange(delegate) {
// eslint-disable-next-line no-param-reassign
delegate.status.changed = delegate.status.voted !== delegate.status.selected;
const list = delegate.status.voted ? this.unvoteList : this.voteList;
if (delegate.status.changed) {
list.push(delegate);
} else {
list.splice(list.indexOf(delegate), 1);
}
}

clearSearch() {
this.$scope.search = '';
}

addToUnvoteList(vote) {
const delegate = this.delegates.filter(d => d.username === vote.username)[0] || vote;
if (delegate.status.selected) {
this.unvoteList.push(delegate);
}
delegate.status.selected = false;
}

setPendingVotes() {
this.voteList.forEach((delegate) => {
/* eslint-disable no-param-reassign */
delegate.status.changed = false;
delegate.status.voted = true;
delegate.status.pending = true;
});
this.votePendingList = this.voteList.splice(0, this.voteList.length);

this.unvoteList.forEach((delegate) => {
delegate.status.changed = false;
delegate.status.voted = false;
delegate.status.pending = true;
/* eslint-enable no-param-reassign */
});
this.unvotePendingList = this.unvoteList.splice(0, this.unvoteList.length);
this.checkPendingVotes();
}

checkPendingVotes() {
this.$timeout(() => {
this.$peers.listAccountDelegates({
address: this.account.address,
}).then((data) => {
this.votedList = data.delegates || [];
this.votedDict = {};
(this.votedList).forEach((delegate) => {
this.votedDict[delegate.username] = delegate;
});
this.votePendingList = this.votePendingList.filter((vote) => {
if (this.votedDict[vote.username]) {
// eslint-disable-next-line no-param-reassign
vote.status.pending = false;
return false;
}
return true;
});
this.unvotePendingList = this.unvotePendingList.filter((vote) => {
if (!this.votedDict[vote.username]) {
// eslint-disable-next-line no-param-reassign
vote.status.pending = false;
return false;
}
return true;
});
if (this.votePendingList.length + this.unvotePendingList.length > 0) {
this.checkPendingVotes();
}
});
}, UPDATE_INTERVAL);
}

parseVoteListFromInput() {
this._parseListFromInput('voteList');
}

parseUnvoteListFromInput() {
this._parseListFromInput('unvoteList');
}

_parseListFromInput(listName) {
const list = this[listName];
this.invalidUsernames = [];
this.pendingRequests = 0;
this.usernameList = this.usernameInput.trim().split(this.usernameSeparator);
this.usernameList.forEach((username) => {
if ((listName === 'voteList' && !this.votedDict[username.trim()]) ||
(listName === 'unvoteList' && this.votedDict[username.trim()])) {
this._setSelected(username.trim(), list);
}
});

if (this.pendingRequests === 0) {
this._selectFinish(true, list);
}
}

_selectFinish(success, list) {
if (list.length !== 0) {
this.usernameListActive = false;
this.usernameInput = '';
this.openVoteDialog();
} else {
const toast = this.$mdToast.simple();
toast.toastClass('lsk-toast-error');
toast.textContent('No delegate usernames could be parsed from the input');
this.$mdToast.show(toast);
}
}

_setSelected(username, list) {
const delegate = this.delegates.filter(d => d.username === username)[0];
if (delegate) {
this._selectDelegate(delegate, list);
} else {
this.pendingRequests++;
this.$peers.getDelegate({ username,
}).then((data) => {
this._selectDelegate(data.delegate, list);
}).catch(() => {
this.invalidUsernames.push(username);
}).finally(() => {
this.pendingRequests--;
if (this.pendingRequests === 0) {
this._selectFinish(this.invalidUsernames.length === 0, list);
}
});
}
}

// eslint-disable-next-line class-methods-use-this
_selectDelegate(delegate, list) {
// eslint-disable-next-line no-param-reassign
delegate.status = delegate.status || {};
// eslint-disable-next-line no-param-reassign
delegate.status.selected = true;
if (list.indexOf(delegate) === -1) {
list.push(delegate);
}
}

openVoteDialog() {
this.$mdDialog.show({
controllerAs: '$ctrl',
controller: class voteDialog {
constructor($scope, account, passphrase, voteList, unvoteList) {
this.$scope = $scope;
this.$scope.account = account;
this.$scope.passphrase = passphrase;
this.$scope.voteList = voteList;
this.$scope.unvoteList = unvoteList;
}
},
template:
'<md-dialog>' +
'<vote account="account" passphrase="passphrase" ' +
'vote-list="voteList" unvote-list="unvoteList">' +
'</vote>' +
'</md-dialog>',
fullscreen: (this.$mdMedia('sm') || this.$mdMedia('xs')) && this.$scope.customFullscreen,
locals: {
account: this.account,
passphrase: this.passphrase,
voteList: this.voteList,
unvoteList: this.unvoteList,
},
}).then((() => {
this.setPendingVotes();
}));
}
},
});

86 changes: 86 additions & 0 deletions src/app/components/delegates/delegates.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
delegates {
.pull-right {
float: right;
}

.right-action-buttons {
margin: -8px 0;
}

button {
margin: -10px;
}

i {
vertical-align: inherit;
margin-left: 8px;
margin-right: 4px;
}

.green-link {
color: #7cb342;
}
.red-link {
color: #c62828;
}
.remove-votes-link {
margin-right: -2px;
}

.upvote {
background-color: rgb(226, 238, 213);
}
.downvote {
background-color: rgb(255, 228, 220);
}
.pending {
background-color: #eaeae9;
}

md-card-title {
md-input-container {
margin: 0;
padding: 0;
}
.md-errors-spacer {
min-height: 0;
}
}

.search-append {
color: #aaa;
cursor: pointer;
margin-left: -30px;
z-index: 10;
}

.label {
font-weight: bold;
background-color: #5f696e;
color: #fff;
border-radius: 3px;
padding: 4px 9px;
white-space: nowrap;
}

.status {
line-height: 2em;
}

.filter-select md-select{
display: inline-block;
margin: 0 10px
}
}

.lsk-vote-remove-button {
float: right;
position: absolute;
right: 4px;
top: 4px;

.material-icons {
vertical-align: inherit;
}
}

Loading