From 0bb7538cd128231cdbe335c9fde5e2b0f6466949 Mon Sep 17 00:00:00 2001 From: Simon Backx Date: Mon, 17 Oct 2022 15:44:18 +0200 Subject: [PATCH] Added feedback events to activity feed (#15639) fixes https://github.com/TryGhost/Team/issues/2051 fixes https://github.com/TryGhost/Team/issues/2052 --- .../members-activity/event-type-filter.js | 6 ++ ghost/admin/app/helpers/parse-member-event.js | 25 +++++--- ...nt-less-like-this--feature-attribution.svg | 3 + .../assets/icons/event-less-like-this.svg | 3 + ...nt-more-like-this--feature-attribution.svg | 3 + .../assets/icons/event-more-like-this.svg | 3 + .../output/mappers/activity-feed-events.js | 62 ++++++++++++++----- .../core/core/server/services/members/api.js | 3 +- ghost/members-api/lib/MembersAPI.js | 4 +- ghost/members-api/lib/repositories/event.js | 35 +++++++++++ 10 files changed, 122 insertions(+), 25 deletions(-) create mode 100644 ghost/admin/public/assets/icons/event-less-like-this--feature-attribution.svg create mode 100644 ghost/admin/public/assets/icons/event-less-like-this.svg create mode 100644 ghost/admin/public/assets/icons/event-more-like-this--feature-attribution.svg create mode 100644 ghost/admin/public/assets/icons/event-more-like-this.svg diff --git a/ghost/admin/app/components/members-activity/event-type-filter.js b/ghost/admin/app/components/members-activity/event-type-filter.js index 3ef8416553..920e53dc8e 100644 --- a/ghost/admin/app/components/members-activity/event-type-filter.js +++ b/ghost/admin/app/components/members-activity/event-type-filter.js @@ -22,6 +22,12 @@ export default class MembersActivityEventTypeFilter extends Component { if (this.settings.commentsEnabled !== 'off') { extended.push({event: 'comment_event', icon: 'event-comment', name: 'Comments'}); } + if (this.settings.emailTrackClicks) { + extended.push({event: 'click_event', icon: 'event-click', name: 'Clicked link'}); + } + if (this.feature.audienceFeedback) { + extended.push({event: 'feedback_event', icon: 'event-more-like-this', name: 'Feedback'}); + } if (this.args.hiddenEvents?.length) { return extended.filter(t => !this.args.hiddenEvents.includes(t.event)); diff --git a/ghost/admin/app/helpers/parse-member-event.js b/ghost/admin/app/helpers/parse-member-event.js index 3f9ceef7f4..164230a8c0 100644 --- a/ghost/admin/app/helpers/parse-member-event.js +++ b/ghost/admin/app/helpers/parse-member-event.js @@ -88,6 +88,14 @@ export default class ParseMemberEventHelper extends Helper { icon = 'click'; } + if (event.type === 'feedback_event') { + if (event.data.score === 1) { + icon = 'more-like-this'; + } else { + icon = 'less-like-this'; + } + } + return 'event-' + icon + (this.feature.get('memberAttribution') ? '--feature-attribution' : ''); } @@ -159,6 +167,13 @@ export default class ParseMemberEventHelper extends Helper { if (event.type === 'click_event') { return 'clicked link in email'; } + + if (event.type === 'feedback_event') { + if (event.data.score === 1) { + return 'more like this'; + } + return 'less like this'; + } } /** @@ -202,7 +217,7 @@ export default class ParseMemberEventHelper extends Helper { } } - if (event.type === 'click_event') { + if (event.type === 'click_event' || event.type === 'feedback_event') { if (event.data.post) { return event.data.post.title; } @@ -241,13 +256,7 @@ export default class ParseMemberEventHelper extends Helper { * Make the object clickable */ getURL(event) { - if (event.type === 'comment_event') { - if (event.data.post) { - return event.data.post.url; - } - } - - if (event.type === 'click_event') { + if (event.type === 'comment_event' || event.type === 'click_event' || event.type === 'feedback_event') { if (event.data.post) { return event.data.post.url; } diff --git a/ghost/admin/public/assets/icons/event-less-like-this--feature-attribution.svg b/ghost/admin/public/assets/icons/event-less-like-this--feature-attribution.svg new file mode 100644 index 0000000000..3362f17b71 --- /dev/null +++ b/ghost/admin/public/assets/icons/event-less-like-this--feature-attribution.svg @@ -0,0 +1,3 @@ + + + diff --git a/ghost/admin/public/assets/icons/event-less-like-this.svg b/ghost/admin/public/assets/icons/event-less-like-this.svg new file mode 100644 index 0000000000..3362f17b71 --- /dev/null +++ b/ghost/admin/public/assets/icons/event-less-like-this.svg @@ -0,0 +1,3 @@ + + + diff --git a/ghost/admin/public/assets/icons/event-more-like-this--feature-attribution.svg b/ghost/admin/public/assets/icons/event-more-like-this--feature-attribution.svg new file mode 100644 index 0000000000..411c20833a --- /dev/null +++ b/ghost/admin/public/assets/icons/event-more-like-this--feature-attribution.svg @@ -0,0 +1,3 @@ + + + diff --git a/ghost/admin/public/assets/icons/event-more-like-this.svg b/ghost/admin/public/assets/icons/event-more-like-this.svg new file mode 100644 index 0000000000..411c20833a --- /dev/null +++ b/ghost/admin/public/assets/icons/event-more-like-this.svg @@ -0,0 +1,3 @@ + + + diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js index f659569f7f..f2e1ff24fc 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js @@ -2,6 +2,21 @@ const mapComment = require('./comments'); const url = require('../utils/url'); const _ = require('lodash'); +const memberFields = [ + 'id', + 'uuid', + 'name', + 'email', + 'avatar_image' +]; + +const postFields = [ + 'id', + 'uuid', + 'title', + 'url' +]; + const commentEventMapper = (json, frame) => { return { ...json, @@ -10,26 +25,11 @@ const commentEventMapper = (json, frame) => { }; const clickEventMapper = (json, frame) => { - const memberFields = [ - 'id', - 'uuid', - 'name', - 'email', - 'avatar_image' - ]; - const linkFields = [ 'from', 'to' ]; - const postFields = [ - 'id', - 'uuid', - 'title', - 'url' - ]; - const data = json.data; const response = {}; @@ -59,6 +59,35 @@ const clickEventMapper = (json, frame) => { }; }; +const feedbackEventMapper = (json, frame) => { + const feedbackFields = [ + 'id', + 'score', + 'created_at' + ]; + + const data = json.data; + const response = _.pick(data, feedbackFields); + + if (data.post) { + url.forPost(data.post.id, data.post, frame); + response.post = _.pick(data.post, postFields); + } else { + response.post = null; + } + + if (data.member) { + response.member = _.pick(data.member, memberFields); + } else { + response.member = null; + } + + return { + ...json, + data: response + }; +}; + function serializeAttribution(attribution) { if (!attribution) { return attribution; @@ -82,6 +111,9 @@ const activityFeedMapper = (event, frame) => { if (event.type === 'click_event') { return clickEventMapper(event, frame); } + if (event.type === 'feedback_event') { + return feedbackEventMapper(event, frame); + } if (event.data?.attribution) { event.data.attribution = serializeAttribution(event.data.attribution); } diff --git a/ghost/core/core/server/services/members/api.js b/ghost/core/core/server/services/members/api.js index 944999141b..f3e1d01a9d 100644 --- a/ghost/core/core/server/services/members/api.js +++ b/ghost/core/core/server/services/members/api.js @@ -194,7 +194,8 @@ function createApiInstance(config) { StripePrice: models.StripePrice, Product: models.Product, Settings: models.Settings, - Comment: models.Comment + Comment: models.Comment, + MemberFeedback: models.MemberFeedback }, stripeAPIService: stripeService.api, offersAPI: offersService.api, diff --git a/ghost/members-api/lib/MembersAPI.js b/ghost/members-api/lib/MembersAPI.js index 2210cfc17d..b4cf61d599 100644 --- a/ghost/members-api/lib/MembersAPI.js +++ b/ghost/members-api/lib/MembersAPI.js @@ -58,7 +58,8 @@ module.exports = function MembersAPI({ StripePrice, Product, Settings, - Comment + Comment, + MemberFeedback }, stripeAPIService, offersAPI, @@ -112,6 +113,7 @@ module.exports = function MembersAPI({ MemberCreatedEvent, SubscriptionCreatedEvent, MemberLinkClickEvent, + MemberFeedback, Comment, labsService, memberAttributionService diff --git a/ghost/members-api/lib/repositories/event.js b/ghost/members-api/lib/repositories/event.js index 3752f25d47..a1d5923578 100644 --- a/ghost/members-api/lib/repositories/event.js +++ b/ghost/members-api/lib/repositories/event.js @@ -12,6 +12,7 @@ module.exports = class EventRepository { SubscriptionCreatedEvent, MemberPaidSubscriptionEvent, MemberLinkClickEvent, + MemberFeedback, Comment, labsService, memberAttributionService @@ -27,6 +28,7 @@ module.exports = class EventRepository { this._MemberCreatedEvent = MemberCreatedEvent; this._SubscriptionCreatedEvent = SubscriptionCreatedEvent; this._MemberLinkClickEvent = MemberLinkClickEvent; + this._MemberFeedback = MemberFeedback; this._memberAttributionService = memberAttributionService; } @@ -56,6 +58,10 @@ module.exports = class EventRepository { pageActions.push({type: 'email_failed_event', action: 'getEmailFailedEvents'}); } + if (this._labsService.isSet('audienceFeedback')) { + pageActions.push({type: 'feedback_event', action: 'getFeedbackEvents'}); + } + let filters = this.getNQLSubset(options.filter); //Filter events to query @@ -360,6 +366,35 @@ module.exports = class EventRepository { }; } + async getFeedbackEvents(options = {}, filters = {}) { + options = { + ...options, + withRelated: ['member', 'post'], + filter: [] + }; + if (filters['data.created_at']) { + options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'created_at:')); + } + if (filters['data.member_id']) { + options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:')); + } + options.filter = options.filter.join('+'); + + const {data: models, meta} = await this._MemberFeedback.findPage(options); + + const data = models.map((model) => { + return { + type: 'feedback_event', + data: model.toJSON(options) + }; + }); + + return { + data, + meta + }; + } + async getEmailDeliveredEvents(options = {}, filters = {}) { options = { ...options,