mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 03:44:29 +03:00
Added dynamic columns to member list from filter UI
refs https://github.com/TryGhost/Team/issues/943 - adds new columns to member list table based on selected filters in UI - handles dynamic columns in members list with formatted output like for labels - works behind the filtering feature flag
This commit is contained in:
parent
948934da2d
commit
247f24394d
@ -53,7 +53,8 @@
|
||||
type="button"
|
||||
class="gh-btn gh-btn-text gh-btn-link green gh-btn-icon gh-add-filter"
|
||||
title="Delete filter"
|
||||
{{on "click" (fn this.deleteFilter filter.id)}}
|
||||
{{!-- {{on "click" (fn this.deleteFilter filter.id)}} --}}
|
||||
{{action "deleteFilter" filter.id}}
|
||||
>
|
||||
{{svg-jar "close"}} <span class="hidden">Delete filter</span>
|
||||
</button>
|
||||
|
@ -8,34 +8,35 @@ const FILTER_PROPERTIES = [
|
||||
// Basic
|
||||
{label: 'Name', name: 'name', group: 'Basic'},
|
||||
{label: 'Email', name: 'email', group: 'Basic'},
|
||||
{label: 'Location', name: 'location', group: 'Basic'},
|
||||
// {label: 'Location', name: 'location', group: 'Basic'},
|
||||
{label: 'Newsletter subscription status', name: 'subscribed', group: 'Basic'},
|
||||
{label: 'Label', name: 'label', group: 'Basic'},
|
||||
|
||||
// Member subscription
|
||||
{label: 'Member status', name: 'member-status', group: 'Subscription'},
|
||||
{label: 'Tier', name: 'tier', group: 'Subscription'},
|
||||
{label: 'Billing period', name: 'billing-period', group: 'Subscription'},
|
||||
{label: 'Member status', name: 'status', group: 'Subscription'},
|
||||
// {label: 'Tier', name: 'tier', group: 'Subscription'},
|
||||
// {label: 'Billing period', name: 'billing-period', group: 'Subscription'},
|
||||
|
||||
// Emails
|
||||
{label: 'Emails sent (all time)', name: 'x', group: 'Email'},
|
||||
{label: 'Emails opened (all time)', name: 'x', group: 'Email'},
|
||||
{label: 'Open rate (all time)', name: 'x', group: 'Email'},
|
||||
{label: 'Emails sent (30 days)', name: 'x', group: 'Email'},
|
||||
{label: 'Emails opened (30 days)', name: 'x', group: 'Email'},
|
||||
{label: 'Open rate (30 days)', name: 'x', group: 'Email'},
|
||||
{label: 'Emails sent (60 days)', name: 'x', group: 'Email'},
|
||||
{label: 'Emails opened (60 days)', name: 'x', group: 'Email'},
|
||||
{label: 'Open rate (60 days)', name: 'x', group: 'Email'},
|
||||
{label: 'Stripe subscription status', name: 'status', group: 'Email'}
|
||||
{label: 'Emails sent (all time)', name: 'email_count', group: 'Email'},
|
||||
{label: 'Emails opened (all time)', name: 'email_opened_count', group: 'Email'},
|
||||
{label: 'Open rate (all time)', name: 'email_open_rate', group: 'Email'},
|
||||
// {label: 'Emails sent (30 days)', name: 'x', group: 'Email'},
|
||||
// {label: 'Emails opened (30 days)', name: 'x', group: 'Email'},
|
||||
// {label: 'Open rate (30 days)', name: 'x', group: 'Email'},
|
||||
// {label: 'Emails sent (60 days)', name: 'x', group: 'Email'},
|
||||
// {label: 'Emails opened (60 days)', name: 'x', group: 'Email'},
|
||||
// {label: 'Open rate (60 days)', name: 'x', group: 'Email'},
|
||||
{label: 'Stripe subscription status', name: 'stripe-status', group: 'Email'}
|
||||
];
|
||||
|
||||
const FILTER_RELATIONS = [
|
||||
{label: 'is', name: 'is'},
|
||||
{label: 'is not', name: 'is-not'},
|
||||
{label: 'contains', name: 'contains'},
|
||||
{label: 'exists', name: 'exists'},
|
||||
{label: 'does not exist', name: 'does-not-exist'}
|
||||
{label: 'in', name: 'in'}
|
||||
// {label: 'contains', name: 'contains'},
|
||||
// {label: 'exists', name: 'exists'},
|
||||
// {label: 'does not exist', name: 'does-not-exist'}
|
||||
];
|
||||
|
||||
export default class GhMembersFilterLabsComponent extends Component {
|
||||
@ -71,7 +72,8 @@ export default class GhMembersFilterLabsComponent extends Component {
|
||||
let query = '';
|
||||
filters.forEach((filter) => {
|
||||
const relationStr = filter.relation === 'is-not' ? '-' : '';
|
||||
query += `${filter.type}:${relationStr}'${filter.value}',`;
|
||||
const filterValue = filter.value.includes(' ') ? `'${filter.value}'` : filter.value;
|
||||
query += `${filter.type}:${relationStr}${filterValue}+`;
|
||||
});
|
||||
return query.slice(0, -1);
|
||||
}
|
||||
@ -103,6 +105,6 @@ export default class GhMembersFilterLabsComponent extends Component {
|
||||
@action
|
||||
applyFilter() {
|
||||
const query = this.generateNqlFilter(this.filters);
|
||||
this.args.onApplyFilter(query);
|
||||
this.args.onApplyFilter(query, this.filters);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,45 @@
|
||||
{{#if (eq @filterColumn 'label')}}
|
||||
<LinkTo @route="member" @model={{@member}} title="Member details" class="gh-list-data middarkgrey f8">
|
||||
<span class="midlightgrey">{{labels}}</span>
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq @filterColumn 'status')}}
|
||||
<LinkTo @route="member" @model={{@member}} title="Member details" class="gh-list-data middarkgrey f8">
|
||||
{{#if (not (is-empty @member.status))}}
|
||||
<span class="gh-members-list-open-rate-mobile">{{capitalize @member.status}}</span>
|
||||
{{else}}
|
||||
<span class="midlightgrey">-</span>
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq @filterColumn 'email_count')}}
|
||||
<LinkTo @route="member" @model={{@member}} title="Member details" class="gh-list-data middarkgrey f8">
|
||||
{{#if (not (is-empty @member.emailCount))}}
|
||||
<span class="gh-members-list-open-rate-mobile">{{@member.emailCount}}</span>
|
||||
{{else}}
|
||||
<span class="midlightgrey">-</span>
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq @filterColumn 'email_opened_count')}}
|
||||
<LinkTo @route="member" @model={{@member}} title="Member details" class="gh-list-data middarkgrey f8">
|
||||
{{#if (not (is-empty @member.emailOpenedCount))}}
|
||||
<span class="gh-members-list-open-rate-mobile">{{@member.emailOpenedCount}}</span>
|
||||
{{else}}
|
||||
<span class="midlightgrey">-</span>
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq @filterColumn 'subscribed')}}
|
||||
<LinkTo @route="member" @model={{@member}} title="Member details" class="gh-list-data middarkgrey f8">
|
||||
{{#if (not (is-empty @member.subscribed))}}
|
||||
<span class="gh-members-list-open-rate-mobile">{{@member.subscribed}}</span>
|
||||
{{else}}
|
||||
<span class="midlightgrey">-</span>
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
@ -0,0 +1,12 @@
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
export default class GhMembersListItemColumnLabs extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
|
||||
get labels() {
|
||||
const labelData = this.args.member.get('labels') || [];
|
||||
return labelData.map(label => label.name).join(',');
|
||||
}
|
||||
}
|
71
ghost/admin/app/components/gh-members-list-item-labs.hbs
Normal file
71
ghost/admin/app/components/gh-members-list-item-labs.hbs
Normal file
@ -0,0 +1,71 @@
|
||||
<tr>
|
||||
{{#if @member.is_loading}}
|
||||
<div class="gh-list-data gh-members-list-basic gh-list-loadingcell">
|
||||
<div class="gh-list-loading-title"></div>
|
||||
<div class="gh-list-loading-detail"></div>
|
||||
</div>
|
||||
<div class="gh-list-data"></div>
|
||||
<div class="gh-list-data"></div>
|
||||
<div class="gh-list-data"></div>
|
||||
{{#each @filterColumns as |filterColumn|}}
|
||||
<div class="gh-list-data"></div>
|
||||
{{/each}}
|
||||
{{!-- <div class="gh-list-data"></div>
|
||||
<div class="gh-list-data"></div>
|
||||
<div class="gh-list-data"></div>
|
||||
<div class="gh-list-data"></div>
|
||||
<div class="gh-list-data"></div>
|
||||
<div class="gh-list-data"></div> --}}
|
||||
{{else}}
|
||||
<LinkTo @route="member" @model={{@member}} title="Member details" class="gh-list-data">
|
||||
<div class="flex items-center">
|
||||
<GhMemberAvatar @member={{@member}} @containerClass="w9 h9 mr3 flex-shrink-0" />
|
||||
<div class="w-80">
|
||||
<h3 class="ma0 pa0 gh-members-list-name {{if (not @member.name) "gh-members-name-noname"}}">{{or @member.name @member.email}}</h3>
|
||||
{{#if @member.name}}
|
||||
<p class="ma0 pa0 middarkgrey f8 gh-members-list-email">{{@member.email}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</LinkTo>
|
||||
|
||||
{{#if (feature "emailAnalytics")}}
|
||||
<LinkTo @route="member" @model={{@member}} title="Member details" class="gh-list-data middarkgrey f8 {{if (not @member.name) "gh-members-list-open-rate-noname"}}">
|
||||
{{#if (not (is-empty @member.emailOpenRate))}}
|
||||
<span class="gh-members-list-open-rate-mobile">{{@member.emailOpenRate}}%</span>
|
||||
{{else}}
|
||||
<span class="midlightgrey">N/A</span>
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
|
||||
<LinkTo @route="member" @model={{@member}} title="Member details" class="gh-list-data 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
|
||||
{{else}}
|
||||
{{#if @member.geolocation.country}}
|
||||
{{@member.geolocation.country}}
|
||||
{{else}}
|
||||
<span class="midlightgrey">Unknown</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<span class="midlightgrey">Unknown</span>
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
|
||||
<LinkTo @route="member" @model={{@member}} title="Member details" class="gh-list-data middarkgrey f8">
|
||||
{{#if @member.createdAtUTC}}
|
||||
<div>{{moment-format @member.createdAtUTC "D MMM YYYY"}}</div>
|
||||
<div class="midlightgrey gh-members-list-subscribed-moment">{{moment-from-now @member.createdAtUTC}}</div>
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
{{#each @filterColumns as |filterColumn|}}
|
||||
<GhMembersListItemColumnLabs @member={{@member}} @filterColumn={{filterColumn}} />
|
||||
{{!-- <LinkTo @route="member" @model={{@member}} title="Member details" class="gh-list-data middarkgrey f8">
|
||||
<span class="midlightgrey">Unknown</span>
|
||||
</LinkTo> --}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</tr>
|
@ -5,6 +5,7 @@ import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
||||
import moment from 'moment';
|
||||
import {A} from '@ember/array';
|
||||
import {action} from '@ember/object';
|
||||
import {capitalize} from '@ember/string';
|
||||
import {ghPluralize} from 'ghost-admin/helpers/gh-pluralize';
|
||||
import {resetQueryParams} from 'ghost-admin/helpers/reset-query-params';
|
||||
import {inject as service} from '@ember/service';
|
||||
@ -51,6 +52,7 @@ export default class MembersController extends Controller {
|
||||
@tracked modalLabel = null;
|
||||
@tracked showLabelModal = false;
|
||||
@tracked showDeleteMembersModal = false;
|
||||
@tracked filters = A([]);
|
||||
|
||||
@tracked _availableLabels = A([]);
|
||||
|
||||
@ -151,6 +153,21 @@ export default class MembersController extends Controller {
|
||||
return !!(this.label || this.paidParam || this.searchParam || this.filterParam);
|
||||
}
|
||||
|
||||
get filterColumns() {
|
||||
const defaultColumns = ['name', 'email'];
|
||||
return this.filters.map((filter) => {
|
||||
return filter.type;
|
||||
}).filter((f, idx, arr) => {
|
||||
return arr.indexOf(f) === idx;
|
||||
}).filter(d => !defaultColumns.includes(d));
|
||||
}
|
||||
|
||||
get filterColumnLabels() {
|
||||
return this.filterColumns.map((d) => {
|
||||
return capitalize(d.replace(/_/g, ' '));
|
||||
});
|
||||
}
|
||||
|
||||
getApiQueryObject({params, extraFilters = []} = {}) {
|
||||
let {label, paidParam, searchParam, filterParam} = params ? params : this;
|
||||
|
||||
@ -195,7 +212,8 @@ export default class MembersController extends Controller {
|
||||
}
|
||||
|
||||
@action
|
||||
applyFilter(filterStr) {
|
||||
applyFilter(filterStr, filters) {
|
||||
this.filters = filters;
|
||||
this.filterParam = filterStr || null;
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ export default class MembersRoute extends AuthenticatedRoute {
|
||||
searchParam: {refreshModel: true, replace: true},
|
||||
paidParam: {refreshModel: true},
|
||||
orderParam: {refreshModel: true},
|
||||
filterParam: {refreshModel: true, replace: true}
|
||||
filterParam: {refreshModel: true}
|
||||
};
|
||||
|
||||
// redirect to posts screen if:
|
||||
|
@ -88,30 +88,34 @@
|
||||
{{#unless this.members.loading}}
|
||||
<section class="view-container">
|
||||
{{#if (feature "membersFiltering")}}
|
||||
<div class="gh-list-scrolling">
|
||||
<table class="gh-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{this.listHeader}}</th>
|
||||
<th>Open rate</th>
|
||||
<th>Location</th>
|
||||
<th>Created</th>
|
||||
<th>Open rate</th>
|
||||
<th>Location</th>
|
||||
<th>Created</th>
|
||||
<th>Open rate</th>
|
||||
<th>Location</th>
|
||||
<th>Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<VerticalCollection @tagName="tbody" @items={{this.members}} @key="id" @containerSelector=".gh-main" @estimateHeight={{69}} @staticHeight={{true}} @bufferSize={{20}} as |member|>
|
||||
<GhMembersListItem
|
||||
@member={{member}}
|
||||
data-test-member={{member.id}}
|
||||
/>
|
||||
</VerticalCollection>
|
||||
</table>
|
||||
</div>
|
||||
<div class="gh-list-scrolling">
|
||||
<table class="gh-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{this.listHeader}}</th>
|
||||
<th>Open rate</th>
|
||||
<th>Location</th>
|
||||
<th>Created</th>
|
||||
{{#each this.filterColumnLabels as |filterColumn|}}
|
||||
<th>{{filterColumn}}</th>
|
||||
{{/each}}
|
||||
{{!-- <th>Open rate</th>
|
||||
<th>Location</th>
|
||||
<th>Created</th>
|
||||
<th>Open rate</th>
|
||||
<th>Location</th>
|
||||
<th>Created</th> --}}
|
||||
</tr>
|
||||
</thead>
|
||||
<VerticalCollection @tagName="tbody" @items={{this.members}} @key="id" @containerSelector=".gh-main" @estimateHeight={{69}} @staticHeight={{true}} @bufferSize={{20}} as |member|>
|
||||
<GhMembersListItemLabs
|
||||
@member={{member}}
|
||||
@filterColumns={{this.filterColumns}}
|
||||
data-test-member={{member.id}}
|
||||
/>
|
||||
</VerticalCollection>
|
||||
</table>
|
||||
</div>
|
||||
{{else}}
|
||||
<section class="content-list">
|
||||
<ol class="members-list gh-list {{unless this.members "no-posts"}}">
|
||||
|
Loading…
Reference in New Issue
Block a user