Updated the filter naming in Post Analytics (#15898)

closes TryGhost/Team#2329
- Replace 'Received' on 'Sent' in member's filter
- Moved links for feedback analytics from chart to table
This commit is contained in:
Elena Baidakova 2022-11-30 17:39:37 +04:00 committed by GitHub
parent 26d51687b1
commit 95c3a68c34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 214 additions and 47 deletions

View File

@ -1,7 +1,7 @@
import Component from '@glimmer/component';
import moment from 'moment-timezone';
import nql from '@tryghost/nql-lang';
import {AUDIENCE_FEEDBACK_FILTER, CREATED_AT_FILTER, EMAIL_CLICKED_FILTER, EMAIL_COUNT_FILTER, EMAIL_FILTER, EMAIL_OPENED_COUNT_FILTER, EMAIL_OPENED_FILTER, EMAIL_OPEN_RATE_FILTER, EMAIL_RECEIVED_FILTER, LABEL_FILTER, LAST_SEEN_FILTER, NAME_FILTER, NEXT_BILLING_DATE_FILTER, PLAN_INTERVAL_FILTER, SIGNUP_ATTRIBUTION_FILTER, STATUS_FILTER, SUBSCRIBED_FILTER, SUBSCRIPTION_ATTRIBUTION_FILTER, SUBSCRIPTION_START_DATE_FILTER, SUBSCRIPTION_STATUS_FILTER, TIER_FILTER} from './filters';
import {AUDIENCE_FEEDBACK_FILTER, CREATED_AT_FILTER, EMAIL_CLICKED_FILTER, EMAIL_COUNT_FILTER, EMAIL_FILTER, EMAIL_OPENED_COUNT_FILTER, EMAIL_OPENED_FILTER, EMAIL_OPEN_RATE_FILTER, EMAIL_RECEIVED_FILTER, EMAIL_SENT_FILTER, LABEL_FILTER, LAST_SEEN_FILTER, NAME_FILTER, NEXT_BILLING_DATE_FILTER, PLAN_INTERVAL_FILTER, SIGNUP_ATTRIBUTION_FILTER, STATUS_FILTER, SUBSCRIBED_FILTER, SUBSCRIPTION_ATTRIBUTION_FILTER, SUBSCRIPTION_START_DATE_FILTER, SUBSCRIPTION_STATUS_FILTER, TIER_FILTER} from './filters';
import {TrackedArray} from 'tracked-built-ins';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
@ -44,6 +44,7 @@ const FILTER_GROUPS = [
EMAIL_OPENED_COUNT_FILTER,
EMAIL_OPEN_RATE_FILTER,
EMAIL_RECEIVED_FILTER,
EMAIL_SENT_FILTER,
EMAIL_OPENED_FILTER,
EMAIL_CLICKED_FILTER,
AUDIENCE_FEEDBACK_FILTER
@ -143,13 +144,20 @@ export default class MembersFilter extends Component {
})
]);
get availableFilterProperties() {
get filterProperties() {
let availableFilters = FILTER_PROPERTIES;
const hasMultipleTiers = this.membersUtils.hasMultipleTiers;
// exclude any filters that are behind disabled feature flags
availableFilters = availableFilters.filter(prop => !prop.feature || this.feature[prop.feature]);
availableFilters = availableFilters.filter(prop => !prop.setting || this.settings[prop.setting]);
availableFilters = availableFilters.filter(prop => !prop.excludeForFeature || !this.feature[prop.excludeForFeature]);
return availableFilters;
}
get availableFilterProperties() {
let availableFilters = this.filterProperties;
const hasMultipleTiers = this.membersUtils.hasMultipleTiers;
// exclude tiers filter if site has only single tier
availableFilters = availableFilters
@ -224,7 +232,7 @@ export default class MembersFilter extends Component {
let query = '';
filters.forEach((filter) => {
const filterProperty = FILTER_PROPERTIES.find(prop => prop.name === filter.type);
const filterProperty = this.filterProperties.find(prop => prop.name === filter.type);
if (filterProperty.buildNqlFilter) {
query += `${filterProperty.buildNqlFilter(filter)}+`;
@ -283,7 +291,7 @@ export default class MembersFilter extends Component {
const parsedFilters = [];
// Check custom parsing
for (const filterProperties of FILTER_PROPERTIES) {
for (const filterProperties of this.filterProperties) {
if (filterProperties.parseNqlFilter) {
// This filter has a custom parsing function
const parsedFilter = filterProperties.parseNqlFilter(filter);
@ -305,7 +313,7 @@ export default class MembersFilter extends Component {
parsedFilters.push(...this.parseNqlFilter(filter.yg));
} else {
const filterKeys = Object.keys(filter);
const validKeys = FILTER_PROPERTIES.map(prop => prop.name);
const validKeys = this.filterProperties.map(prop => prop.name);
for (const key of filterKeys) {
if (validKeys.includes(key)) {
@ -339,7 +347,7 @@ export default class MembersFilter extends Component {
const key = keys[0];
const nqlValue = nqlFilter[key];
const filterProperty = FILTER_PROPERTIES.find(prop => prop.name === key);
const filterProperty = this.filterProperties.find(prop => prop.name === key);
let relation;
let value;
@ -415,8 +423,8 @@ export default class MembersFilter extends Component {
}
if (relation && value) {
const properties = FILTER_PROPERTIES.find(prop => key === prop.name);
if (FILTER_PROPERTIES.find(prop => key === prop.name)) {
const properties = this.filterProperties.find(prop => key === prop.name);
if (this.filterProperties.find(prop => key === prop.name)) {
return new Filter({
properties,
relation,
@ -473,7 +481,7 @@ export default class MembersFilter extends Component {
newType = newType.target.value;
}
const newProp = FILTER_PROPERTIES.find(prop => prop.name === newType);
const newProp = this.filterProperties.find(prop => prop.name === newType);
if (!newProp) {
// eslint-disable-next-line no-console
@ -534,7 +542,7 @@ export default class MembersFilter extends Component {
this.fetchFilterResourcesTask.perform();
}
@action
@action
applyFiltersPressed(dropdown) {
dropdown?.actions.close();
this.applyFilter();

View File

@ -2,9 +2,10 @@ import {MATCH_RELATION_OPTIONS} from './relation-options';
export const EMAIL_RECEIVED_FILTER = {
label: 'Received email',
name: 'emails.post_id',
valueType: 'string',
resource: 'email',
name: 'emails.post_id',
valueType: 'string',
resource: 'email',
excludeForFeature: 'suppressionList',
relationOptions: MATCH_RELATION_OPTIONS,
columnLabel: 'Received email',
getColumnValue: (member, filter) => {

View File

@ -0,0 +1,16 @@
import {MATCH_RELATION_OPTIONS} from './relation-options';
export const EMAIL_SENT_FILTER = {
label: 'Sent email',
name: 'emails.post_id',
valueType: 'string',
resource: 'email',
feature: 'suppressionList',
relationOptions: MATCH_RELATION_OPTIONS,
columnLabel: 'Sent email',
getColumnValue: (member, filter) => {
return {
text: filter.resource?.title ?? ''
};
}
};

View File

@ -19,4 +19,5 @@ export * from './email-opened-count';
export * from './email-open-rate';
export * from './email-clicked';
export * from './email-received';
export * from './email-sent';
export * from './audience-feedback';

View File

@ -50,10 +50,9 @@
<tabs.tabPanel>
<Posts::PostActivityFeed
@postId={{this.post.id}}
@post={{this.post}}
@eventType="sent"
@linkQuery={{hash filterParam=(concat "emails.post_id:" this.post.id) }}
@linkText="Received"
/>
</tabs.tabPanel>
@ -65,10 +64,9 @@
<tabs.tabPanel>
<Posts::PostActivityFeed
@postId={{this.post.id}}
@post={{this.post}}
@eventType="opened"
@linkQuery={{hash filterParam=(concat "opened_emails.post_id:" this.post.id) }}
@linkText="Opened"
/>
</tabs.tabPanel>
{{/if}}
@ -81,10 +79,9 @@
<tabs.tabPanel>
<Posts::PostActivityFeed
@postId={{this.post.id}}
@post={{this.post}}
@eventType="clicked"
@linkQuery={{hash filterParam=(concat "clicked_links.post_id:" this.post.id) }}
@linkText="Clicked"
/>
</tabs.tabPanel>
{{/if}}
@ -97,7 +94,7 @@
<tabs.tabPanel>
<Posts::PostActivityFeed
@postId={{this.post.id}}
@post={{this.post}}
@eventType="feedback"
@data={{this.feedbackChartData}}
/>
@ -112,7 +109,7 @@
</tabs.tab>
<tabs.tabPanel>
<Posts::PostActivityFeed @postId={{this.post.id}} @eventType="conversion" />
<Posts::PostActivityFeed @post={{this.post}} @eventType="conversion" />
</tabs.tabPanel>
{{/if}}
</Tabs::Tabs>

View File

@ -30,7 +30,7 @@
<span class="gh-feedback-events-tooltip-metric">{{this.tooltipData.label}}</span>
</div>
{{#if this.tooltipData.href}}
{{#if (and this.tooltipData.href (not (feature "suppressionList")))}}
<div class="gh-feedback-events-tooltip-footer">
<LinkTo
class="gh-post-activity-feed-pagination-link gh-post-activity-chart-link"

View File

@ -1,5 +1,5 @@
<div class="gh-post-activity-feed">
{{#let (activity-feed-fetcher filter=(members-event-filter post=@postId includeEvents=this.getEventTypes) pageSize=this.pageSize) as |eventsFetcher|}}
{{#let (activity-feed-fetcher filter=(members-event-filter post=@post.id includeEvents=this.getEventTypes) pageSize=this.pageSize) as |eventsFetcher|}}
{{#if eventsFetcher.isError}}
<div class="gh-dashboard-list-body">
<div class="gh-dashboard-list-error">
@ -84,29 +84,22 @@
{{/if}}
<div class="gh-post-activity-feed-footer">
{{#if @linkQuery}}
{{#if (feature "suppressionList")}}
<div class="gh-post-activity-feed-pagination-link-wrapper">
{{svg-jar "filter"}}
See members for
<LinkTo
class="gh-post-activity-feed-pagination-link"
@route="members"
@query={{@linkQuery}}
>
{{@linkText}}
</LinkTo>
</div>
{{else}}
<LinkTo
class="gh-post-activity-feed-pagination-link"
@route="members"
@query={{@linkQuery}}
>
{{svg-jar "filter"}}
See members
</LinkTo>
{{/if}}
{{#if (feature "suppressionList")}}
<Posts::PostActivityFeed::FooterLinks
@eventType={{this.eventType}}
@post={{@post}}
/>
{{/if}}
{{#if (and @linkQuery (not (feature "suppressionList")))}}
<LinkTo
class="gh-post-activity-feed-pagination-link"
@route="members"
@query={{@linkQuery}}
>
{{svg-jar "filter"}}
See members
</LinkTo>
{{/if}}
<div class="gh-post-activity-feed-pagination">

View File

@ -0,0 +1,44 @@
{{#if (eq @eventType "sent")}}
<Posts::PostActivityFeed::Link>
<LinkTo
class="gh-post-activity-feed-pagination-link"
@route="members"
@query={{hash filterParam=(concat "emails.post_id:" @post.id) }}
>
Sent
</LinkTo>
</Posts::PostActivityFeed::Link>
{{else if (eq @eventType "opened")}}
<Posts::PostActivityFeed::Link>
<LinkTo
class="gh-post-activity-feed-pagination-link"
@route="members"
@query={{hash filterParam=(concat "opened_emails.post_id:" @post.id) }}
>
Opened
</LinkTo>
</Posts::PostActivityFeed::Link>
{{else if (eq @eventType "clicked")}}
<Posts::PostActivityFeed::Link>
<LinkTo
class="gh-post-activity-feed-pagination-link"
@route="members"
@query={{hash filterParam=(concat "clicked_links.post_id:" @post.id) }}
>
Clicked
</LinkTo>
</Posts::PostActivityFeed::Link>
{{else if (eq @eventType "feedback")}}
<Posts::PostActivityFeed::Link>
{{#each this.feedbackLinks as |link|}}
<LinkTo
class="gh-post-activity-feed-pagination-link"
@route="members"
@query={{hash filterParam=link.filterParam}}
>
{{link.label}}
</LinkTo>
{{link.separator}}
{{/each}}
</Posts::PostActivityFeed::Link>
{{/if}}

View File

@ -0,0 +1,48 @@
import Component from '@glimmer/component';
export default class FooterLinks extends Component {
get feedbackLinks() {
const post = this.args.post;
const positiveLink = {filterParam: '(feedback.post_id:' + post.id + '+feedback.score:1)', label: 'More like this'};
const negativeLink = {filterParam: '(feedback.post_id:' + post.id + '+feedback.score:0)', label: 'Less like this'};
const data = [
{link: positiveLink, hidden: !post.count.positive_feedback},
{link: negativeLink, hidden: !post.count.negative_feedback}
];
const links = this.collectLinks(data);
return this.addSeparator(links, 'and');
}
collectLinks(list) {
const data = [];
list.forEach((item) => {
if (item.hidden) {
return;
}
data.push(item.link);
});
return data;
}
addSeparator(links, separator) {
const data = [];
links.forEach((item, index) => {
const link = {...item};
const isLastItem = links.length - 1 === index;
if (isLastItem) {
data.push(link);
return;
}
link.separator = separator;
data.push(link);
});
return data;
}
}

View File

@ -0,0 +1,5 @@
<div class="gh-post-activity-feed-pagination-link-wrapper">
{{svg-jar "filter"}}
See members for
{{yield}}
</div>

View File

@ -0,0 +1,54 @@
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {find, render} from '@ember/test-helpers';
import {hbs} from 'ember-cli-htmlbars';
import {setupRenderingTest} from 'ember-mocha';
describe('Integration: Component: posts/post-activity-feed/footer-links', function () {
setupRenderingTest();
it('renders just one link if negative feedback > 0', async function () {
this.set('post', {id: 'id', count: {positive_feedback: 0, negative_feedback: 1}});
await render(hbs`
<Posts::PostActivityFeed::FooterLinks
@eventType="feedback"
@post={{this.post}}
/>`);
const link = find('.gh-post-activity-feed-pagination-link-wrapper');
expect(link).to.contain.text('Less like this');
expect(link).not.to.contain.text('and');
expect(link).not.to.contain.text('More like this');
});
it('renders just one link if positive feedback > 0', async function () {
this.set('post', {id: 'id', count: {positive_feedback: 1, negative_feedback: 0}});
await render(hbs`
<Posts::PostActivityFeed::FooterLinks
@eventType="feedback"
@post={{this.post}}
/>`);
const link = find('.gh-post-activity-feed-pagination-link-wrapper');
expect(link).not.to.contain.text('Less like this');
expect(link).not.to.contain.text('and');
expect(link).to.contain.text('More like this');
});
it('renders positive and negative links with separator', async function () {
this.set('post', {id: 'id', count: {positive_feedback: 1, negative_feedback: 1}});
await render(hbs`
<Posts::PostActivityFeed::FooterLinks
@eventType="feedback"
@post={{this.post}}
/>`);
const link = find('.gh-post-activity-feed-pagination-link-wrapper');
expect(link).to.contain.text('Less like this');
expect(link).to.contain.text('and');
expect(link).to.contain.text('More like this');
});
});