mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-26 04:13:30 +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"
|
type="button"
|
||||||
class="gh-btn gh-btn-text gh-btn-link green gh-btn-icon gh-add-filter"
|
class="gh-btn gh-btn-text gh-btn-link green gh-btn-icon gh-add-filter"
|
||||||
title="Delete 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>
|
{{svg-jar "close"}} <span class="hidden">Delete filter</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -8,34 +8,35 @@ const FILTER_PROPERTIES = [
|
|||||||
// Basic
|
// Basic
|
||||||
{label: 'Name', name: 'name', group: 'Basic'},
|
{label: 'Name', name: 'name', group: 'Basic'},
|
||||||
{label: 'Email', name: 'email', 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: 'Newsletter subscription status', name: 'subscribed', group: 'Basic'},
|
||||||
{label: 'Label', name: 'label', group: 'Basic'},
|
{label: 'Label', name: 'label', group: 'Basic'},
|
||||||
|
|
||||||
// Member subscription
|
// Member subscription
|
||||||
{label: 'Member status', name: 'member-status', group: 'Subscription'},
|
{label: 'Member status', name: 'status', group: 'Subscription'},
|
||||||
{label: 'Tier', name: 'tier', group: 'Subscription'},
|
// {label: 'Tier', name: 'tier', group: 'Subscription'},
|
||||||
{label: 'Billing period', name: 'billing-period', group: 'Subscription'},
|
// {label: 'Billing period', name: 'billing-period', group: 'Subscription'},
|
||||||
|
|
||||||
// Emails
|
// Emails
|
||||||
{label: 'Emails sent (all time)', name: 'x', group: 'Email'},
|
{label: 'Emails sent (all time)', name: 'email_count', group: 'Email'},
|
||||||
{label: 'Emails opened (all time)', name: 'x', group: 'Email'},
|
{label: 'Emails opened (all time)', name: 'email_opened_count', group: 'Email'},
|
||||||
{label: 'Open rate (all time)', name: 'x', group: 'Email'},
|
{label: 'Open rate (all time)', name: 'email_open_rate', group: 'Email'},
|
||||||
{label: 'Emails sent (30 days)', name: 'x', group: 'Email'},
|
// {label: 'Emails sent (30 days)', name: 'x', group: 'Email'},
|
||||||
{label: 'Emails opened (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: 'Open rate (30 days)', name: 'x', group: 'Email'},
|
||||||
{label: 'Emails sent (60 days)', name: 'x', group: 'Email'},
|
// {label: 'Emails sent (60 days)', name: 'x', group: 'Email'},
|
||||||
{label: 'Emails opened (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: 'Open rate (60 days)', name: 'x', group: 'Email'},
|
||||||
{label: 'Stripe subscription status', name: 'status', group: 'Email'}
|
{label: 'Stripe subscription status', name: 'stripe-status', group: 'Email'}
|
||||||
];
|
];
|
||||||
|
|
||||||
const FILTER_RELATIONS = [
|
const FILTER_RELATIONS = [
|
||||||
{label: 'is', name: 'is'},
|
{label: 'is', name: 'is'},
|
||||||
{label: 'is not', name: 'is-not'},
|
{label: 'is not', name: 'is-not'},
|
||||||
{label: 'contains', name: 'contains'},
|
{label: 'in', name: 'in'}
|
||||||
{label: 'exists', name: 'exists'},
|
// {label: 'contains', name: 'contains'},
|
||||||
{label: 'does not exist', name: 'does-not-exist'}
|
// {label: 'exists', name: 'exists'},
|
||||||
|
// {label: 'does not exist', name: 'does-not-exist'}
|
||||||
];
|
];
|
||||||
|
|
||||||
export default class GhMembersFilterLabsComponent extends Component {
|
export default class GhMembersFilterLabsComponent extends Component {
|
||||||
@ -71,7 +72,8 @@ export default class GhMembersFilterLabsComponent extends Component {
|
|||||||
let query = '';
|
let query = '';
|
||||||
filters.forEach((filter) => {
|
filters.forEach((filter) => {
|
||||||
const relationStr = filter.relation === 'is-not' ? '-' : '';
|
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);
|
return query.slice(0, -1);
|
||||||
}
|
}
|
||||||
@ -103,6 +105,6 @@ export default class GhMembersFilterLabsComponent extends Component {
|
|||||||
@action
|
@action
|
||||||
applyFilter() {
|
applyFilter() {
|
||||||
const query = this.generateNqlFilter(this.filters);
|
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 moment from 'moment';
|
||||||
import {A} from '@ember/array';
|
import {A} from '@ember/array';
|
||||||
import {action} from '@ember/object';
|
import {action} from '@ember/object';
|
||||||
|
import {capitalize} from '@ember/string';
|
||||||
import {ghPluralize} from 'ghost-admin/helpers/gh-pluralize';
|
import {ghPluralize} from 'ghost-admin/helpers/gh-pluralize';
|
||||||
import {resetQueryParams} from 'ghost-admin/helpers/reset-query-params';
|
import {resetQueryParams} from 'ghost-admin/helpers/reset-query-params';
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
@ -51,6 +52,7 @@ export default class MembersController extends Controller {
|
|||||||
@tracked modalLabel = null;
|
@tracked modalLabel = null;
|
||||||
@tracked showLabelModal = false;
|
@tracked showLabelModal = false;
|
||||||
@tracked showDeleteMembersModal = false;
|
@tracked showDeleteMembersModal = false;
|
||||||
|
@tracked filters = A([]);
|
||||||
|
|
||||||
@tracked _availableLabels = A([]);
|
@tracked _availableLabels = A([]);
|
||||||
|
|
||||||
@ -151,6 +153,21 @@ export default class MembersController extends Controller {
|
|||||||
return !!(this.label || this.paidParam || this.searchParam || this.filterParam);
|
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 = []} = {}) {
|
getApiQueryObject({params, extraFilters = []} = {}) {
|
||||||
let {label, paidParam, searchParam, filterParam} = params ? params : this;
|
let {label, paidParam, searchParam, filterParam} = params ? params : this;
|
||||||
|
|
||||||
@ -195,7 +212,8 @@ export default class MembersController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
applyFilter(filterStr) {
|
applyFilter(filterStr, filters) {
|
||||||
|
this.filters = filters;
|
||||||
this.filterParam = filterStr || null;
|
this.filterParam = filterStr || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ export default class MembersRoute extends AuthenticatedRoute {
|
|||||||
searchParam: {refreshModel: true, replace: true},
|
searchParam: {refreshModel: true, replace: true},
|
||||||
paidParam: {refreshModel: true},
|
paidParam: {refreshModel: true},
|
||||||
orderParam: {refreshModel: true},
|
orderParam: {refreshModel: true},
|
||||||
filterParam: {refreshModel: true, replace: true}
|
filterParam: {refreshModel: true}
|
||||||
};
|
};
|
||||||
|
|
||||||
// redirect to posts screen if:
|
// redirect to posts screen if:
|
||||||
|
@ -88,30 +88,34 @@
|
|||||||
{{#unless this.members.loading}}
|
{{#unless this.members.loading}}
|
||||||
<section class="view-container">
|
<section class="view-container">
|
||||||
{{#if (feature "membersFiltering")}}
|
{{#if (feature "membersFiltering")}}
|
||||||
<div class="gh-list-scrolling">
|
<div class="gh-list-scrolling">
|
||||||
<table class="gh-list">
|
<table class="gh-list">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{this.listHeader}}</th>
|
<th>{{this.listHeader}}</th>
|
||||||
<th>Open rate</th>
|
<th>Open rate</th>
|
||||||
<th>Location</th>
|
<th>Location</th>
|
||||||
<th>Created</th>
|
<th>Created</th>
|
||||||
<th>Open rate</th>
|
{{#each this.filterColumnLabels as |filterColumn|}}
|
||||||
<th>Location</th>
|
<th>{{filterColumn}}</th>
|
||||||
<th>Created</th>
|
{{/each}}
|
||||||
<th>Open rate</th>
|
{{!-- <th>Open rate</th>
|
||||||
<th>Location</th>
|
<th>Location</th>
|
||||||
<th>Created</th>
|
<th>Created</th>
|
||||||
</tr>
|
<th>Open rate</th>
|
||||||
</thead>
|
<th>Location</th>
|
||||||
<VerticalCollection @tagName="tbody" @items={{this.members}} @key="id" @containerSelector=".gh-main" @estimateHeight={{69}} @staticHeight={{true}} @bufferSize={{20}} as |member|>
|
<th>Created</th> --}}
|
||||||
<GhMembersListItem
|
</tr>
|
||||||
@member={{member}}
|
</thead>
|
||||||
data-test-member={{member.id}}
|
<VerticalCollection @tagName="tbody" @items={{this.members}} @key="id" @containerSelector=".gh-main" @estimateHeight={{69}} @staticHeight={{true}} @bufferSize={{20}} as |member|>
|
||||||
/>
|
<GhMembersListItemLabs
|
||||||
</VerticalCollection>
|
@member={{member}}
|
||||||
</table>
|
@filterColumns={{this.filterColumns}}
|
||||||
</div>
|
data-test-member={{member.id}}
|
||||||
|
/>
|
||||||
|
</VerticalCollection>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<section class="content-list">
|
<section class="content-list">
|
||||||
<ol class="members-list gh-list {{unless this.members "no-posts"}}">
|
<ol class="members-list gh-list {{unless this.members "no-posts"}}">
|
||||||
|
Loading…
Reference in New Issue
Block a user