Added email open rate to posts list in admin (#1772)

no issue

- added "Sends" and "Opens" columns to the posts list to see newsletter performance at a glance
- "Sends" will show the type of members sent to (free, paid, all) as a tooltip
- "Opens" shows open rate by default and total opens on hover
This commit is contained in:
Sanne de Vries 2020-11-26 18:19:05 +01:00 committed by GitHub
parent 1dd2b33499
commit a2a98b575f
7 changed files with 116 additions and 49 deletions

View File

@ -1,4 +1,8 @@
<li class="gh-list-row gh-posts-list-item" ...attributes>
<li class="gh-list-row gh-posts-list-item"
{{on "mouseover" this.mouseOver}}
{{on "mouseleave" this.mouseLeave}}
...attributes
>
<LinkTo @route="editor.edit" @models={{array @post.displayName @post.id}} class="permalink gh-list-data gh-post-list-featured" @title="Edit this post">
{{#if @post.isFeatured}}
<span data-tooltip="Featured" class="dib pl1 pr1 nr1 nl1">{{svg-jar "star-filled" class="fill-blue w3 h3"}}</span>
@ -17,21 +21,48 @@
in <span class="midgrey-l2 fw5">{{@post.primaryTag.name}}</span>
{{/if}}
{{#if (eq @post.email.status "submitted")}}
• Sent to <span class="midgrey-l2 fw5">{{gh-pluralize @post.email.emailCount "member"}}</span>
{{/if}}
{{gh-format-post-time @post.updatedAtUTC draft=true}}
{{#if @post.isScheduled}}
<span class="gh-schedule-time">Will be published {{this.scheduledText}}</span>
{{/if}}
</span>
</p>
</LinkTo>
{{#if (eq @post.displayName "post")}}
<LinkTo @route="editor.edit" @models={{array @post.displayName @post.id}} class="permalink gh-list-data gh-post-list-recipients" @title="Edit this post">
<div class="flex fw4">
{{#if (or @post.email @post.willEmail)}}
{{#if (eq @post.email.status "submitted")}}
<span class="flex" data-tooltip="{{capitalize @post.email.recipientFilter}} members">
<span class="f7 darkgrey fw6 gh-content-email-stats">{{@post.email.emailCount}}</span>
<span class="midgrey-l2 fw4 gh-content-email-stats-mobile">{{gh-pluralize @post.email.emailCount "send"}}</span>
</span>
{{/if}}
{{/if}}
</div>
</LinkTo>
{{/if}}
{{#if (eq @post.displayName "post")}}
<LinkTo @route="editor.edit" @models={{array @post.displayName @post.id}} class="permalink gh-list-data gh-post-list-opens" @title="Edit this post">
{{#if (and @post.email.trackOpens (eq @post.email.status "submitted"))}}
<div class="flex">
<span class="f7 darkgrey fw6 gh-content-email-stats">
{{#if this.isHovered}}
{{@post.email.openedCount}}
{{else}}
{{@post.email.openRate}}%&nbsp;
{{/if}}
</span>
<span class="midgrey-l2 fw4 gh-content-email-stats-mobile">{{@post.email.openRate}}% opens</span>
</div>
{{/if}}
</LinkTo>
{{/if}}
<LinkTo @route="editor.edit" @models={{array @post.displayName @post.id}} class="permalink gh-list-data gh-post-list-status" @title="Edit this post">
<div class="flex items-center">
{{#if @post.isScheduled}}
<span class="gh-content-status-draft gh-badge nowrap">
<span class="gh-content-status-draft gh-badge nowrap" title="Scheduled" data-tooltip="{{this.scheduledText}} to {{capitalize @post.emailRecipientFilter}} members">
Scheduled
</span>
{{/if}}
@ -47,30 +78,6 @@
Published
</span>
{{/if}}
{{#if this.session.user.isOwnerOrAdmin}}
{{#if (or @post.email (and @post.isScheduled this.sendEmailWhenPublished))}}
{{#if (eq @post.email.status "failed")}}
<span data-tooltip="Failed to send post by email" class="gh-content-status-emailed error">
{{svg-jar "send-email" class="stroke-red"}}
</span>
{{else}}
{{#if @post.isScheduled}}
<span title="To be send by email" data-tooltip="To be sent by email" class="gh-content-status-emailed scheduled">
{{svg-jar "send-email" class="stroke-green-d2"}}
</span>
{{else}}
<span title="Sent by email" data-tooltip="Sent by email" class="gh-content-status-emailed">
{{svg-jar "send-email" class="stroke-midgrey"}}
</span>
{{/if}}
{{/if}}
{{/if}}
{{/if}}
</div>
</LinkTo>
<LinkTo @route="editor.edit" @models={{array @post.displayName @post.id}} class="permalink gh-list-data gh-post-list-updated" @title="Edit this post">
<span class="nowrap">{{gh-format-post-time @post.updatedAtUTC draft=true}}</span>
</LinkTo>
</li>

View File

@ -1,11 +1,15 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {formatPostTime} from 'ghost-admin/helpers/gh-format-post-time';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class GhPostsListItemComponent extends Component {
@service session;
@service settings;
@tracked isHovered = false;
get authorNames() {
return this.args.post.authors.map(author => author.name || author.email).join(', ');
}
@ -19,10 +23,6 @@ export default class GhPostsListItemComponent extends Component {
let {post} = this.args;
let text = [];
if (post.emailRecipientFilter && post.emailRecipientFilter !== 'none') {
text.push(`and sent to ${post.emailRecipientFilter} members`);
}
let formattedTime = formatPostTime(
post.publishedAtUTC,
{timezone: this.settings.get('timezone'), scheduled: true}
@ -31,4 +31,14 @@ export default class GhPostsListItemComponent extends Component {
return text.join(' ');
}
@action
mouseOver() {
this.isHovered = true;
}
@action
mouseLeave() {
this.isHovered = false;
}
}

View File

@ -1,8 +1,8 @@
import Model, {attr, belongsTo} from '@ember-data/model';
import {computed} from '@ember/object';
import {equal} from '@ember/object/computed';
export default Model.extend({
emailCount: attr('number'),
error: attr('string'),
html: attr('string'),
plaintext: attr('string'),
@ -11,6 +11,14 @@ export default Model.extend({
subject: attr('string'),
submittedAtUTC: attr('moment-utc'),
uuid: attr('string'),
recipientFilter: attr('string'),
emailCount: attr('number', {defaultValue: 0}),
deliveredCount: attr('number', {defaultValue: 0}),
openedCount: attr('number', {defaultValue: 0}),
failedCount: attr('number', {defaultValue: 0}),
trackOpens: attr('boolean'),
createdAtUTC: attr('moment-utc'),
createdBy: attr('string'),
@ -22,6 +30,16 @@ export default Model.extend({
isSuccess: equal('status', 'submitted'),
isFailure: equal('status', 'failed'),
openRate: computed('emailCount', 'openedCount', function () {
let {emailCount, openedCount} = this;
if (emailCount === 0) {
return 0;
}
return Math.round(openedCount / emailCount * 100);
}),
retry() {
return this.store.adapterFor('email').retry(this);
}

View File

@ -149,6 +149,10 @@ export default Model.extend(Comparable, ValidationEngine, {
internalTags: filterBy('tags', 'isInternal', true),
isScheduled: equal('status', 'scheduled'),
willEmail: computed('emailRecipientFilter', function () {
return this.emailRecipientFilter !== 'none';
}),
previewUrl: computed('uuid', 'ghostPaths.url', 'config.blogUrl', function () {
let blogUrl = this.get('config.blogUrl');
let uuid = this.uuid;

View File

@ -160,12 +160,13 @@
padding-left: 10px;
}
.gh-posts-status-header {
width: 160px;
.gh-posts-sends-header,
.gh-posts-opens-header {
width: 120px;
}
.gh-posts-lastupdate-header {
width: 160px;
.gh-posts-status-header {
width: 140px;
}
.gh-post-list-title {
@ -179,7 +180,9 @@
}
.gh-post-list-updated,
.gh-post-list-author {
.gh-post-list-author,
.gh-post-list-recipients,
.gh-post-list-opens {
color: var(--middarkgrey);
font-size: 1.3rem;
}
@ -201,7 +204,7 @@
color: var(--middarkgrey);
}
.gh-schedule-time {
.gh-schedule-plan {
color: var(--green-d1);
}
@ -219,6 +222,10 @@
margin-right: 3px;
}
.gh-content-email-stats-mobile {
display: none;
}
.gh-content-status-draft,
.gh-content-status-published,
.gh-content-status-scheduled,
@ -340,9 +347,10 @@
padding: 20px 28px 4px;
}
.gh-post-list-status {
.gh-post-list-status,
.gh-post-list-recipients,
.gh-post-list-opens {
display: inline-block;
order: 3;
border: none;
padding: 0 4px 20px 28px;
font-size: 1.3rem;
@ -352,9 +360,21 @@
text-overflow: ellipsis;
}
.gh-post-list-status {
order: 3;
}
.gh-post-list-recipients {
order: 4;
}
.gh-post-list-opens {
order: 5;
}
.gh-post-list-updated {
display: inline-block;
order: 4;
order: 6;
border: none;
padding: 0 4px 20px;
font-size: 1.3rem;
@ -366,7 +386,7 @@
.gh-post-list-author {
display: inline-block;
order: 5;
order: 7;
border: none;
padding: 0 4px 20px 0;
font-size: 1.3rem;
@ -385,6 +405,14 @@
display: none;
}
.gh-content-email-stats {
display: none;
}
.gh-content-email-stats-mobile {
display: inherit;
}
.post-header {
justify-content: flex-end;
min-height: 120px;

View File

@ -33,7 +33,6 @@
<div class="gh-list-header no-padding">{{!--Favorite indicator column: no header--}}</div>
<div class="gh-list-header gh-posts-title-header">Title</div>
<div class="gh-list-header gh-posts-status-header">Status</div>
<div class="gh-list-header gh-posts-lastupdate-header">Last update</div>
</li>
{{/if}}

View File

@ -32,8 +32,9 @@
<li class="gh-list-row header">
<div class="gh-list-header no-padding">{{!--Favorite indicator column: no header--}}</div>
<div class="gh-list-header gh-posts-title-header">Title</div>
<div class="gh-list-header gh-posts-sends-header">Sends</div>
<div class="gh-list-header gh-posts-opens-header">Opens</div>
<div class="gh-list-header gh-posts-status-header">Status</div>
<div class="gh-list-header gh-posts-lastupdate-header">Last update</div>
</li>
{{/if}}