WIP Group contributions in UI
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
import EmberObject, { computed, observer } from '@ember/object';
|
||||
import { sort } from '@ember/object/computed';
|
||||
import { isPresent } from '@ember/utils';
|
||||
import { isEmpty, isPresent } from '@ember/utils';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
@@ -12,6 +12,7 @@ export default Component.extend({
|
||||
classNames: ['contributions'],
|
||||
|
||||
selectedContribution: null,
|
||||
expandedGroup: null,
|
||||
|
||||
showQuickFilter: false,
|
||||
hideSmallContributions: false,
|
||||
@@ -33,23 +34,96 @@ export default Component.extend({
|
||||
return this.contributions.mapBy('kind').uniq();
|
||||
}),
|
||||
|
||||
contributionsFiltered: computed('contributions.[]', 'hideSmallContributions', 'contributorId', 'contributionKind', function() {
|
||||
return this.contributions.filter(c => {
|
||||
let included = true;
|
||||
// Groups the contributions by their `groupId` (derived from `url`; see
|
||||
// utils/contribution-grouping-key), then filters the groups: a group is
|
||||
// included when any of its items matches the active filters, and when
|
||||
// included all of its items render (grouped rows stay whole — filtering by
|
||||
// a contributor does not collapse a group into a singleton). Preserves the
|
||||
// input order so the controller's date-sort is retained. Manual
|
||||
// contributions without a URL each form their own single-item group.
|
||||
//
|
||||
// Each group object exposes: groupId, items, contributors, contributorIds,
|
||||
// amount (single contribution's, not a sum), kind, description, url, date,
|
||||
// isGrouped, visibleContributors (capped at 3), extraContributorCount.
|
||||
groupedContributions: computed('contributions.[]', 'hideSmallContributions', 'contributorId', 'contributionKind', function() {
|
||||
const groupsMap = new Map();
|
||||
const orderedGroups = [];
|
||||
|
||||
if (this.hideSmallContributions &&
|
||||
c.amount <= 500) { included = false; }
|
||||
|
||||
if (isPresent(this.contributorId) &&
|
||||
c.contributorId !== parseInt(this.contributorId)) { included = false; }
|
||||
|
||||
if (isPresent(this.contributionKind) &&
|
||||
c.kind !== this.contributionKind) { included = false; }
|
||||
|
||||
return included;
|
||||
this.contributions.forEach(c => {
|
||||
const groupId = c.groupId;
|
||||
if (isEmpty(groupId)) {
|
||||
orderedGroups.push({ groupId: null, items: [c] });
|
||||
} else if (groupsMap.has(groupId)) {
|
||||
groupsMap.get(groupId).items.push(c);
|
||||
} else {
|
||||
const group = { groupId, items: [c] };
|
||||
groupsMap.set(groupId, group);
|
||||
orderedGroups.push(group);
|
||||
}
|
||||
});
|
||||
|
||||
return orderedGroups
|
||||
.filter(raw => raw.items.any(c => this.matchesFilters(c)))
|
||||
.map(raw => {
|
||||
const items = raw.items;
|
||||
const first = items.firstObject;
|
||||
const contributorIds = items.mapBy('contributorId').uniq();
|
||||
const contributors = contributorIds
|
||||
.map(id => this.contributors.findBy('id', id))
|
||||
.filter(Boolean);
|
||||
|
||||
return EmberObject.create({
|
||||
groupId: raw.groupId,
|
||||
items,
|
||||
contributors,
|
||||
contributorIds,
|
||||
amount: first ? first.amount : null,
|
||||
kind: first ? first.kind : null,
|
||||
description: first ? first.description : null,
|
||||
url: first ? first.url : null,
|
||||
date: first ? first.date : null,
|
||||
isGrouped: items.length > 1,
|
||||
visibleContributors: contributors.slice(0, 3),
|
||||
extraContributorCount: Math.max(0, contributors.length - 3)
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
// Shared filter predicates for a single contribution. Used to decide group
|
||||
// inclusion in `groupedContributions`. A group is shown when any of its
|
||||
// items passes; "hide small contributions" thus keeps a group as long as
|
||||
// any item is above the threshold.
|
||||
matchesFilters (c) {
|
||||
if (this.hideSmallContributions && c.amount <= 500) return false;
|
||||
if (isPresent(this.contributorId) &&
|
||||
c.contributorId !== parseInt(this.contributorId)) return false;
|
||||
if (isPresent(this.contributionKind) &&
|
||||
c.kind !== this.contributionKind) return false;
|
||||
return true;
|
||||
},
|
||||
|
||||
// Auto-expands the group containing the selected contribution so the
|
||||
// selected child row is visible with the `selected` highlight.
|
||||
autoExpandSelected: observer('selectedContributionId', function() {
|
||||
this._autoExpandSelected();
|
||||
}),
|
||||
|
||||
_autoExpandSelected () {
|
||||
const selectedId = this.selectedContributionId;
|
||||
if (!selectedId) return;
|
||||
const group = this.groupedContributions.find(g =>
|
||||
g.items.findBy('id', selectedId)
|
||||
);
|
||||
if (group && group.isGrouped && this.expandedGroup !== group.groupId) {
|
||||
this.set('expandedGroup', group.groupId);
|
||||
}
|
||||
},
|
||||
|
||||
init () {
|
||||
this._super(...arguments);
|
||||
this._autoExpandSelected();
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
veto (contributionId) {
|
||||
@@ -62,6 +136,14 @@ export default Component.extend({
|
||||
|
||||
openContributionDetails(contribution) {
|
||||
this.router.transitionTo('dashboard.contributions.show', contribution);
|
||||
},
|
||||
|
||||
toggleGroup (group) {
|
||||
if (this.expandedGroup === group.groupId) {
|
||||
this.set('expandedGroup', null);
|
||||
} else {
|
||||
this.set('expandedGroup', group.groupId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -30,27 +30,76 @@
|
||||
{{/if}}
|
||||
|
||||
<ul class="item-list contribution-list {{if @loading 'loading'}}">
|
||||
{{#each this.contributionsFiltered as |contribution|}}
|
||||
<li role="button" data-contribution-id={{contribution.id}}
|
||||
{{action "openContributionDetails" contribution}}
|
||||
class="{{item-status contribution}}{{if (eq contribution.id @selectedContributionId) " selected"}}">
|
||||
<p class="meta">
|
||||
<span class="recipient"><UserAvatar @contributor={{contribution.contributor}} /></span>
|
||||
<span class="category {{contribution.kind}}">({{contribution.kind}})</span>
|
||||
<span class="title">{{contribution.description}}</span>
|
||||
</p>
|
||||
<p class="kredits-amount">
|
||||
<span class="amount">{{contribution.amount}}</span><span class="symbol">₭S</span>
|
||||
</p>
|
||||
{{#unless contribution.vetoed}}
|
||||
{{#unless (is-confirmed-contribution contribution)}}
|
||||
<p class="voting">
|
||||
{{input type="button" class="button small danger" value="veto"
|
||||
click=(action "veto" contribution.id)
|
||||
disabled=contribution.hasPendingChanges}}
|
||||
{{#each this.groupedContributions as |group|}}
|
||||
{{#if group.isGrouped}}
|
||||
<li role="button" data-group-id={{group.groupId}}
|
||||
{{action "toggleGroup" group}}
|
||||
class="grouped {{if (eq this.expandedGroup group.groupId) "expanded"}}">
|
||||
<p class="meta">
|
||||
<span class="avatars">
|
||||
{{#each group.visibleContributors as |contributor|}}
|
||||
<UserAvatar @contributor={{contributor}} />
|
||||
{{/each}}
|
||||
{{#if group.extraContributorCount}}
|
||||
<span class="avatar-overflow">+{{group.extraContributorCount}}</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class="category {{group.kind}}">({{group.kind}})</span>
|
||||
<span class="title">{{group.description}}</span>
|
||||
</p>
|
||||
<p class="kredits-amount">
|
||||
<span class="amount">{{group.amount}}</span><span class="symbol">₭S</span>
|
||||
</p>
|
||||
</li>
|
||||
{{#if (eq this.expandedGroup group.groupId)}}
|
||||
{{#each group.items key="id" as |contribution|}}
|
||||
<li role="button" data-contribution-id={{contribution.id}}
|
||||
{{action "openContributionDetails" contribution}}
|
||||
class="group-item {{item-status contribution}}{{if (eq contribution.id @selectedContributionId) " selected"}}">
|
||||
<p class="meta">
|
||||
<span class="recipient"><UserAvatar @contributor={{contribution.contributor}} /></span>
|
||||
<span class="category {{contribution.kind}}">({{contribution.kind}})</span>
|
||||
<span class="title">{{contribution.description}}</span>
|
||||
</p>
|
||||
<p class="kredits-amount">
|
||||
<span class="amount">{{contribution.amount}}</span><span class="symbol">₭S</span>
|
||||
</p>
|
||||
{{#unless contribution.vetoed}}
|
||||
{{#unless (is-confirmed-contribution contribution)}}
|
||||
<p class="voting">
|
||||
{{input type="button" class="button small danger" value="veto"
|
||||
click=(action "veto" contribution.id)
|
||||
disabled=contribution.hasPendingChanges}}
|
||||
</p>
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#each group.items as |contribution|}}
|
||||
<li role="button" data-contribution-id={{contribution.id}}
|
||||
{{action "openContributionDetails" contribution}}
|
||||
class="{{item-status contribution}}{{if (eq contribution.id @selectedContributionId) " selected"}}">
|
||||
<p class="meta">
|
||||
<span class="recipient"><UserAvatar @contributor={{contribution.contributor}} /></span>
|
||||
<span class="category {{contribution.kind}}">({{contribution.kind}})</span>
|
||||
<span class="title">{{contribution.description}}</span>
|
||||
</p>
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
</li>
|
||||
<p class="kredits-amount">
|
||||
<span class="amount">{{contribution.amount}}</span><span class="symbol">₭S</span>
|
||||
</p>
|
||||
{{#unless contribution.vetoed}}
|
||||
{{#unless (is-confirmed-contribution contribution)}}
|
||||
<p class="voting">
|
||||
{{input type="button" class="button small danger" value="veto"
|
||||
click=(action "veto" contribution.id)
|
||||
disabled=contribution.hasPendingChanges}}
|
||||
</p>
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user