WIP Group contributions by URL

This commit is contained in:
2026-07-26 14:08:02 +02:00
parent 606f5c9383
commit a7e017971a
6 changed files with 218 additions and 4 deletions
+9
View File
@@ -2,6 +2,8 @@ import EmberObject, { computed } from '@ember/object';
import { isEmpty, isPresent } from '@ember/utils';
import moment from 'moment';
import contributionGroupingKey from 'kredits-web/utils/contribution-grouping-key';
export default EmberObject.extend({
// Contract
id: null,
@@ -44,6 +46,13 @@ export default EmberObject.extend({
return isPresent(this.pendingTx);
}),
// Stable key shared by all per-contributor contributions created for the
// same issue/pull request (derived from `url`). Manual contributions without
// a URL return null and are treated as singletons (not grouped).
groupId: computed('url', function() {
return contributionGroupingKey(this);
}),
serialize () {
return JSON.stringify(this);
}
+68
View File
@@ -227,6 +227,74 @@ export default Service.extend({
return kreditsByContributor;
}),
// Groups contributions that were created together for the same
// issue/pull request (one contribution per contributor). The grouping key
// is the contribution's `url` (see utils/contribution-grouping-key).
// Manual contributions without a URL each form their own single-item group
// (they are never collapsed together, since `null` is not a real key).
//
// Each group is an EmberObject with: groupId, items, contributors,
// contributorIds, totalAmount, kind, description, url, date, isGrouped
// (true when more than one contribution shares the key).
contributionsGrouped: computed('contributions.[]', function() {
const groupsMap = new Map();
const singletons = [];
this.contributions.forEach(c => {
const groupId = c.groupId;
if (isEmpty(groupId)) {
singletons.push(c);
} else if (groupsMap.has(groupId)) {
groupsMap.get(groupId).items.push(c);
} else {
groupsMap.set(groupId, { groupId, items: [c] });
}
});
const groups = [
...groupsMap.values(),
...singletons.map(c => ({ groupId: null, items: [c] }))
];
return groups.map(group => {
const items = group.items;
const first = items.firstObject;
const contributorIds = items.mapBy('contributorId').uniq();
const contributors = contributorIds
.map(id => this.contributors.findBy('id', id))
.filter(Boolean);
const totalAmount = items
.mapBy('amount')
.reduce((a, b) => a + (b || 0), 0);
return EmberObject.create({
groupId: group.groupId,
items,
contributors,
contributorIds,
totalAmount,
kind: first ? first.kind : null,
description: first ? first.description : null,
url: first ? first.url : null,
date: first ? first.date : null,
isGrouped: items.length > 1
});
});
}),
// Returns the other contributions that share a grouping key with the given
// contribution (i.e. the co-contributors' contributions). Returns an empty
// array when the contribution has no group (manual, no URL) or has no
// siblings.
siblingContributions (contribution) {
if (!contribution) return [];
const groupId = contribution.groupId;
if (isEmpty(groupId)) return [];
const group = this.contributionsGrouped.findBy('groupId', groupId);
if (!group) return [];
return group.items.filter(c => c !== contribution);
},
contributionsUnconfirmed: computed('contributions.[]', 'currentBlock', function() {
return this.contributions
.filter(c => c.confirmedAt > this.currentBlock);
+17
View File
@@ -0,0 +1,17 @@
import { isEmpty } from '@ember/utils';
//
// Derives a stable grouping key for a contribution, so that contributions
// created for the same issue/pull request (one per contributor) can be
// linked together in the UI.
//
// The bot writes the exact same `url` string (the html_url of the issue/PR)
// into every per-contributor IPFS document for a given piece of work, so the
// top-level `url` field on the model is sufficient as a grouping key. Manual
// contributions without a URL return null and are treated as singletons.
//
export default function contributionGroupingKey (contribution) {
let url = contribution ? contribution.url : null;
if (isEmpty(url)) return null;
return url;
}