diff --git a/ghost/admin/app/components/dashboard/charts/recents.hbs b/ghost/admin/app/components/dashboard/charts/recents.hbs index cdee3b484b..6f1e1794ec 100644 --- a/ghost/admin/app/components/dashboard/charts/recents.hbs +++ b/ghost/admin/app/components/dashboard/charts/recents.hbs @@ -93,8 +93,18 @@ {{#let (parse-member-event event eventsFetcher.hasMultipleNewsletters) as |parsedEvent|}}
- - {{parsedEvent.subject}} + {{#if parsedEvent.member}} + + {{parsedEvent.subject}} + {{else}} + {{#if parsedEvent.email}} + + {{parsedEvent.subject}} + {{else}} + + {{parsedEvent.subject}} + {{/if}} + {{/if}}
{{svg-jar parsedEvent.icon }} diff --git a/ghost/admin/app/components/gh-member-avatar.js b/ghost/admin/app/components/gh-member-avatar.js index 4f1ec98979..c0744e5788 100644 --- a/ghost/admin/app/components/gh-member-avatar.js +++ b/ghost/admin/app/components/gh-member-avatar.js @@ -13,9 +13,9 @@ const stringToHslColor = function (str, saturation, lightness) { export default class GhMemberAvatarComponent extends Component { get memberName() { - let {member} = this.args; + let {member, name} = this.args; - return member?.name || member?.email || 'NM'; + return member?.name || member?.email || name || 'NM'; } get avatarImage() { diff --git a/ghost/admin/app/components/posts/post-activity-feed.hbs b/ghost/admin/app/components/posts/post-activity-feed.hbs index 2691002f8c..54834785cf 100644 --- a/ghost/admin/app/components/posts/post-activity-feed.hbs +++ b/ghost/admin/app/components/posts/post-activity-feed.hbs @@ -45,8 +45,18 @@ {{#let (parse-member-event event) as |parsedEvent|}}
- - {{parsedEvent.subject}} + {{#if parsedEvent.member}} + + {{parsedEvent.subject}} + {{else}} + {{#if parsedEvent.email}} + + {{parsedEvent.subject}} + {{else}} + + {{parsedEvent.subject}} + {{/if}} + {{/if}}
{{svg-jar parsedEvent.icon }} diff --git a/ghost/admin/app/helpers/parse-member-event.js b/ghost/admin/app/helpers/parse-member-event.js index 3bb6e3c167..ddf8369d0b 100644 --- a/ghost/admin/app/helpers/parse-member-event.js +++ b/ghost/admin/app/helpers/parse-member-event.js @@ -10,7 +10,7 @@ export default class ParseMemberEventHelper extends Helper { @service membersUtils; compute([event, hasMultipleNewsletters]) { - const subject = event.data.member.name || event.data.member.email; + const subject = event.data.member ? (event.data.member.name || event.data.member.email) : (event.data.name || event.data.email || ''); const icon = this.getIcon(event); const action = this.getAction(event, hasMultipleNewsletters); const info = this.getInfo(event); @@ -110,6 +110,10 @@ export default class ParseMemberEventHelper extends Helper { } } + if (event.type === 'donation_event') { + icon = 'subscriptions'; + } + return 'event-' + icon; } @@ -203,6 +207,10 @@ export default class ParseMemberEventHelper extends Helper { } return 'less like this'; } + + if (event.type === 'donation_event') { + return `Made a one-time payment`; + } } /** @@ -222,7 +230,7 @@ export default class ParseMemberEventHelper extends Helper { * Clickable object, shown between action and info, or in a separate column in some views */ getObject(event) { - if (event.type === 'signup_event' || event.type === 'subscription_event') { + if (event.type === 'signup_event' || event.type === 'subscription_event' || event.type === 'donation_event') { if (event.data.attribution?.title) { return event.data.attribution.title; } @@ -278,6 +286,12 @@ export default class ParseMemberEventHelper extends Helper { return 'Free'; } + if (event.type === 'donation_event') { + const symbol = getSymbol(event.data.currency); + const formattedAmount = symbol + getNonDecimal(event.data.amount, event.data.currency); + return formattedAmount; + } + return; } @@ -304,7 +318,7 @@ export default class ParseMemberEventHelper extends Helper { } } - if (['signup_event', 'subscription_event'].includes(event.type)) { + if (['signup_event', 'subscription_event', 'donation_event'].includes(event.type)) { if (event.data.attribution && event.data.attribution.url) { return event.data.attribution.url; } diff --git a/ghost/core/core/server/services/members/api.js b/ghost/core/core/server/services/members/api.js index fbfdf26a44..69a8b98997 100644 --- a/ghost/core/core/server/services/members/api.js +++ b/ghost/core/core/server/services/members/api.js @@ -182,6 +182,7 @@ function createApiInstance(config) { } }, models: { + DonationPaymentEvent: models.DonationPaymentEvent, EmailRecipient: models.EmailRecipient, StripeCustomer: models.MemberStripeCustomer, StripeCustomerSubscription: models.StripeCustomerSubscription, diff --git a/ghost/members-api/lib/members-api.js b/ghost/members-api/lib/members-api.js index 854726fcb0..00715695db 100644 --- a/ghost/members-api/lib/members-api.js +++ b/ghost/members-api/lib/members-api.js @@ -37,6 +37,7 @@ module.exports = function MembersAPI({ getSubject }, models: { + DonationPaymentEvent, EmailRecipient, StripeCustomer, StripeCustomerSubscription, @@ -107,6 +108,7 @@ module.exports = function MembersAPI({ }); const eventRepository = new EventRepository({ + DonationPaymentEvent, EmailRecipient, MemberSubscribeEvent, MemberPaidSubscriptionEvent, diff --git a/ghost/members-api/lib/repositories/EventRepository.js b/ghost/members-api/lib/repositories/EventRepository.js index 0a407b5854..b86ff1312a 100644 --- a/ghost/members-api/lib/repositories/EventRepository.js +++ b/ghost/members-api/lib/repositories/EventRepository.js @@ -17,6 +17,7 @@ function replaceCustomFilterTransformer(filter) { module.exports = class EventRepository { constructor({ + DonationPaymentEvent, EmailRecipient, MemberSubscribeEvent, MemberPaymentEvent, @@ -32,6 +33,7 @@ module.exports = class EventRepository { labsService, memberAttributionService }) { + this._DonationPaymentEvent = DonationPaymentEvent; this._MemberSubscribeEvent = MemberSubscribeEvent; this._MemberPaidSubscriptionEvent = MemberPaidSubscriptionEvent; this._MemberPaymentEvent = MemberPaymentEvent; @@ -65,7 +67,8 @@ module.exports = class EventRepository { {type: 'click_event', action: 'getClickEvents'}, {type: 'aggregated_click_event', action: 'getAggregatedClickEvents'}, {type: 'signup_event', action: 'getSignupEvents'}, - {type: 'subscription_event', action: 'getSubscriptionEvents'} + {type: 'subscription_event', action: 'getSubscriptionEvents'}, + {type: 'donation_event', action: 'getDonationEvents'} ]; // Some events are not filterable by post_id @@ -352,6 +355,59 @@ module.exports = class EventRepository { }; } + async getDonationEvents(options = {}, filter) { + options = { + ...options, + withRelated: [ + 'member', + 'postAttribution', + 'userAttribution', + 'tagAttribution' + ], + filter: 'member_id:-null+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' + }), + + (f) => { + // Special one: when data.post_id is used, replace it with two filters: attribution_id:x+attribution_type:post + return expandFilters(f, [{ + key: 'data.post_id', + replacement: 'attribution_id', + expansion: {attribution_type: 'post'} + }]); + } + ) + }; + + const {data: models, meta} = await this._DonationPaymentEvent.findPage(options); + + const data = models.map((model) => { + const json = model.toJSON(options); + delete json.postAttribution?.mobiledoc; + delete json.postAttribution?.lexical; + delete json.postAttribution?.plaintext; + return { + type: 'donation_event', + data: { + ...json, + attribution: this._memberAttributionService.getEventAttribution(model) + } + }; + }); + + return { + data, + meta + }; + } + async getCommentEvents(options = {}, filter) { options = { ...options,