Merge pull request #209 from 67P/feature/veto_reimbursements
Finish Reimbursement UI (MVP)
This commit was merged in pull request #209.
This commit is contained in:
@@ -81,7 +81,9 @@ export default class AddExpenseItemComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isPresent(this.tags)) {
|
if (isPresent(this.tags)) {
|
||||||
expense.tags = this.tags.split(',').map(t => t.trim());
|
expense.tags = this.tags.split(',')
|
||||||
|
.map(t => t.trim())
|
||||||
|
.filter(t => t.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.args.addExpenseItem(expense);
|
this.args.addExpenseItem(expense);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { action } from '@ember/object';
|
|||||||
import { A } from '@ember/array';
|
import { A } from '@ember/array';
|
||||||
import { scheduleOnce } from '@ember/runloop';
|
import { scheduleOnce } from '@ember/runloop';
|
||||||
import isValidAmount from 'kredits-web/utils/is-valid-amount';
|
import isValidAmount from 'kredits-web/utils/is-valid-amount';
|
||||||
|
import readFileContent from 'kredits-web/utils/read-file-content';
|
||||||
import config from 'kredits-web/config/environment';
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
export default class AddReimbursementComponent extends Component {
|
export default class AddReimbursementComponent extends Component {
|
||||||
@@ -84,6 +85,23 @@ export default class AddReimbursementComponent extends Component {
|
|||||||
this.total = btcAmount.toFixed(8);
|
this.total = btcAmount.toFixed(8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO use ember-concurrency here
|
||||||
|
// https://github.com/67P/kredits-web/pull/209#discussion_r1064234421
|
||||||
|
@action
|
||||||
|
async addExpensesFromFile (evt) {
|
||||||
|
const content = await readFileContent(evt.target.files[0]);
|
||||||
|
const expenses = JSON.parse(content);
|
||||||
|
|
||||||
|
if (expenses instanceof Array) {
|
||||||
|
for (const item of expenses) {
|
||||||
|
this.addExpenseItem(item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Expenses in file must be a list of items:");
|
||||||
|
console.debug(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateContributor (event) {
|
updateContributor (event) {
|
||||||
this.recipientId = event.target.value;
|
this.recipientId = event.target.value;
|
||||||
|
|||||||
@@ -87,3 +87,9 @@
|
|||||||
<AddExpenseItem @addExpenseItem={{fn this.addExpenseItem}} />
|
<AddExpenseItem @addExpenseItem={{fn this.addExpenseItem}} />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</form>
|
</form>
|
||||||
|
<form id="add-expenses-from-file">
|
||||||
|
<h3>Add expense items from file</h3>
|
||||||
|
<input type="file" multiple="false"
|
||||||
|
onchange={{fn this.addExpensesFromFile}}
|
||||||
|
accept="application/json" />
|
||||||
|
</form>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { inject as service } from '@ember/service';
|
|||||||
import { alias } from '@ember/object/computed';
|
import { alias } from '@ember/object/computed';
|
||||||
|
|
||||||
export default class BudgetBalancesComponent extends Component {
|
export default class BudgetBalancesComponent extends Component {
|
||||||
@service communityFunds
|
@service communityFunds;
|
||||||
@alias('communityFunds.balances') balances;
|
@alias('communityFunds.balances') balances;
|
||||||
|
|
||||||
get loading () {
|
get loading () {
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import { sort } from '@ember/object/computed';
|
import { sort } from '@ember/object/computed';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
export default class ReimbursementListComponent extends Component {
|
export default class ReimbursementListComponent extends Component {
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
itemSorting = Object.freeze(['pendingStatus:asc', 'id:desc']);
|
itemSorting = Object.freeze(['pendingStatus:asc', 'id:desc']);
|
||||||
@sort('args.items', 'itemSorting') itemsSorted;
|
@sort('args.items', 'itemSorting') itemsSorted;
|
||||||
|
|
||||||
|
get ipfsGatewayUrl () {
|
||||||
|
return config.ipfs.gatewayUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
veto (id) {
|
||||||
|
this.kredits.vetoReimbursement(id).then(transaction => {
|
||||||
|
console.debug('[controllers:budget] Veto submitted to chain: '+transaction.hash);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,12 @@
|
|||||||
<li data-reimbursement-id={{reimbursement.id}}
|
<li data-reimbursement-id={{reimbursement.id}}
|
||||||
class="{{item-status reimbursement}}">
|
class="{{item-status reimbursement}}">
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
<span class="recipient"><UserAvatar @contributor={{reimbursement.contributor}} /></span>
|
<span class="title">
|
||||||
<span class="title">Expenses covered by {{reimbursement.contributor.name}}</span>
|
Expenses covered by {{reimbursement.contributor.name}}
|
||||||
|
</span>
|
||||||
|
<span class="recipient">
|
||||||
|
<UserAvatar @contributor={{reimbursement.contributor}} />
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="token-amount">
|
<p class="token-amount">
|
||||||
<span class="amount">
|
<span class="amount">
|
||||||
@@ -26,6 +30,17 @@
|
|||||||
</li>
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
|
{{#if this.kredits.currentUserIsCore}}
|
||||||
|
<p>
|
||||||
|
<a href="{{this.ipfsGatewayUrl}}/{{reimbursement.ipfsHash}}"
|
||||||
|
class="button small" target="_blank" rel="noopener noreferrer">
|
||||||
|
Inspect IPFS data
|
||||||
|
</a>
|
||||||
|
<button {{on "click" (fn this.veto reimbursement.id)}}
|
||||||
|
disabled={{reimbursement.vetoed}}
|
||||||
|
class="button small danger">veto</button>
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
</li>
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
+23
-15
@@ -617,6 +617,18 @@ export default Service.extend({
|
|||||||
yield this.fetchMissingObjects.perform('Reimbursement');
|
yield this.fetchMissingObjects.perform('Reimbursement');
|
||||||
}).group('syncTaskGroup'),
|
}).group('syncTaskGroup'),
|
||||||
|
|
||||||
|
vetoReimbursement (id) {
|
||||||
|
console.debug('[kredits] veto against reimbursement', id);
|
||||||
|
const reimbursement = this.reimbursements.findBy('id', id);
|
||||||
|
|
||||||
|
return this.kredits.Reimbursement.functions.veto(id, { gasLimit: 300000 })
|
||||||
|
.then(data => {
|
||||||
|
console.debug('[kredits] veto response', data);
|
||||||
|
reimbursement.set('pendingTx', data);
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
// Contract events
|
// Contract events
|
||||||
//
|
//
|
||||||
@@ -676,7 +688,7 @@ export default Service.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async handleContributionVetoed (contributionId) {
|
async handleContributionVetoed (contributionId) {
|
||||||
console.debug('[kredits] ContributionVetoed event received for ', contributionId);
|
console.debug('[kredits] ContributionVetoed event received for #', contributionId);
|
||||||
const c = this.contributions.findBy('id', contributionId);
|
const c = this.contributions.findBy('id', contributionId);
|
||||||
|
|
||||||
if (c) {
|
if (c) {
|
||||||
@@ -687,9 +699,6 @@ export default Service.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
|
||||||
// TODO test when reimbursement txs are successful
|
|
||||||
//
|
|
||||||
async handleReimbursementAdded (id, addedByAccount, amount) {
|
async handleReimbursementAdded (id, addedByAccount, amount) {
|
||||||
console.debug('[kredits] ReimbursementAdded event received', { id, addedByAccount, amount });
|
console.debug('[kredits] ReimbursementAdded event received', { id, addedByAccount, amount });
|
||||||
|
|
||||||
@@ -703,20 +712,19 @@ export default Service.extend({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await this.kredits.Reimbursement.getById(id);
|
const data = await this.kredits.Reimbursement.getById(id);
|
||||||
this.loadReimbursementFromData(data);
|
const r = this.loadReimbursementFromData(data);
|
||||||
|
this.browserCache.reimbursements.setItem(r.id.toString(), r.serialize());
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
async handleReimbursementVetoed (id) {
|
||||||
// TODO test when reimbursement txs are successful and veto is implemented
|
console.debug(`[kredits] ReimbursementVetoed received for #${id}`);
|
||||||
//
|
const r = this.reimbursements.findBy('id', id);
|
||||||
handleReimbursementVetoed (id) {
|
console.debug('[kredits] reimbursement', r);
|
||||||
console.debug('[kredits] ReimbursementVetoed received for ', id);
|
|
||||||
const reimbursement = this.reimbursements.findBy('id', id);
|
|
||||||
console.debug('[kredits] reimbursement', this.reimbursement);
|
|
||||||
|
|
||||||
if (reimbursement) {
|
if (r) {
|
||||||
reimbursement.set('vetoed', true);
|
r.set('vetoed', true);
|
||||||
reimbursement.set('pendingTx', null);
|
r.set('pendingTx', null);
|
||||||
|
this.browserCache.reimbursements.setItem(r.id.toString(), r.serialize());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ ul.reimbursement-list {
|
|||||||
grid-row-gap: 0.5rem;
|
grid-row-gap: 0.5rem;
|
||||||
padding-top: 1.6rem;
|
padding-top: 1.6rem;
|
||||||
|
|
||||||
|
&.vetoed {
|
||||||
|
text-decoration: line-through;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
.token-amount {
|
.token-amount {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export default function (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
reader.onload = event => resolve(event.target.result)
|
||||||
|
reader.onerror = error => reject(error)
|
||||||
|
reader.readAsText(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user