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:
Râu Cao
2023-01-09 11:34:37 +08:00
committed by GitHub
9 changed files with 98 additions and 20 deletions
+3 -1
View File
@@ -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);
+19 -1
View File
@@ -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,8 +85,25 @@ 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 @action
updateContributor(event) { 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
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>
+1 -1
View File
@@ -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);
});
}
} }
+17 -2
View File
@@ -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
View File
@@ -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;
+8
View File
@@ -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)
})
}