mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
🎨 Added 'Changed email address' event to Member Activity (#20493)
fixes https://linear.app/tryghost/issue/ENG-1256 - when a member changes their email address, surface it in Member Activity
This commit is contained in:
parent
fca8941740
commit
7f963e9c2a
8 changed files with 264 additions and 7 deletions
|
@ -11,7 +11,8 @@ const ALL_EVENT_TYPES = [
|
||||||
{event: 'email_opened_event', icon: 'filter-dropdown-email-opened', name: 'Email opened', group: 'emails'},
|
{event: 'email_opened_event', icon: 'filter-dropdown-email-opened', name: 'Email opened', group: 'emails'},
|
||||||
{event: 'email_delivered_event', icon: 'filter-dropdown-email-received', name: 'Email received', group: 'emails'},
|
{event: 'email_delivered_event', icon: 'filter-dropdown-email-received', name: 'Email received', group: 'emails'},
|
||||||
{event: 'email_complaint_event', icon: 'filter-dropdown-email-flagged-as-spam', name: 'Email flagged as spam', group: 'emails'},
|
{event: 'email_complaint_event', icon: 'filter-dropdown-email-flagged-as-spam', name: 'Email flagged as spam', group: 'emails'},
|
||||||
{event: 'email_failed_event', icon: 'filter-dropdown-email-bounced', name: 'Email bounced', group: 'emails'}
|
{event: 'email_failed_event', icon: 'filter-dropdown-email-bounced', name: 'Email bounced', group: 'emails'},
|
||||||
|
{event: 'email_change_event', icon: 'filter-dropdown-email-address-changed', name: 'Email address changed', group: 'emails'}
|
||||||
];
|
];
|
||||||
|
|
||||||
export default class MembersActivityEventTypeFilter extends Component {
|
export default class MembersActivityEventTypeFilter extends Component {
|
||||||
|
|
|
@ -114,6 +114,10 @@ export default class ParseMemberEventHelper extends Helper {
|
||||||
icon = 'subscriptions';
|
icon = 'subscriptions';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.type === 'email_change_event') {
|
||||||
|
icon = 'email-changed';
|
||||||
|
}
|
||||||
|
|
||||||
return 'event-' + icon;
|
return 'event-' + icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,8 +212,15 @@ export default class ParseMemberEventHelper extends Helper {
|
||||||
return 'less like this';
|
return 'less like this';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.type === 'email_change_event') {
|
||||||
|
if (event.data.from_email && event.data.to_email) {
|
||||||
|
return `Email address changed from ${event.data.from_email} to ${event.data.to_email}`;
|
||||||
|
}
|
||||||
|
return 'Email address changed';
|
||||||
|
}
|
||||||
|
|
||||||
if (event.type === 'donation_event') {
|
if (event.type === 'donation_event') {
|
||||||
return `Made a one-time payment`;
|
return 'Made a one-time payment';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
4
ghost/admin/public/assets/icons/event-email-changed.svg
Normal file
4
ghost/admin/public/assets/icons/event-email-changed.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke="#6C747D" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14.6 8.364v5.818c0 1.715 2.703 1.962 4.067-.356 1.157-1.963.873-4.955-.571-6.923-2.125-2.9-7.039-3.984-10.608-1.588-3.28 2.202-4.448 6.658-2.635 10.258 1.793 3.564 6.003 5.29 9.813 4.002"/>
|
||||||
|
<path stroke="#6C747D" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11.52 15.23c1.701 0 3.08-1.495 3.08-3.34 0-1.846-1.379-3.341-3.08-3.341-1.702 0-3.081 1.495-3.081 3.34 0 1.846 1.38 3.342 3.08 3.342Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 597 B |
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M10.736 4.806v5.058c0 1.56 2.371 1.907 3.613-.203 1.052-1.785.794-4.509-.52-6.302C11.894.722 7.422-.265 4.173 1.916 1.19 3.92.127 7.975 1.776 11.252c1.632 3.243 5.463 4.814 8.931 3.641"/>
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M7.882 10.94c1.57 0 2.843-1.362 2.843-3.041 0-1.68-1.273-3.04-2.843-3.04S5.04 6.218 5.04 7.898c0 1.679 1.272 3.04 2.842 3.04Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 561 B |
|
@ -2910,12 +2910,12 @@ Object {
|
||||||
Object {
|
Object {
|
||||||
"comped": 4,
|
"comped": 4,
|
||||||
"date": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
"date": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||||
"free": 4,
|
"free": 5,
|
||||||
"paid": 2,
|
"paid": 2,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"resource": "members",
|
"resource": "members",
|
||||||
"total": 10,
|
"total": 11,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -6323,6 +6323,130 @@ Object {
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Members API can change the email address 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": null,
|
||||||
|
"referrer_medium": "Ghost Admin",
|
||||||
|
"referrer_source": "Created manually",
|
||||||
|
"referrer_url": null,
|
||||||
|
"title": null,
|
||||||
|
"type": null,
|
||||||
|
"url": null,
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": "jon.snow@test.com",
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"email_suppression": Object {
|
||||||
|
"info": null,
|
||||||
|
"suppressed": false,
|
||||||
|
},
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": "Jon Snow",
|
||||||
|
"newsletters": Array [],
|
||||||
|
"note": null,
|
||||||
|
"status": "free",
|
||||||
|
"subscribed": false,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Array [],
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API can change the email address 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": "665",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
|
||||||
|
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API can change the email address 3: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": null,
|
||||||
|
"referrer_medium": "Ghost Admin",
|
||||||
|
"referrer_source": "Created manually",
|
||||||
|
"referrer_url": null,
|
||||||
|
"title": null,
|
||||||
|
"type": null,
|
||||||
|
"url": null,
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": "aegon.targaryen@test.com",
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"email_suppression": Object {
|
||||||
|
"info": null,
|
||||||
|
"suppressed": false,
|
||||||
|
},
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": "Jon Snow",
|
||||||
|
"newsletters": Array [],
|
||||||
|
"note": null,
|
||||||
|
"status": "free",
|
||||||
|
"subscribed": false,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Array [],
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API can change the email address 4: [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": "672",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API can change the email address 5: [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": "1118",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Members API without Stripe Add should fail when comped flag is passed in but Stripe is not enabled 1: [body] 1`] = `
|
exports[`Members API without Stripe Add should fail when comped flag is passed in but Stripe is not enabled 1: [body] 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"errors": Array [
|
"errors": Array [
|
||||||
|
|
|
@ -2131,6 +2131,83 @@ describe('Members API', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can change the email address', async function () {
|
||||||
|
const memberToChange = {
|
||||||
|
name: 'Jon Snow',
|
||||||
|
email: 'jon.snow@test.com',
|
||||||
|
newsletters: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const memberChanged = {
|
||||||
|
email: 'aegon.targaryen@test.com'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create member
|
||||||
|
const {body} = await agent
|
||||||
|
.post(`/members/`)
|
||||||
|
.body({members: [memberToChange]})
|
||||||
|
.expectStatus(201)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 0))
|
||||||
|
})
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
'content-version': anyContentVersion,
|
||||||
|
etag: anyEtag,
|
||||||
|
location: anyLocationFor('members')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update email address
|
||||||
|
const newMember = body.members[0];
|
||||||
|
await agent
|
||||||
|
.put(`/members/${newMember.id}/`)
|
||||||
|
.body({members: [memberChanged]})
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 0))
|
||||||
|
})
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
'content-version': anyContentVersion,
|
||||||
|
etag: anyEtag
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check member events
|
||||||
|
await assertMemberEvents({
|
||||||
|
eventType: 'MemberEmailChangeEvent',
|
||||||
|
memberId: newMember.id,
|
||||||
|
asserts: [
|
||||||
|
{
|
||||||
|
from_email: 'jon.snow@test.com',
|
||||||
|
to_email: 'aegon.targaryen@test.com'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check activity feed
|
||||||
|
const {body: eventsBody} = await agent
|
||||||
|
.get(`/members/events?filter=data.member_id:'${newMember.id}'`)
|
||||||
|
.body({members: [memberChanged]})
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
'content-version': anyContentVersion,
|
||||||
|
etag: anyEtag
|
||||||
|
});
|
||||||
|
|
||||||
|
const events = eventsBody.events;
|
||||||
|
|
||||||
|
matchArrayWithoutOrder(events, [
|
||||||
|
{
|
||||||
|
type: 'email_change_event',
|
||||||
|
data: {
|
||||||
|
from_email: 'jon.snow@test.com',
|
||||||
|
to_email: 'aegon.targaryen@test.com'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'signup_event'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
describe('email_disabled', function () {
|
describe('email_disabled', function () {
|
||||||
const testMemberId = '6543c13c13575e086a06b222';
|
const testMemberId = '6543c13c13575e086a06b222';
|
||||||
const suppressedEmail = 'suppressed@email.com';
|
const suppressedEmail = 'suppressed@email.com';
|
||||||
|
|
|
@ -122,7 +122,8 @@ module.exports = function MembersAPI({
|
||||||
EmailSpamComplaintEvent,
|
EmailSpamComplaintEvent,
|
||||||
Comment,
|
Comment,
|
||||||
labsService,
|
labsService,
|
||||||
memberAttributionService
|
memberAttributionService,
|
||||||
|
MemberEmailChangeEvent
|
||||||
});
|
});
|
||||||
|
|
||||||
const memberBREADService = new MemberBREADService({
|
const memberBREADService = new MemberBREADService({
|
||||||
|
|
|
@ -31,7 +31,8 @@ module.exports = class EventRepository {
|
||||||
EmailSpamComplaintEvent,
|
EmailSpamComplaintEvent,
|
||||||
Comment,
|
Comment,
|
||||||
labsService,
|
labsService,
|
||||||
memberAttributionService
|
memberAttributionService,
|
||||||
|
MemberEmailChangeEvent
|
||||||
}) {
|
}) {
|
||||||
this._DonationPaymentEvent = DonationPaymentEvent;
|
this._DonationPaymentEvent = DonationPaymentEvent;
|
||||||
this._MemberSubscribeEvent = MemberSubscribeEvent;
|
this._MemberSubscribeEvent = MemberSubscribeEvent;
|
||||||
|
@ -48,6 +49,7 @@ module.exports = class EventRepository {
|
||||||
this._MemberFeedback = MemberFeedback;
|
this._MemberFeedback = MemberFeedback;
|
||||||
this._EmailSpamComplaintEvent = EmailSpamComplaintEvent;
|
this._EmailSpamComplaintEvent = EmailSpamComplaintEvent;
|
||||||
this._memberAttributionService = memberAttributionService;
|
this._memberAttributionService = memberAttributionService;
|
||||||
|
this._MemberEmailChangeEvent = MemberEmailChangeEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEventTimeline(options = {}) {
|
async getEventTimeline(options = {}) {
|
||||||
|
@ -76,7 +78,8 @@ module.exports = class EventRepository {
|
||||||
pageActions.push(
|
pageActions.push(
|
||||||
{type: 'newsletter_event', action: 'getNewsletterSubscriptionEvents'},
|
{type: 'newsletter_event', action: 'getNewsletterSubscriptionEvents'},
|
||||||
{type: 'login_event', action: 'getLoginEvents'},
|
{type: 'login_event', action: 'getLoginEvents'},
|
||||||
{type: 'payment_event', action: 'getPaymentEvents'}
|
{type: 'payment_event', action: 'getPaymentEvents'},
|
||||||
|
{type: 'email_change_event', action: 'getEmailChangeEvent'}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -764,6 +767,38 @@ module.exports = class EventRepository {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getEmailChangeEvent(options = {}, filter) {
|
||||||
|
options = {
|
||||||
|
...options,
|
||||||
|
withRelated: ['member'],
|
||||||
|
filter: 'custom:true',
|
||||||
|
mongoTransformer: chainTransformers(
|
||||||
|
// First set the filter manually
|
||||||
|
replaceCustomFilterTransformer(filter),
|
||||||
|
|
||||||
|
// Map the used keys in that filter
|
||||||
|
...mapKeys({
|
||||||
|
'data.created_at': 'created_at',
|
||||||
|
'data.member_id': 'member_id'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
const {data: models, meta} = await this._MemberEmailChangeEvent.findPage(options);
|
||||||
|
|
||||||
|
const data = models.map((model) => {
|
||||||
|
return {
|
||||||
|
type: 'email_change_event',
|
||||||
|
data: model.toJSON(options)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
meta
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split the filter in two parts:
|
* Split the filter in two parts:
|
||||||
* - One with 'type' that will be applied to all the pages
|
* - One with 'type' that will be applied to all the pages
|
||||||
|
|
Loading…
Add table
Reference in a new issue