1
0
Fork 0
mirror of https://github.com/TryGhost/Ghost-Admin.git synced 2023-12-14 02:33:04 +01:00

Added open-rate column and ordering to the members list (#1790)

closes https://github.com/TryGhost/Ghost/issues/12421

- added `emailOpenRate` property to member model
- added open-rate column to the members list
  - hidden when email analytics is disabled
- added `{{feature "flag"}}` helper so feature flags can be checked in templates without injecting the feature service into the backing class
- added `order` query param to the members controller/route and wired it into the data fetching routine
- added order dropdown to the filter bar with "Newest" (default) and "Open rate" as the two options
  - whole dropdown is hidden if email analytics is disabled

Co-authored-by: Kevin Ansfield <kevin@lookingsideways.co.uk>
This commit is contained in:
Sanne de Vries 2020-12-08 20:23:57 +01:00 committed by GitHub
parent 1372c0ddbc
commit 3d3173ca90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 108 additions and 16 deletions

View file

@ -53,4 +53,20 @@
>
{{#if paidParam.name}}{{paidParam.name}}{{else}}<span class="red">Unknown paid status</span>{{/if}}
</PowerSelect>
</div>
<div class="gh-contenfilter-menu gh-contentfilter-sort" data-test-select="members-order">
<PowerSelect
@selected={{@selectedOrder}}
@options={{@availableOrders}}
@searchEnabled={{false}}
@onChange={{@onOrderChange}}
@triggerComponent="gh-power-select/trigger"
@triggerClass="gh-contentfilter-menu-trigger"
@dropdownClass="gh-contentfilter-menu-dropdown"
@matchTriggerWidth={{false}}
as |order|
>
{{#if order.name}}{{order.name}}{{else}}<span class="red">Unknown</span>{{/if}}
</PowerSelect>
</div>

View file

@ -4,8 +4,9 @@
<div class="gh-list-loading-title"></div>
<div class="gh-list-loading-detail"></div>
</div>
<div class="gh-list-data gh-members-list-geolocation gh-list-cellwidth-20"></div>
<div class="gh-list-data gh-members-list-subscribed-at gh-list-cellwidth-20"></div>
<div class="gh-list-data gh-members-list-open-rate gh-list-cellwidth-10"></div>
<div class="gh-list-data gh-members-list-geolocation gh-list-cellwidth-10"></div>
<div class="gh-list-data gh-members-list-subscribed-at gh-list-cellwidth-10"></div>
<div class="gh-list-data gh-members-list-chevron gh-list-cellwidth-chevron"></div>
{{else}}
<LinkTo @route="member" @model={{@member}} title="Member details" class="gh-list-data gh-members-list-basic">
@ -20,7 +21,15 @@
</div>
</LinkTo>
<LinkTo @route="member" @model={{@member}} title="Member details" class="gh-list-data gh-members-list-geolocation gh-list-cellwidth-20 middarkgrey f8 {{if (not @member.name) "gh-members-geolocation-noname"}}">
{{#if (feature "emailAnalytics")}}
<LinkTo @route="member" @model={{@member}} title="Member details" class="gh-list-data gh-members-list-open-rate middarkgrey f8">
{{#if (not (is-empty @member.emailOpenRate))}}
<span>{{@member.emailOpenRate}}%</span>
{{/if}}
</LinkTo>
{{/if}}
<LinkTo @route="member" @model={{@member}} title="Member details" class="gh-list-data gh-members-list-geolocation middarkgrey f8 {{if (not @member.name) "gh-members-geolocation-noname"}}">
{{#if (and @member.geolocation @member.geolocation.country)}}
{{#if (and (eq @member.geolocation.country_code "US") @member.geolocation.region)}}
{{@member.geolocation.region}}, US
@ -32,7 +41,7 @@
{{/if}}
</LinkTo>
<LinkTo @route="member" @model={{@member}} title="Member details" class="gh-list-data gh-members-list-subscribed-at gh-list-cellwidth-20 middarkgrey f8 {{if (not @member.name) "gh-members-subscribed-noname"}}">
<LinkTo @route="member" @model={{@member}} title="Member details" class="gh-list-data gh-members-list-subscribed-at middarkgrey f8 {{if (not @member.name) "gh-members-subscribed-noname"}}">
{{#if @member.createdAtUTC}}
{{moment-format @member.createdAtUTC "D MMM YYYY"}}
<span class="midlightgrey">({{moment-from-now @member.createdAtUTC}})</span>

View file

@ -32,7 +32,8 @@ export default class MembersController extends Controller {
queryParams = [
'label',
{paidParam: 'paid'},
{searchParam: 'search'}
{searchParam: 'search'},
{orderParam: 'order'}
];
@tracked members = A([]);
@ -40,6 +41,7 @@ export default class MembersController extends Controller {
@tracked searchParam = '';
@tracked paidParam = null;
@tracked label = null;
@tracked orderParam = null;
@tracked modalLabel = null;
@tracked showLabelModal = false;
@tracked showDeleteMembersModal = false;
@ -83,6 +85,27 @@ export default class MembersController extends Controller {
return !this.searchParam && !this.paidParam && !this.label;
}
get availableOrders() {
// don't return anything if email analytics is disabled because
// we don't want to show an order dropdown with only a single option
if (this.feature.get('emailAnalytics')) {
return [{
name: 'Newest',
value: null
}, {
name: 'Open rate',
value: 'email_open_rate'
}];
}
return [];
}
get selectedOrder() {
return this.availableOrders.find(order => order.value === this.orderParam);
}
get availableLabels() {
let labels = this._availableLabels
.filter(label => !label.isNew)
@ -124,6 +147,11 @@ export default class MembersController extends Controller {
this.membersStats.fetch();
}
@action
changeOrder(order) {
this.orderParam = order.value;
}
@action
search(e) {
this.searchTask.perform(e.target.value);
@ -221,7 +249,7 @@ export default class MembersController extends Controller {
@task({restartable: true})
*fetchMembersTask(params) {
// params is undefined when called as a "refresh" of the model
let {label, paidParam, searchParam} = typeof params === 'undefined' ? this : params;
let {label, paidParam, searchParam, orderParam} = typeof params === 'undefined' ? this : params;
if (!searchParam) {
this.resetSearch();
@ -234,10 +262,12 @@ export default class MembersController extends Controller {
let forceReload = !params
|| label !== this._lastLabel
|| paidParam !== this._lastPaidParam
|| searchParam !== this._lastSearchParam;
|| searchParam !== this._lastSearchParam
|| orderParam !== this._orderParam;
this._lastLabel = label;
this._lastPaidParam = paidParam;
this._lastSearchParam = searchParam;
this._lastOrderParam = orderParam;
// unless we have a forced reload, do not re-fetch the members list unless it's more than a minute old
// keeps navigation between list->details->list snappy
@ -251,11 +281,12 @@ export default class MembersController extends Controller {
const labelFilter = label ? `label:'${label}'+` : '';
const paidQuery = paidParam ? {paid: paidParam} : {};
const searchQuery = searchParam ? {search: searchParam} : {};
const order = orderParam ? `${orderParam} desc` : `created_at desc`;
query = Object.assign({
order,
limit: range.length,
page: range.page,
order: 'created_at desc',
filter: `${labelFilter}created_at:<='${moment.utc(this._startDate).format('YYYY-MM-DD HH:mm:ss')}'`
}, paidQuery, searchQuery, query);

10
app/helpers/feature.js Normal file
View file

@ -0,0 +1,10 @@
import Helper from '@ember/component/helper';
import {inject as service} from '@ember/service';
export default class EnableDeveloperExperimentsHelper extends Helper {
@service feature;
compute([featureFlag]) {
return this.feature.get(featureFlag);
}
}

View file

@ -18,7 +18,8 @@ export const DEFAULT_QUERY_PARAMS = {
'members.index': {
label: null,
paid: null,
search: ''
search: '',
order: null
}
};

View file

@ -15,6 +15,7 @@ export default Model.extend(ValidationEngine, {
labels: hasMany('label', {embedded: 'always', async: false}),
comped: attr('boolean', {defaultValue: false}),
geolocation: attr('json-string'),
emailOpenRate: attr('number'),
ghostPaths: service(),
ajax: service(),

View file

@ -7,7 +7,8 @@ export default class MembersRoute extends AuthenticatedRoute {
queryParams = {
label: {refreshModel: true},
searchParam: {refreshModel: true, replace: true},
paidParam: {refreshModel: true}
paidParam: {refreshModel: true},
orderParam: {refreshModel: true}
};
// redirect to posts screen if:

View file

@ -121,7 +121,13 @@ p.gh-members-list-email {
margin: -2px 0 -1px;
}
.gh-members-list-open-rate,
.gh-members-list-geolocation {
width: 120px;
}
.gh-members-list-subscribed-at {
width: 240px;
margin-right: -8px;
padding-right: 0;
}
@ -324,15 +330,26 @@ p.gh-members-list-email {
padding-bottom: 16px;
}
.gh-members-list-open-rate {
display: inline-block;
width: auto;
margin-top: -16px;
padding: 0 0 0 64px;
}
.gh-members-list-open-rate::after {
content: "open rate •";
}
.gh-members-list-geolocation {
display: inline-block;
padding: 0 0 0 64px;
margin-top: -16px;
width: auto;
margin-top: -16px;
padding: 0 0 0 4px;
}
.gh-members-list-geolocation::after {
content: "";
content: "";
}
.gh-members-geolocation-noname {

View file

@ -11,6 +11,9 @@
@selectedPaidParam={{this.selectedPaidParam}}
@availablePaidParams={{this.paidParams}}
@onPaidParamChange={{this.changePaidParam}}
@availableOrders={{this.availableOrders}}
@selectedOrder={{this.selectedOrder}}
@onOrderChange={{this.changeOrder}}
/>
<div class="relative gh-members-header-search">
{{svg-jar "search" class="gh-input-search-icon"}}
@ -62,7 +65,7 @@
<section class="view-container">
{{#if (and this.members this.showingAll)}}
<section>
<GhMembersChart nightShift={{this.feature.nightShift}} />
<GhMembersChart nightShift={{feature "nightShift"}} />
</section>
{{/if}}
@ -71,8 +74,11 @@
{{#if this.members}}
<li class="gh-list-row header relative">
<div class="gh-list-header" data-test-list-header>{{this.listHeader}}</div>
<div class="gh-list-header gh-members-list-geolocation gh-list-cellwidth-20 nowrap">Location</div>
<div class="gh-list-header gh-members-list-subscribed-at gh-list-cellwidth-20 nowrap">Created</div>
{{#if (feature "emailAnalytics")}}
<div class="gh-list-header gh-members-list-open-rate nowrap">Open rate</div>
{{/if}}
<div class="gh-list-header gh-members-list-geolocation nowrap">Location</div>
<div class="gh-list-header gh-members-list-subscribed-at nowrap">Created</div>
<div class="gh-list-header gh-members-list-chevron gh-list-cellwidth-chevron"></div>
</li>
<VerticalCollection @items={{this.members}} @key="id" @containerSelector=".gh-main" @estimateHeight={{69}} @staticHeight={{true}} @bufferSize={{20}} as |member|>