diff --git a/ghost/admin/app/components/member/activity-feed.hbs b/ghost/admin/app/components/member/activity-feed.hbs index 8c22e50d032..95d7d54747a 100644 --- a/ghost/admin/app/components/member/activity-feed.hbs +++ b/ghost/admin/app/components/member/activity-feed.hbs @@ -6,7 +6,7 @@ {{else}} - {{#let (members-event-fetcher filter=(members-event-filter member=@member.id) pageSize=5) as |eventsFetcher|}} + {{#let (members-event-fetcher filter=(members-event-filter member=@member.id excludedEvents=this.excludedEventTypes) pageSize=5) as |eventsFetcher|}}
diff --git a/ghost/admin/app/components/member/activity-feed.js b/ghost/admin/app/components/member/activity-feed.js index d9f5654a5a2..ec9e34e1483 100644 --- a/ghost/admin/app/components/member/activity-feed.js +++ b/ghost/admin/app/components/member/activity-feed.js @@ -3,6 +3,7 @@ import {action} from '@ember/object'; export default class ActivityFeed extends Component { linkScrollerTimeout = null; // needs to be global so can be cleared when needed across functions + excludedEventTypes = ['email_sent_event']; @action enterLinkURL(event) { @@ -29,4 +30,4 @@ export default class ActivityFeed extends Component { child.style.transform = 'translateX(0)'; parent.classList.remove('scroller'); } -} \ No newline at end of file +} diff --git a/ghost/admin/app/components/posts/post-activity-feed.js b/ghost/admin/app/components/posts/post-activity-feed.js index a42551abfb8..0f44846cf87 100644 --- a/ghost/admin/app/components/posts/post-activity-feed.js +++ b/ghost/admin/app/components/posts/post-activity-feed.js @@ -6,6 +6,7 @@ const allEvents = [ 'click_event', 'signup_event', 'subscription_event', + 'email_sent_event', 'email_delivered_event', 'email_opened_event', 'email_failed_event', @@ -13,7 +14,7 @@ const allEvents = [ ]; const eventTypes = { - sent: ['email_delivered_event'], + sent: ['email_sent_event'], opened: ['email_opened_event'], clicked: ['click_event'], feedback: ['feedback_event'], diff --git a/ghost/admin/app/controllers/members-activity.js b/ghost/admin/app/controllers/members-activity.js index ed22f6671af..07b9004a9a4 100644 --- a/ghost/admin/app/controllers/members-activity.js +++ b/ghost/admin/app/controllers/members-activity.js @@ -26,6 +26,9 @@ export default class MembersActivityController extends Controller { if (!this.member) { hiddenEvents.push(...EMAIL_EVENTS); + } else { + // Always hide sent event + hiddenEvents.push('email_sent_event'); } if (this.settings.editorDefaultEmailRecipients === 'disabled') { diff --git a/ghost/admin/app/helpers/members-event-filter.js b/ghost/admin/app/helpers/members-event-filter.js index 489468a50c7..a19f8b36559 100644 --- a/ghost/admin/app/helpers/members-event-filter.js +++ b/ghost/admin/app/helpers/members-event-filter.js @@ -3,7 +3,7 @@ import classic from 'ember-classic-decorator'; import {isBlank} from '@ember/utils'; import {inject as service} from '@ember/service'; -export const EMAIL_EVENTS = ['email_delivered_event','email_opened_event','email_failed_event']; +export const EMAIL_EVENTS = ['email_sent_event', 'email_delivered_event', 'email_opened_event','email_failed_event']; export const NEWSLETTER_EVENTS = ['newsletter_event']; @classic diff --git a/ghost/admin/app/helpers/parse-member-event.js b/ghost/admin/app/helpers/parse-member-event.js index 164230a8c0a..a0a904b54a3 100644 --- a/ghost/admin/app/helpers/parse-member-event.js +++ b/ghost/admin/app/helpers/parse-member-event.js @@ -72,7 +72,7 @@ export default class ParseMemberEventHelper extends Helper { icon = 'opened-email'; } - if (event.type === 'email_delivered_event') { + if (event.type === 'email_delivered_event' || event.type === 'email_sent_event') { icon = 'received-email'; } @@ -149,7 +149,7 @@ export default class ParseMemberEventHelper extends Helper { return 'opened email'; } - if (event.type === 'email_delivered_event') { + if (event.type === 'email_delivered_event' || event.type === 'email_sent_event') { return 'received email'; } diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap index e0e4be36181..7d1bf8e8cab 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap @@ -43,15 +43,35 @@ Object { "data": Any, "type": Any, }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, ], "meta": Object { "pagination": Object { - "limit": 10, + "limit": "20", "next": null, "page": null, "pages": 1, "prev": null, - "total": 10, + "total": 15, }, }, } @@ -61,7 +81,7 @@ exports[`Activity Feed API Can filter events by post id 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "17358", + "content-length": "23031", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -86,9 +106,9 @@ Object { "limit": "2", "next": null, "page": null, - "pages": 5, + "pages": 8, "prev": null, - "total": 10, + "total": 15, }, }, } @@ -422,6 +442,55 @@ Object { } `; +exports[`Activity Feed API Returns email sent events in activity feed 1: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": 10, + "next": null, + "page": null, + "pages": 1, + "prev": null, + "total": 5, + }, + }, +} +`; + +exports[`Activity Feed API Returns email sent events in activity feed 2: [headers] 1`] = ` +Object { + "access-control-allow-origin": "http://127.0.0.1:2369", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "5774", + "content-type": "application/json; charset=utf-8", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Accept-Version, Origin, Accept-Encoding", + "x-powered-by": "Express", +} +`; + exports[`Activity Feed API Returns feedback events in activity feed 1: [body] 1`] = ` Object { "events": Array [ diff --git a/ghost/core/test/e2e-api/admin/activity-feed.test.js b/ghost/core/test/e2e-api/admin/activity-feed.test.js index 1074806c545..282b97686a2 100644 --- a/ghost/core/test/e2e-api/admin/activity-feed.test.js +++ b/ghost/core/test/e2e-api/admin/activity-feed.test.js @@ -126,6 +126,26 @@ describe('Activity Feed API', function () { }); }); + it('Returns email sent events in activity feed', async function () { + // Check activity feed + await agent + .get(`/members/events?filter=type:email_sent_event`) + .expectStatus(200) + .matchHeaderSnapshot({ + etag: anyEtag + }) + .matchBodySnapshot({ + events: new Array(5).fill({ + type: anyString, + data: anyObject + }) + }) + .expect(({body}) => { + assert(body.events.find(e => e.type === 'email_sent_event'), 'Expected an email sent event'); + assert(!body.events.find(e => e.type !== 'email_sent_event'), 'Expected only email sent events'); + }); + }); + it('Returns email delivered events in activity feed', async function () { // Check activity feed await agent @@ -170,13 +190,13 @@ describe('Activity Feed API', function () { const postId = fixtureManager.get('posts', 0).id; await agent - .get(`/members/events?filter=data.post_id:${postId}`) + .get(`/members/events?filter=data.post_id:${postId}&limit=20`) .expectStatus(200) .matchHeaderSnapshot({ etag: anyEtag }) .matchBodySnapshot({ - events: new Array(10).fill({ + events: new Array(15).fill({ type: anyString, data: anyObject }) @@ -191,10 +211,11 @@ describe('Activity Feed API', function () { assert(body.events.find(e => e.type === 'signup_event'), 'Expected a signup event'); assert(body.events.find(e => e.type === 'subscription_event'), 'Expected a subscription event'); assert(body.events.find(e => e.type === 'email_delivered_event'), 'Expected an email delivered event'); + assert(body.events.find(e => e.type === 'email_sent_event'), 'Expected an email sent event'); assert(body.events.find(e => e.type === 'email_opened_event'), 'Expected an email opened event'); // Assert total is correct - assert.equal(body.meta.pagination.total, 10); + assert.equal(body.meta.pagination.total, 15); }); }); @@ -216,7 +237,7 @@ describe('Activity Feed API', function () { assert(!body.events.find(e => (e.data?.post?.id ?? e.data?.attribution?.id ?? e.data?.email?.post_id) !== postId), 'Should only return events for the post'); // Assert total is correct - assert.equal(body.meta.pagination.total, 10); + assert.equal(body.meta.pagination.total, 15); }); }); }); diff --git a/ghost/members-api/lib/repositories/event.js b/ghost/members-api/lib/repositories/event.js index 52c9c28bfcf..d9ba3d4bbff 100644 --- a/ghost/members-api/lib/repositories/event.js +++ b/ghost/members-api/lib/repositories/event.js @@ -60,6 +60,7 @@ module.exports = class EventRepository { } if (this._EmailRecipient) { + pageActions.push({type: 'email_sent_event', action: 'getEmailSentEvents'}); pageActions.push({type: 'email_delivered_event', action: 'getEmailDeliveredEvents'}); pageActions.push({type: 'email_opened_event', action: 'getEmailOpenedEvents'}); pageActions.push({type: 'email_failed_event', action: 'getEmailFailedEvents'}); @@ -431,6 +432,46 @@ module.exports = class EventRepository { }; } + async getEmailSentEvents(options = {}, filters = {}) { + options = { + ...options, + withRelated: ['member', 'email'], + filter: ['failed_at:null', 'processed_at:-null'] + }; + if (filters['data.created_at']) { + options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'processed_at:')); + } + if (filters['data.member_id']) { + options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:')); + } + if (filters['data.post_id']) { + options.filter.push(filters['data.post_id'].replace(/data.post_id:/g, 'email.post_id:')); + } + options.filter = options.filter.join('+'); + options.order = options.order.replace(/created_at/g, 'processed_at'); + + const {data: models, meta} = await this._EmailRecipient.findPage( + options + ); + + const data = models.map((model) => { + return { + type: 'email_sent_event', + data: { + member_id: model.get('member_id'), + created_at: model.get('processed_at'), + member: model.related('member').toJSON(), + email: model.related('email').toJSON() + } + }; + }); + + return { + data, + meta + }; + } + async getEmailDeliveredEvents(options = {}, filters = {}) { options = { ...options,