mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-08 02:52:39 -05:00
Refactored member activity list components
refs https://github.com/TryGhost/Team/issues/1277 - renamed `<GhActivityTimeline>` to `<GhMemberActivityEventParser>` and modified so that it yields parsed events rather than directly renders them - makes the component re-usable as it can be used to decorate raw events ready for use in context-specific templates - switches to using a getter to yield the parsed events so that they will update automatically when the `@events` argument changes - updated `<Dashboard::LatestMemberActivity>` to use `<GhMemberActivityEventParser>` and keep the member activity box output local to itself - added integration tests for `<Dashboard::LatestMemberActivity>` - added Mirage setup for member activity event models/serializers/route
This commit is contained in:
parent
8bcb534feb
commit
92ece7b373
9 changed files with 211 additions and 50 deletions
|
@ -11,7 +11,31 @@
|
|||
<code>{{this.eventsError.message}}</code>
|
||||
</p>
|
||||
{{else}}
|
||||
<GhEventTimeline @events={{this.eventsData}}/>
|
||||
<GhMemberActivityEventParser @events={{this.eventsData}} as |parsedEvents|>
|
||||
<div class="gh-event-timeline">
|
||||
{{#if parsedEvents}}
|
||||
<ul class="gh-dashboard-activity-feed">
|
||||
{{#each parsedEvents as |event|}}
|
||||
<li data-test-dashboard-member-activity-item>
|
||||
<LinkTo class="member-details" @route="member" @model="{{event.member_id}}">
|
||||
<div class="activity">
|
||||
<div>
|
||||
<span class="member">{{event.subject}}</span> {{event.action}} {{event.object}} <span class="highlight">{{event.info}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</LinkTo>
|
||||
<span class="time">{{event.timestamp}}</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<div class="gh-no-data-list" data-test-no-member-activities>
|
||||
{{svg-jar "no-data-list"}}
|
||||
<span>No member activity available.</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</GhMemberActivityEventParser>
|
||||
|
||||
{{#if (feature "membersActivityFeed")}}
|
||||
<div class="gh-dashboard-top-members-footer">
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
<div class="gh-event-timeline">
|
||||
{{#unless this.parsedEvents}}
|
||||
<div class="gh-no-data-list">
|
||||
{{svg-jar "no-data-list"}}
|
||||
<span>No member activity available.</span>
|
||||
</div>
|
||||
{{/unless}}
|
||||
<ul class="gh-dashboard-activity-feed">
|
||||
{{#each this.parsedEvents as |event|}}
|
||||
<li>
|
||||
<LinkTo class="member-details" @route="member" @model="{{event.member_id}}">
|
||||
<div class="activity">
|
||||
<div>
|
||||
<span class="member">{{event.subject}}</span> {{event.action}} {{event.object}} <span class="highlight">{{event.info}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</LinkTo>
|
||||
<span class="time">{{event.timestamp}}</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
|
@ -0,0 +1 @@
|
|||
{{yield this.parsedEvents}}
|
|
@ -1,15 +1,30 @@
|
|||
import Component from '@glimmer/component';
|
||||
import moment from 'moment';
|
||||
import {getNonDecimal, getSymbol} from 'ghost-admin/utils/currency';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class EventTimeline extends Component {
|
||||
@tracked
|
||||
parsedEvents = null;
|
||||
get parsedEvents() {
|
||||
if (!this.args.events) {
|
||||
return [];
|
||||
}
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.parseEvents(this.args.events);
|
||||
return this.args.events.map((event) => {
|
||||
let subject = event.data.member.name || event.data.member.email;
|
||||
let icon = this.getIcon(event);
|
||||
let action = this.getAction(event);
|
||||
let object = this.getObject(event);
|
||||
let info = this.getInfo(event);
|
||||
let timestamp = moment(event.data.created_at).fromNow();
|
||||
return {
|
||||
member_id: event.data.member_id,
|
||||
icon,
|
||||
subject,
|
||||
action,
|
||||
object,
|
||||
info,
|
||||
timestamp
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getIcon(event) {
|
||||
|
@ -74,24 +89,4 @@ export default class EventTimeline extends Component {
|
|||
}
|
||||
return;
|
||||
}
|
||||
|
||||
parseEvents(events) {
|
||||
this.parsedEvents = events.map((event) => {
|
||||
let subject = event.data.member.name || event.data.member.email;
|
||||
let icon = this.getIcon(event);
|
||||
let action = this.getAction(event);
|
||||
let object = this.getObject(event);
|
||||
let info = this.getInfo(event);
|
||||
let timestamp = moment(event.data.created_at).fromNow();
|
||||
return {
|
||||
member_id: event.data.member_id,
|
||||
icon,
|
||||
subject,
|
||||
action,
|
||||
object,
|
||||
info,
|
||||
timestamp
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import faker from 'faker';
|
||||
import moment from 'moment';
|
||||
import {Response} from 'ember-cli-mirage';
|
||||
import {extractFilterParam, paginateModelCollection} from '../utils';
|
||||
import {extractFilterParam, paginateModelCollection, paginatedResponse} from '../utils';
|
||||
import {isEmpty} from '@ember/utils';
|
||||
|
||||
export function mockMembersStats(server) {
|
||||
|
@ -154,5 +154,17 @@ export default function mockMembers(server) {
|
|||
}, '');
|
||||
});
|
||||
|
||||
server.get('/members/events/', function ({memberActivityEvents}, {queryParams}) {
|
||||
let {limit} = queryParams;
|
||||
|
||||
limit = +limit || 15;
|
||||
|
||||
let collection = memberActivityEvents.all().sort((a, b) => {
|
||||
return (new Date(a.createdAt)) - (new Date(b.createdAt));
|
||||
}).slice(0, limit);
|
||||
|
||||
return collection;
|
||||
});
|
||||
|
||||
mockMembersStats(server);
|
||||
}
|
||||
|
|
53
ghost/admin/mirage/factories/member-activity-event.js
Normal file
53
ghost/admin/mirage/factories/member-activity-event.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
import faker from 'faker';
|
||||
import moment from 'moment';
|
||||
import {Factory} from 'ember-cli-mirage';
|
||||
|
||||
const EVENT_TYPES = [
|
||||
'newsletter_event',
|
||||
'login_event',
|
||||
'subscription_event',
|
||||
'payment_event',
|
||||
'login_event',
|
||||
'signup_event',
|
||||
'email_delivered_event',
|
||||
'email_opened_event',
|
||||
'email_failed_event'
|
||||
];
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export default Factory.extend({
|
||||
type() { return faker.random.arrayElement([EVENT_TYPES]); },
|
||||
createdAt() { return moment.utc().format(); },
|
||||
|
||||
afterCreate(event, server) {
|
||||
if (!event.member) {
|
||||
event.update({member: server.create('member')});
|
||||
}
|
||||
|
||||
if (event.type === 'newsletter_event') {
|
||||
event.update({
|
||||
data: {
|
||||
source: 'member',
|
||||
subscribed: event.subscribed !== undefined ? event.subscribed : faker.datatype.boolean()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (event.type === 'subscription_event') {
|
||||
event.update({
|
||||
data: {
|
||||
source: 'stripe'
|
||||
// TODO: add from_plan, to_plan, currency, mrr_delta
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (event.type === 'payment_event') {
|
||||
// TODO: add data attributes
|
||||
}
|
||||
|
||||
if (event.type === 'signup_event') {
|
||||
// TODO: add data attributes
|
||||
}
|
||||
}
|
||||
});
|
6
ghost/admin/mirage/models/member-activity-event.js
Normal file
6
ghost/admin/mirage/models/member-activity-event.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import {Model, belongsTo} from 'ember-cli-mirage';
|
||||
|
||||
export default Model.extend({
|
||||
email: belongsTo(),
|
||||
member: belongsTo()
|
||||
});
|
42
ghost/admin/mirage/serializers/member-activity-event.js
Normal file
42
ghost/admin/mirage/serializers/member-activity-event.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import BaseSerializer from './application';
|
||||
|
||||
export default BaseSerializer.extend({
|
||||
embed: true,
|
||||
|
||||
keyForCollection() {
|
||||
return 'events';
|
||||
},
|
||||
|
||||
include() {
|
||||
// these are always embedded but will be moved onto the `data` object in the serializer
|
||||
return ['member', 'email'];
|
||||
},
|
||||
|
||||
serialize() {
|
||||
const serialized = BaseSerializer.prototype.serialize.call(this, ...arguments);
|
||||
|
||||
const events = serialized.events.map((serializedEvent) => {
|
||||
const data = Object.assign({}, serializedEvent.data, {
|
||||
id: serializedEvent.id,
|
||||
created_at: serializedEvent.created_at
|
||||
});
|
||||
|
||||
if (serializedEvent.member) {
|
||||
data.member = serializedEvent.member;
|
||||
data.member_id = serializedEvent.member_id;
|
||||
}
|
||||
|
||||
if (serializedEvent.email) {
|
||||
data.email = serializedEvent.email;
|
||||
data.email_id = serializedEvent.email_id;
|
||||
}
|
||||
|
||||
return {
|
||||
type: serializedEvent.type,
|
||||
data
|
||||
};
|
||||
});
|
||||
|
||||
return {events};
|
||||
}
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
import hbs from 'htmlbars-inline-precompile';
|
||||
import {authenticateSession} from 'ember-simple-auth/test-support';
|
||||
import {describe, it} from 'mocha';
|
||||
import {expect} from 'chai';
|
||||
import {find, findAll, render} from '@ember/test-helpers';
|
||||
import {setupMirage} from 'ember-cli-mirage/test-support';
|
||||
import {setupRenderingTest} from 'ember-mocha';
|
||||
|
||||
describe('Integration: Component: <Dashboard::LatestMemberActivity>', function () {
|
||||
const hooks = setupRenderingTest();
|
||||
setupMirage(hooks);
|
||||
|
||||
it('renders with no activities', async function () {
|
||||
await render(hbs(`<Dashboard::LatestMemberActivity />`));
|
||||
|
||||
expect(find('[data-test-dashboard-member-activity]')).to.exist;
|
||||
expect(find('[data-test-no-member-activities]')).to.exist;
|
||||
});
|
||||
|
||||
it('renders 5 latest activities', async function () {
|
||||
this.server.createList('member-activity-event', 10);
|
||||
|
||||
await render(hbs(`<Dashboard::LatestMemberActivity />`));
|
||||
|
||||
expect(find('[data-test-dashboard-member-activity]')).to.exist;
|
||||
expect(find('[data-test-no-member-activities]')).to.not.exist;
|
||||
|
||||
expect(findAll('[data-test-dashboard-member-activity-item]').length).to.equal(5);
|
||||
});
|
||||
|
||||
it('renders nothing when owner has not completed launch', async function () {
|
||||
let role = this.server.create('role', {name: 'Owner'});
|
||||
this.server.create('user', {roles: [role]});
|
||||
await authenticateSession();
|
||||
const sessionService = this.owner.lookup('service:session');
|
||||
await sessionService.populateUser();
|
||||
|
||||
this.server.create('setting', {
|
||||
key: 'editor_is_launch_complete',
|
||||
value: false,
|
||||
group: 'editor'
|
||||
});
|
||||
const settingsService = this.owner.lookup('service:settings');
|
||||
await settingsService.fetch();
|
||||
|
||||
await render(hbs(`<Dashboard::LatestMemberActivity />`));
|
||||
|
||||
expect(find('[data-test-dashboard-member-activity]')).to.not.exist;
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue