Show unconfirmed balances in toplist #112
@@ -9,10 +9,10 @@ export default Component.extend({
|
||||
actions: {
|
||||
|
||||
toggleContributorInfo(contributor) {
|
||||
if (contributor.get('showMetadata')) {
|
||||
if (contributor.showMetadata) {
|
||||
contributor.set('showMetadata', false);
|
||||
} else {
|
||||
this.contributors.setEach('showMetadata', false);
|
||||
this.contributorList.setEach('showMetadata', false);
|
||||
contributor.set('showMetadata', true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,34 @@
|
||||
<tbody>
|
||||
{{#each contributors as |contributor|}}
|
||||
<tr role="button" class={{if (is-current-user contributor) "current-user"}} {{action "toggleContributorInfo" contributor}}>
|
||||
{{#each contributorList as |c|}}
|
||||
<tr role="button" class={{if (is-current-user c.contributor) "current-user"}} {{action "toggleContributorInfo" c}}>
|
||||
<td class="person">
|
||||
{{user-avatar contributor=contributor}} {{contributor.name}}
|
||||
{{user-avatar contributor=c.contributor}} {{c.contributor.name}}
|
||||
</td>
|
||||
<td class="kredits">
|
||||
<span class="amount">{{contributor.totalKreditsEarned}}</span>
|
||||
<span class="amount">
|
||||
{{#if showUnconfirmedKredits}}
|
||||
{{c.amountTotal}}
|
||||
{{else}}
|
||||
{{c.amountConfirmed}}
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class="symbol">₭S</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="metadata {{if (is-current-user contributor) "current-user"}} {{if contributor.showMetadata "visible"}}">
|
||||
<tr class="metadata {{if (is-current-user c.contributor) "current-user"}} {{if c.showMetadata "visible"}}">
|
||||
<td colspan="2">
|
||||
<ul>
|
||||
<li><a href="https://rinkeby.etherscan.io/address/{{contributor.account}}">Inspect Ethereum transactions</a></li>
|
||||
{{#if contributor.ipfsHash}}
|
||||
<li><a href="https://ipfs.io/ipfs/{{contributor.ipfsHash}}">Inspect IPFS profile</a></li>
|
||||
<li>
|
||||
<a href="https://rinkeby.etherscan.io/address/{{c.contributor.account}}" target="_blank" rel="noopener">Inspect Ethereum transactions</a>
|
||||
</li>
|
||||
{{#if c.contributor.ipfsHash}}
|
||||
<li>
|
||||
<a href="https://ipfs.io/ipfs/{{c.contributor.ipfsHash}}" target="_blank" rel="noopener">Inspect IPFS profile</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
{{#if contributor.showMetadata}}
|
||||
<pre>{{contributor.ipfsData}}</pre>
|
||||
{{#if c.showMetadata}}
|
||||
<pre>{{c.contributor.ipfsData}}</pre>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
+15
-23
@@ -1,38 +1,30 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { alias, filter, sort } from '@ember/object/computed';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { computed } from '@ember/object';
|
||||
import { isPresent } from '@ember/utils';
|
||||
import { alias, not, sort } from '@ember/object/computed';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default Controller.extend({
|
||||
kredits: service(),
|
||||
currentBlock: alias('kredits.currentBlock'),
|
||||
|
||||
contributors: alias('kredits.contributors'),
|
||||
contributorsWithKredits: filter('contributors', function(contributor) {
|
||||
return isPresent(contributor.totalKreditsEarnedRaw) &&
|
||||
contributor.totalKreditsEarnedRaw.gt(0);
|
||||
}),
|
||||
contributorsSorting: Object.freeze(['totalKreditsEarned:desc']),
|
||||
contributorsSorted: sort('contributorsWithKredits', 'contributorsSorting'),
|
||||
|
||||
contributions: alias('kredits.contributions'),
|
||||
contributionsConfirmed: alias('kredits.contributionsConfirmed'),
|
||||
contributionsUnconfirmed: alias('kredits.contributionsUnconfirmed'),
|
||||
|
||||
contributionsSorting: Object.freeze(['id:desc']),
|
||||
|
||||
contributionsUnconfirmed: computed('contributions.[]', 'currentBlock', function() {
|
||||
return this.contributions.filter(contribution => {
|
||||
return contribution.confirmedAt > this.currentBlock;
|
||||
});
|
||||
}),
|
||||
contributionsConfirmed: computed('contributions.[]', 'currentBlock', function() {
|
||||
return this.contributions.filter(contribution => {
|
||||
return contribution.confirmedAt <= this.currentBlock;
|
||||
});
|
||||
}),
|
||||
|
||||
contributionsUnconfirmedSorted: sort('contributionsUnconfirmed', 'contributionsSorting'),
|
||||
contributionsConfirmedSorted: sort('contributionsConfirmed', 'contributionsSorting'),
|
||||
|
||||
kreditsByContributor: alias('kredits.kreditsByContributor'),
|
||||
|
||||
kreditsToplistSorting: computed('showUnconfirmedKredits', function(){
|
||||
return this.showUnconfirmedKredits ? ['amountTotal:desc'] : ['amountConfirmed:desc'];
|
||||
}),
|
||||
kreditsToplist: sort('kreditsByContributor', 'kreditsToplistSorting'),
|
||||
|
||||
showUnconfirmedKredits: true,
|
||||
hideUnconfirmedKredits: not('showUnconfirmedKredits'),
|
||||
|
||||
actions: {
|
||||
|
||||
vetoContribution (contributionId) {
|
||||
|
||||
+35
-3
@@ -3,10 +3,12 @@ import Kredits from 'npm:kredits-contracts';
|
||||
import RSVP from 'rsvp';
|
||||
|
||||
import Service from '@ember/service';
|
||||
import EmberObject from '@ember/object';
|
||||
import { computed } from '@ember/object';
|
||||
import { alias, notEmpty } from '@ember/object/computed';
|
||||
import { isEmpty } from '@ember/utils';
|
||||
|
||||
import groupBy from 'kredits-web/utils/group-by';
|
||||
import formatKredits from 'kredits-web/utils/format-kredits';
|
||||
|
||||
import config from 'kredits-web/config/environment';
|
||||
@@ -20,15 +22,44 @@ export default Service.extend({
|
||||
currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded.
|
||||
currentUser: null,
|
||||
contributors: null,
|
||||
proposals: null,
|
||||
contributions: null,
|
||||
proposals: null,
|
||||
|
||||
currentUserIsContributor: notEmpty('currentUser'),
|
||||
currentUserIsCore: alias('currentUser.isCore'),
|
||||
hasAccounts: notEmpty('currentUserAccounts'),
|
||||
|
||||
accountNeedsUnlock: computed('currentUserAccounts', function() {
|
||||
return this.currentUserAccounts && isEmpty(this.currentUserAccounts);
|
||||
}),
|
||||
|
||||
contributionsUnconfirmed: computed('contributions.[]', 'currentBlock', function() {
|
||||
return this.contributions.filter(contribution => {
|
||||
return contribution.confirmedAt > this.currentBlock;
|
||||
});
|
||||
}),
|
||||
|
||||
contributionsConfirmed: computed('contributions.[]', 'currentBlock', function() {
|
||||
return this.contributions.filter(contribution => {
|
||||
return contribution.confirmedAt <= this.currentBlock;
|
||||
});
|
||||
}),
|
||||
|
||||
kreditsByContributor: computed('contributionsUnconfirmed.[]', 'contributors', function() {
|
||||
const contributionsGrouped = groupBy(this.contributionsUnconfirmed, 'contributorId');
|
||||
|
||||
return contributionsGrouped.map(c => {
|
||||
const amountUnconfirmed = c.items.mapBy('amount').reduce((a, b) => a + b);
|
||||
const contributor = this.contributors.findBy('id', c.value.toString());
|
||||
return EmberObject.create({
|
||||
contributor: contributor,
|
||||
amountUnconfirmed: amountUnconfirmed,
|
||||
amountConfirmed: contributor.totalKreditsEarned,
|
||||
amountTotal: contributor.totalKreditsEarned + amountUnconfirmed
|
||||
})
|
||||
})
|
||||
}),
|
||||
|
||||
init () {
|
||||
this._super(...arguments);
|
||||
this.set('contributors', []);
|
||||
@@ -36,8 +67,9 @@ export default Service.extend({
|
||||
this.set('contributions', []);
|
||||
},
|
||||
|
||||
// this is called in the routes beforeModel(). So it is initialized before everything else
|
||||
// and we can rely on the ethProvider and the potential currentUserAccounts to be available
|
||||
// This is called in the application route's beforeModel(). So it is
|
||||
// initialized before everything else, and we can rely on the ethProvider and
|
||||
// the potential currentUserAccounts to be available
|
||||
getEthProvider: function() {
|
||||
let ethProvider;
|
||||
|
||||
|
||||
+1
-4
@@ -47,16 +47,13 @@ section {
|
||||
&#people {
|
||||
.content {
|
||||
p.stats {
|
||||
padding-top: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1rem;
|
||||
color: white;
|
||||
text-align: center;
|
||||
span.number {
|
||||
font-weight: 600;
|
||||
}
|
||||
@include media-max(small) {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
table.contributor-list {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
|
||||
@@ -6,11 +6,16 @@
|
||||
<h2>Contributors</h2>
|
||||
</header>
|
||||
<div class="content">
|
||||
{{contributor-list contributors=contributorsSorted}}
|
||||
{{contributor-list contributorList=kreditsToplist
|
||||
showUnconfirmedKredits=showUnconfirmedKredits}}
|
||||
|
||||
<p class="stats">
|
||||
<span class="number">{{await kredits.totalKreditsEarned}}</span> kredits issued and distributed among
|
||||
<span class="number">{{contributorsWithKredits.length}}</span> contributors.
|
||||
<span class="number">{{await kredits.totalKreditsEarned}}</span> kredits confirmed and issued to
|
||||
<span class="number">{{contributorsWithKredits.length}}</span> contributors
|
||||
</p>
|
||||
<p class="stats">
|
||||
{{input type="checkbox" id="hide-unnconfirmed-kredits" checked=showUnconfirmedKredits}}
|
||||
<label for="hide-unnconfirmed-kredits">Show unconfirmed kredits in toplist</label>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Code from https://github.com/HeroicEric/ember-group-by (MIT licensed)
|
||||
//
|
||||
import { A } from '@ember/array';
|
||||
import { get } from '@ember/object';
|
||||
import { isPresent } from '@ember/utils';
|
||||
|
||||
export default function groupBy (collection, property) {
|
||||
let groups = A();
|
||||
let items = collection;
|
||||
|
||||
if (items) {
|
||||
|
|
||||
items.forEach(function(item) {
|
||||
let value = get(item, property);
|
||||
let group = groups.findBy('value', value);
|
||||
|
||||
if (isPresent(group)) {
|
||||
get(group, 'items').push(item);
|
||||
} else {
|
||||
group = { property: property, value: value, items: [item] };
|
||||
groups.push(group);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
import Contributor from 'kredits-web/models/contributor';
|
||||
|
||||
const contributors = [];
|
||||
|
||||
const data = [
|
||||
{ id: 1, name: 'Bumi', totalKreditsEarned: 5500 },
|
||||
{ id: 2, name: 'Râu Cao', totalKreditsEarned: 1500 },
|
||||
{ id: 3, name: 'Manuel', totalKreditsEarned: 3000 }
|
||||
];
|
||||
|
||||
data.forEach(attrs => contributors.push(Contributor.create(attrs)));
|
||||
|
||||
export default contributors;
|
||||
@@ -2,17 +2,43 @@ import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import Contributor from 'kredits-web/models/contributor';
|
||||
|
||||
module('Integration | Component | contributor list', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function(assert) {
|
||||
const toplist = [
|
||||
{
|
||||
contributor: Contributor.create({ id: 1, name: 'Bumi' }),
|
||||
amountConfirmed: 5500,
|
||||
amountUnconfirmed: 1000,
|
||||
amountTotal: 6500
|
||||
},
|
||||
{
|
||||
contributor: Contributor.create({ id: 2, name: 'Râu Cao' }),
|
||||
amountConfirmed: 1500,
|
||||
amountUnconfirmed: 2000,
|
||||
amountTotal: 3500
|
||||
}
|
||||
];
|
||||
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||
test('it renders unconfirmed kredits earned', async function(assert) {
|
||||
this.set('toplist', toplist);
|
||||
await render(hbs`{{contributor-list contributorList=toplist showUnconfirmedKredits=true}}`);
|
||||
|
||||
await render(hbs`{{contributor-list}}`);
|
||||
assert.dom('tr:nth-child(1) td.person').hasText('Bumi');
|
||||
assert.dom('tr:nth-child(1) span.amount').hasText('6500');
|
||||
assert.dom('tr:nth-child(3) td.person').hasText('Râu Cao');
|
||||
assert.dom('tr:nth-child(3) span.amount').hasText('3500');
|
||||
});
|
||||
|
||||
assert.dom('*').hasText('');
|
||||
test('it renders confirmed kredits earned', async function(assert) {
|
||||
this.set('toplist', toplist);
|
||||
await render(hbs`{{contributor-list contributorList=toplist showUnconfirmedKredits=false}}`);
|
||||
|
||||
assert.dom('tr:nth-child(1) td.person').hasText('Bumi');
|
||||
assert.dom('tr:nth-child(1) span.amount').hasText('5500');
|
||||
assert.dom('tr:nth-child(3) td.person').hasText('Râu Cao');
|
||||
assert.dom('tr:nth-child(3) span.amount').hasText('1500');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import { isEmpty, isPresent } from '@ember/utils';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import Contributor from 'kredits-web/models/contributor';
|
||||
|
||||
module('Unit | Controller | index', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
let addFixtures = function(controller) {
|
||||
[
|
||||
{ github_username: "neo", github_uid: "318", totalKreditsEarned: 10000 },
|
||||
{ github_username: "morpheus", github_uid: "843", totalKreditsEarned: 15000 },
|
||||
{ github_username: "trinity", github_uid: "123", totalKreditsEarned: 5000 },
|
||||
{ github_username: "mouse", github_uid: "696", totalKreditsEarned: 0 }
|
||||
].forEach(fixture => {
|
||||
controller.get('kredits.contributors').push(Contributor.create(fixture));
|
||||
});
|
||||
};
|
||||
|
||||
test('doesn\'t contain people with 0 balance', function(assert) {
|
||||
let controller = this.owner.lookup('controller:index');
|
||||
addFixtures(controller);
|
||||
|
||||
let contributorsSorted = controller.get('contributorsSorted');
|
||||
|
||||
assert.ok(isPresent(contributorsSorted.findBy('github_username', 'neo')));
|
||||
assert.ok(isEmpty(contributorsSorted.findBy('github_username', 'mouse')));
|
||||
});
|
||||
});
|
||||
// import { isEmpty, isPresent } from '@ember/utils';
|
||||
// import { module, test } from 'qunit';
|
||||
// import { setupTest } from 'ember-qunit';
|
||||
// import Contributor from 'kredits-web/models/contributor';
|
||||
//
|
||||
// module('Unit | Controller | index', function(hooks) {
|
||||
// setupTest(hooks);
|
||||
//
|
||||
// let addFixtures = function(controller) {
|
||||
// [
|
||||
// { github_username: "neo", github_uid: "318", totalKreditsEarned: 10000 },
|
||||
// { github_username: "morpheus", github_uid: "843", totalKreditsEarned: 15000 },
|
||||
// { github_username: "trinity", github_uid: "123", totalKreditsEarned: 5000 },
|
||||
// { github_username: "mouse", github_uid: "696", totalKreditsEarned: 0 }
|
||||
// ].forEach(fixture => {
|
||||
// controller.get('kredits.contributors').push(Contributor.create(fixture));
|
||||
// });
|
||||
// };
|
||||
//
|
||||
// test('doesn\'t contain people with 0 balance', function(assert) {
|
||||
// let controller = this.owner.lookup('controller:index');
|
||||
// addFixtures(controller);
|
||||
//
|
||||
// let contributorsSorted = controller.get('contributorsSorted');
|
||||
//
|
||||
// assert.ok(isPresent(contributorsSorted.findBy('github_username', 'neo')));
|
||||
// assert.ok(isEmpty(contributorsSorted.findBy('github_username', 'mouse')));
|
||||
// });
|
||||
// });
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// Code adapted from https://github.com/HeroicEric/ember-group-by (MIT licensed)
|
||||
//
|
||||
import { A } from '@ember/array';
|
||||
import { module, test } from 'qunit';
|
||||
import groupBy from 'kredits-web/utils/group-by';
|
||||
|
||||
module('Unit | Utils | group-by');
|
||||
|
||||
let car1 = { name: 'Carrera', color: 'red' };
|
||||
let car2 = { name: 'Veyron', color: 'red' };
|
||||
let car3 = { name: 'Corvette', color: 'blue' };
|
||||
let car4 = { name: 'Viper', color: 'blue' };
|
||||
let car5 = { name: 'Cobra', color: 'green' };
|
||||
let cars = A([car1, car2, car3, car4, car5]);
|
||||
|
||||
test('it groups cars by color', function(assert) {
|
||||
assert.expect(1);
|
||||
let redGroup = { property: 'color', value: 'red', items: [car1, car2] };
|
||||
let blueGroup = { property: 'color', value: 'blue', items: [car3, car4] };
|
||||
let greenGroup = { property: 'color', value: 'green', items: [car5] };
|
||||
|
||||
let result = groupBy(cars, 'color');
|
||||
let expected = [redGroup, blueGroup, greenGroup];
|
||||
|
||||
assert.deepEqual(result, expected);
|
||||
});
|
||||
|
||||
test('it does not fail with empty array', function(assert) {
|
||||
let cars = [];
|
||||
|
||||
let result = groupBy(cars, 'color');
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
test('it does not failkwith null', function(assert) {
|
||||
let cars = null;
|
||||
|
||||
let result = groupBy(cars, 'color');
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
Reference in New Issue
Block a user
maybe
if(!items) {
return groups;
}
Did you see that you can now suggest changes to commit in code comments? It's the first icon button in the comment topbar. Makes it much easier to see what the suggested change is as well, because the code behind the comment isn't cut off then...Never mind, it's a bigger change.
I think a single return is a bit cleaner here.