0
Fork 0
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:
Kevin Ansfield 2022-01-19 12:57:58 +00:00
parent 8bcb534feb
commit 92ece7b373
9 changed files with 211 additions and 50 deletions

View file

@ -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">

View file

@ -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>

View file

@ -0,0 +1 @@
{{yield this.parsedEvents}}

View file

@ -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
};
});
}
}

View file

@ -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);
}

View 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
}
}
});

View file

@ -0,0 +1,6 @@
import {Model, belongsTo} from 'ember-cli-mirage';
export default Model.extend({
email: belongsTo(),
member: belongsTo()
});

View 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};
}
});

View file

@ -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;
});
});