mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 22:43:30 +03:00
✨ 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:
parent
1dd2b33499
commit
a2a98b575f
@ -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}}%
|
||||
{{/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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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}}
|
||||
|
||||
|
@ -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}}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user