mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-25 02:31:59 -05:00
Cleaned up member filters (#15784)
fixes https://github.com/TryGhost/Team/issues/2134 fixes https://github.com/TryGhost/Team/issues/2133 - Moved all filters to separate files to make the filter component a lot more readable and easier to maintain. - Removed long switch style code from hbs for filter column values - Filters for features that are disabled (such as open tracking, click tracking or member attribution) are now hidden when they are disabled - The open rate column in the members table is now only visible if open tracking is enabled
This commit is contained in:
parent
c2dfb2b579
commit
b821c84b9e
35 changed files with 481 additions and 441 deletions
|
@ -1,6 +1,7 @@
|
||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import nql from '@tryghost/nql-lang';
|
import nql from '@tryghost/nql-lang';
|
||||||
|
import {AUDIENCE_FEEDBACK_FILTER, CREATED_AT_FILTER, EMAIL_CLICKED_FILTER, EMAIL_COUNT_FILTER, EMAIL_FILTER, EMAIL_OPENED_COUNT_FILTER, EMAIL_OPENED_FILTER, EMAIL_OPEN_RATE_FILTER, EMAIL_RECEIVED_FILTER, LABEL_FILTER, LAST_SEEN_FILTER, NAME_FILTER, NEXT_BILLING_DATE_FILTER, PLAN_INTERVAL_FILTER, SIGNUP_ATTRIBUTION_FILTER, STATUS_FILTER, SUBSCRIBED_FILTER, SUBSCRIPTION_ATTRIBUTION_FILTER, SUBSCRIPTION_START_DATE_FILTER, SUBSCRIPTION_STATUS_FILTER, TIER_FILTER} from './filters';
|
||||||
import {TrackedArray} from 'tracked-built-ins';
|
import {TrackedArray} from 'tracked-built-ins';
|
||||||
import {action} from '@ember/object';
|
import {action} from '@ember/object';
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
|
@ -11,329 +12,50 @@ function escapeNqlString(value) {
|
||||||
return '\'' + value.replace(/'/g, '\\\'') + '\'';
|
return '\'' + value.replace(/'/g, '\\\'') + '\'';
|
||||||
}
|
}
|
||||||
|
|
||||||
const MATCH_RELATION_OPTIONS = [
|
const FILTER_GROUPS = [
|
||||||
{label: 'is', name: 'is'},
|
|
||||||
{label: 'is not', name: 'is-not'}
|
|
||||||
];
|
|
||||||
|
|
||||||
const CONTAINS_RELATION_OPTIONS = [
|
|
||||||
{label: 'is', name: 'is'},
|
|
||||||
{label: 'contains', name: 'contains'},
|
|
||||||
{label: 'does not contain', name: 'does-not-contain'},
|
|
||||||
{label: 'starts with', name: 'starts-with'},
|
|
||||||
{label: 'ends with', name: 'ends-with'}
|
|
||||||
];
|
|
||||||
|
|
||||||
const FEEDBACK_RELATION_OPTIONS = [
|
|
||||||
{label: 'More like this', name: 1},
|
|
||||||
{label: 'Less like this', name: 0}
|
|
||||||
];
|
|
||||||
|
|
||||||
const DATE_RELATION_OPTIONS = [
|
|
||||||
{label: 'before', name: 'is-less'},
|
|
||||||
{label: 'on or before', name: 'is-or-less'},
|
|
||||||
{label: 'after', name: 'is-greater'},
|
|
||||||
{label: 'on or after', name: 'is-or-greater'}
|
|
||||||
];
|
|
||||||
|
|
||||||
const NUMBER_RELATION_OPTIONS = [
|
|
||||||
{label: 'is', name: 'is'},
|
|
||||||
{label: 'is greater than', name: 'is-greater'},
|
|
||||||
{label: 'is less than', name: 'is-less'}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Ideally we should move all the filter definitions to separate files
|
|
||||||
const NAME_FILTER = {
|
|
||||||
label: 'Name',
|
|
||||||
name: 'name',
|
|
||||||
group: 'Basic',
|
|
||||||
valueType: 'string',
|
|
||||||
relationOptions: CONTAINS_RELATION_OPTIONS
|
|
||||||
};
|
|
||||||
|
|
||||||
const FILTER_PROPERTIES = [
|
|
||||||
// Basic
|
|
||||||
NAME_FILTER,
|
|
||||||
{
|
{
|
||||||
label: 'Email',
|
name: 'Basic',
|
||||||
name: 'email',
|
filters: [
|
||||||
group: 'Basic',
|
NAME_FILTER,
|
||||||
valueType: 'string',
|
EMAIL_FILTER,
|
||||||
relationOptions: CONTAINS_RELATION_OPTIONS
|
LABEL_FILTER,
|
||||||
},
|
SUBSCRIBED_FILTER,
|
||||||
{
|
LAST_SEEN_FILTER,
|
||||||
label: 'Label',
|
CREATED_AT_FILTER,
|
||||||
name: 'label',
|
SIGNUP_ATTRIBUTION_FILTER
|
||||||
group: 'Basic',
|
|
||||||
valueType: 'array',
|
|
||||||
columnLabel: 'Label',
|
|
||||||
relationOptions: MATCH_RELATION_OPTIONS
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Newsletter subscription',
|
|
||||||
name: 'subscribed',
|
|
||||||
group: 'Basic',
|
|
||||||
columnLabel: 'Subscribed',
|
|
||||||
relationOptions: MATCH_RELATION_OPTIONS,
|
|
||||||
valueType: 'options',
|
|
||||||
options: [
|
|
||||||
{label: 'Subscribed', name: 'true'},
|
|
||||||
{label: 'Unsubscribed', name: 'false'}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Last seen',
|
name: 'Subscription',
|
||||||
name: 'last_seen_at',
|
filters: [
|
||||||
group: 'Basic',
|
TIER_FILTER,
|
||||||
valueType: 'date',
|
STATUS_FILTER,
|
||||||
columnLabel: 'Last seen at',
|
PLAN_INTERVAL_FILTER,
|
||||||
relationOptions: DATE_RELATION_OPTIONS
|
SUBSCRIPTION_STATUS_FILTER,
|
||||||
},
|
SUBSCRIPTION_START_DATE_FILTER,
|
||||||
{
|
NEXT_BILLING_DATE_FILTER,
|
||||||
label: 'Created',
|
SUBSCRIPTION_ATTRIBUTION_FILTER
|
||||||
name: 'created_at',
|
|
||||||
group: 'Basic',
|
|
||||||
valueType: 'date',
|
|
||||||
relationOptions: DATE_RELATION_OPTIONS
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Signed up on post/page',
|
|
||||||
name: 'signup',
|
|
||||||
group: 'Basic',
|
|
||||||
valueType: 'string',
|
|
||||||
resource: 'post',
|
|
||||||
feature: 'memberAttribution',
|
|
||||||
relationOptions: MATCH_RELATION_OPTIONS,
|
|
||||||
getColumns: filter => [
|
|
||||||
{
|
|
||||||
label: 'Signed up on',
|
|
||||||
getValue: () => {
|
|
||||||
return {
|
|
||||||
class: '',
|
|
||||||
text: filter.resource?.title ?? ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
// Member subscription
|
|
||||||
{
|
|
||||||
label: 'Membership tier',
|
|
||||||
name: 'tier',
|
|
||||||
group: 'Subscription',
|
|
||||||
valueType: 'array',
|
|
||||||
columnLabel: 'Membership tier',
|
|
||||||
relationOptions: MATCH_RELATION_OPTIONS
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Member status',
|
|
||||||
name: 'status',
|
|
||||||
group: 'Subscription',
|
|
||||||
relationOptions: MATCH_RELATION_OPTIONS,
|
|
||||||
valueType: 'options',
|
|
||||||
options: [
|
|
||||||
{label: 'Paid', name: 'paid'},
|
|
||||||
{label: 'Free', name: 'free'},
|
|
||||||
{label: 'Complimentary', name: 'comped'}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Billing period',
|
name: 'Email',
|
||||||
name: 'subscriptions.plan_interval',
|
filters: [
|
||||||
group: 'Subscription',
|
EMAIL_COUNT_FILTER,
|
||||||
columnLabel: 'Billing period',
|
EMAIL_OPENED_COUNT_FILTER,
|
||||||
relationOptions: MATCH_RELATION_OPTIONS,
|
EMAIL_OPEN_RATE_FILTER,
|
||||||
valueType: 'options',
|
EMAIL_RECEIVED_FILTER,
|
||||||
options: [
|
EMAIL_OPENED_FILTER,
|
||||||
{label: 'Monthly', name: 'month'},
|
EMAIL_CLICKED_FILTER,
|
||||||
{label: 'Yearly', name: 'year'}
|
AUDIENCE_FEEDBACK_FILTER
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Stripe subscription status',
|
|
||||||
name: 'subscriptions.status',
|
|
||||||
group: 'Subscription',
|
|
||||||
columnLabel: 'Subscription Status',
|
|
||||||
relationOptions: MATCH_RELATION_OPTIONS,
|
|
||||||
valueType: 'options',
|
|
||||||
options: [
|
|
||||||
{label: 'Active', name: 'active'},
|
|
||||||
{label: 'Trialing', name: 'trialing'},
|
|
||||||
{label: 'Canceled', name: 'canceled'},
|
|
||||||
{label: 'Unpaid', name: 'unpaid'},
|
|
||||||
{label: 'Past Due', name: 'past_due'},
|
|
||||||
{label: 'Incomplete', name: 'incomplete'},
|
|
||||||
{label: 'Incomplete - Expired', name: 'incomplete_expired'}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Paid start date',
|
|
||||||
name: 'subscriptions.start_date',
|
|
||||||
valueType: 'date',
|
|
||||||
group: 'Subscription',
|
|
||||||
columnLabel: 'Paid start date',
|
|
||||||
relationOptions: DATE_RELATION_OPTIONS
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Next billing date',
|
|
||||||
name: 'subscriptions.current_period_end',
|
|
||||||
valueType: 'date',
|
|
||||||
group: 'Subscription',
|
|
||||||
columnLabel: 'Next billing date',
|
|
||||||
relationOptions: DATE_RELATION_OPTIONS
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Subscription started on post/page',
|
|
||||||
name: 'conversion',
|
|
||||||
group: 'Subscription',
|
|
||||||
valueType: 'string',
|
|
||||||
resource: 'post',
|
|
||||||
feature: 'memberAttribution',
|
|
||||||
relationOptions: MATCH_RELATION_OPTIONS,
|
|
||||||
getColumns: filter => [
|
|
||||||
{
|
|
||||||
label: 'Subscription started on',
|
|
||||||
getValue: () => {
|
|
||||||
return {
|
|
||||||
class: '',
|
|
||||||
text: filter.resource?.title ?? ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
// Emails
|
|
||||||
{
|
|
||||||
label: 'Emails sent (all time)',
|
|
||||||
name: 'email_count',
|
|
||||||
group: 'Email',
|
|
||||||
columnLabel: 'Email count',
|
|
||||||
valueType: 'number',
|
|
||||||
relationOptions: NUMBER_RELATION_OPTIONS
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Emails opened (all time)',
|
|
||||||
name: 'email_opened_count',
|
|
||||||
group: 'Email',
|
|
||||||
columnLabel: 'Email opened count',
|
|
||||||
valueType: 'number',
|
|
||||||
relationOptions: NUMBER_RELATION_OPTIONS
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Open rate (all time)',
|
|
||||||
name: 'email_open_rate',
|
|
||||||
group: 'Email',
|
|
||||||
valueType: 'number',
|
|
||||||
relationOptions: NUMBER_RELATION_OPTIONS
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Received email',
|
|
||||||
name: 'emails.post_id',
|
|
||||||
group: 'Email',
|
|
||||||
valueType: 'string',
|
|
||||||
resource: 'email',
|
|
||||||
relationOptions: MATCH_RELATION_OPTIONS,
|
|
||||||
getColumns: filter => [
|
|
||||||
{
|
|
||||||
label: 'Received email',
|
|
||||||
getValue: () => {
|
|
||||||
return {
|
|
||||||
class: '',
|
|
||||||
text: filter.resource?.title ?? ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Opened email',
|
|
||||||
name: 'opened_emails.post_id',
|
|
||||||
group: 'Email',
|
|
||||||
valueType: 'string',
|
|
||||||
resource: 'email',
|
|
||||||
relationOptions: MATCH_RELATION_OPTIONS,
|
|
||||||
getColumns: filter => [
|
|
||||||
{
|
|
||||||
label: 'Opened email',
|
|
||||||
getValue: () => {
|
|
||||||
return {
|
|
||||||
class: '',
|
|
||||||
text: filter.resource?.title ?? ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Clicked email',
|
|
||||||
name: 'clicked_links.post_id',
|
|
||||||
group: 'Email',
|
|
||||||
valueType: 'string',
|
|
||||||
resource: 'email',
|
|
||||||
relationOptions: MATCH_RELATION_OPTIONS,
|
|
||||||
getColumns: filter => [
|
|
||||||
{
|
|
||||||
label: 'Clicked email',
|
|
||||||
getValue: () => {
|
|
||||||
return {
|
|
||||||
class: '',
|
|
||||||
text: filter.resource?.title ?? ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Responded with feedback',
|
|
||||||
name: 'newsletter_feedback',
|
|
||||||
group: 'Email',
|
|
||||||
valueType: 'string',
|
|
||||||
resource: 'email',
|
|
||||||
relationOptions: FEEDBACK_RELATION_OPTIONS,
|
|
||||||
feature: 'audienceFeedback',
|
|
||||||
buildNqlFilter: (filter) => {
|
|
||||||
// Added brackets to make sure we can parse as a single AND filter
|
|
||||||
return `(feedback.post_id:${filter.value}+feedback.score:${filter.relation})`;
|
|
||||||
},
|
|
||||||
parseNqlFilter: (filter) => {
|
|
||||||
if (!filter.$and) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (filter.$and.length === 2) {
|
|
||||||
if (filter.$and[0]['feedback.post_id'] && filter.$and[1]['feedback.score'] !== undefined) {
|
|
||||||
return {
|
|
||||||
relation: parseInt(filter.$and[1]['feedback.score']),
|
|
||||||
value: filter.$and[0]['feedback.post_id']
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getColumns: filter => [
|
|
||||||
{
|
|
||||||
label: 'Email',
|
|
||||||
getValue: () => {
|
|
||||||
return {
|
|
||||||
class: '',
|
|
||||||
text: filter.resource?.title ?? ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Feedback',
|
|
||||||
getValue: () => {
|
|
||||||
return {
|
|
||||||
class: 'gh-members-list-feedback',
|
|
||||||
text: filter.relation === 1 ? 'More like this' : 'Less like this',
|
|
||||||
icon: filter.relation === 1 ? 'event-more-like-this' : 'event-less-like-this'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const FILTER_PROPERTIES = FILTER_GROUPS.flatMap(group => group.filters.map((f) => {
|
||||||
|
f.group = group.name;
|
||||||
|
return f;
|
||||||
|
}));
|
||||||
|
|
||||||
class Filter {
|
class Filter {
|
||||||
@tracked value;
|
@tracked value;
|
||||||
@tracked relation;
|
@tracked relation;
|
||||||
|
@ -413,6 +135,7 @@ export default class MembersFilter extends Component {
|
||||||
@service session;
|
@service session;
|
||||||
@service settings;
|
@service settings;
|
||||||
@service store;
|
@service store;
|
||||||
|
@service membersUtils;
|
||||||
|
|
||||||
@tracked filters = new TrackedArray([
|
@tracked filters = new TrackedArray([
|
||||||
new Filter({
|
new Filter({
|
||||||
|
@ -422,10 +145,11 @@ export default class MembersFilter extends Component {
|
||||||
|
|
||||||
get availableFilterProperties() {
|
get availableFilterProperties() {
|
||||||
let availableFilters = FILTER_PROPERTIES;
|
let availableFilters = FILTER_PROPERTIES;
|
||||||
const hasMultipleTiers = this.store.peekAll('tier').length > 1;
|
const hasMultipleTiers = this.membersUtils.hasMultipleTiers;
|
||||||
|
|
||||||
// exclude any filters that are behind disabled feature flags
|
// exclude any filters that are behind disabled feature flags
|
||||||
availableFilters = availableFilters.filter(prop => !prop.feature || this.feature[prop.feature]);
|
availableFilters = availableFilters.filter(prop => !prop.feature || this.feature[prop.feature]);
|
||||||
|
availableFilters = availableFilters.filter(prop => !prop.setting || this.settings[prop.setting]);
|
||||||
|
|
||||||
// exclude tiers filter if site has only single tier
|
// exclude tiers filter if site has only single tier
|
||||||
availableFilters = availableFilters
|
availableFilters = availableFilters
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
const FEEDBACK_RELATION_OPTIONS = [
|
||||||
|
{label: 'More like this', name: 1},
|
||||||
|
{label: 'Less like this', name: 0}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AUDIENCE_FEEDBACK_FILTER = {
|
||||||
|
label: 'Responded with feedback',
|
||||||
|
name: 'newsletter_feedback',
|
||||||
|
valueType: 'string',
|
||||||
|
resource: 'email',
|
||||||
|
relationOptions: FEEDBACK_RELATION_OPTIONS,
|
||||||
|
feature: 'audienceFeedback',
|
||||||
|
buildNqlFilter: (filter) => {
|
||||||
|
// Added brackets to make sure we can parse as a single AND filter
|
||||||
|
return `(feedback.post_id:${filter.value}+feedback.score:${filter.relation})`;
|
||||||
|
},
|
||||||
|
parseNqlFilter: (filter) => {
|
||||||
|
if (!filter.$and) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (filter.$and.length === 2) {
|
||||||
|
if (filter.$and[0]['feedback.post_id'] && filter.$and[1]['feedback.score'] !== undefined) {
|
||||||
|
return {
|
||||||
|
relation: parseInt(filter.$and[1]['feedback.score']),
|
||||||
|
value: filter.$and[0]['feedback.post_id']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getColumns: filter => [
|
||||||
|
{
|
||||||
|
label: 'Email',
|
||||||
|
getValue: () => {
|
||||||
|
return {
|
||||||
|
class: '',
|
||||||
|
text: filter.resource?.title ?? ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Feedback',
|
||||||
|
getValue: () => {
|
||||||
|
return {
|
||||||
|
class: 'gh-members-list-feedback',
|
||||||
|
text: filter.relation === 1 ? 'More like this' : 'Less like this',
|
||||||
|
icon: filter.relation === 1 ? 'event-more-like-this' : 'event-less-like-this'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
|
@ -0,0 +1,13 @@
|
||||||
|
import moment from 'moment-timezone';
|
||||||
|
|
||||||
|
export function getDateColumnValue(date, filter) {
|
||||||
|
if (!date) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
class: '',
|
||||||
|
text: date ? moment.tz(date, filter.timezone).format('DD MMM YYYY') : '',
|
||||||
|
subtext: moment(date).from(moment()),
|
||||||
|
subtextClass: 'gh-members-list-subscribed-moment'
|
||||||
|
};
|
||||||
|
}
|
8
ghost/admin/app/components/members/filters/created-at.js
Normal file
8
ghost/admin/app/components/members/filters/created-at.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import {DATE_RELATION_OPTIONS} from './relation-options';
|
||||||
|
|
||||||
|
export const CREATED_AT_FILTER = {
|
||||||
|
label: 'Created',
|
||||||
|
name: 'created_at',
|
||||||
|
valueType: 'date',
|
||||||
|
relationOptions: DATE_RELATION_OPTIONS
|
||||||
|
};
|
16
ghost/admin/app/components/members/filters/email-clicked.js
Normal file
16
ghost/admin/app/components/members/filters/email-clicked.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import {MATCH_RELATION_OPTIONS} from './relation-options';
|
||||||
|
|
||||||
|
export const EMAIL_CLICKED_FILTER = {
|
||||||
|
label: 'Clicked email',
|
||||||
|
name: 'clicked_links.post_id',
|
||||||
|
valueType: 'string',
|
||||||
|
resource: 'email',
|
||||||
|
relationOptions: MATCH_RELATION_OPTIONS,
|
||||||
|
columnLabel: 'Clicked email',
|
||||||
|
setting: 'emailTrackClicks',
|
||||||
|
getColumnValue: (member, filter) => {
|
||||||
|
return {
|
||||||
|
text: filter.resource?.title ?? ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
15
ghost/admin/app/components/members/filters/email-count.js
Normal file
15
ghost/admin/app/components/members/filters/email-count.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import {NUMBER_RELATION_OPTIONS} from './relation-options';
|
||||||
|
import {formatNumber} from 'ghost-admin/helpers/format-number';
|
||||||
|
|
||||||
|
export const EMAIL_COUNT_FILTER = {
|
||||||
|
label: 'Emails sent (all time)',
|
||||||
|
name: 'email_count',
|
||||||
|
columnLabel: 'Email count',
|
||||||
|
valueType: 'number',
|
||||||
|
relationOptions: NUMBER_RELATION_OPTIONS,
|
||||||
|
getColumnValue: (member) => {
|
||||||
|
return {
|
||||||
|
text: formatNumber(member.emailCount)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
import {NUMBER_RELATION_OPTIONS} from './relation-options';
|
||||||
|
|
||||||
|
export const EMAIL_OPEN_RATE_FILTER = {
|
||||||
|
label: 'Open rate (all time)',
|
||||||
|
name: 'email_open_rate',
|
||||||
|
valueType: 'number',
|
||||||
|
setting: 'emailTrackOpens',
|
||||||
|
relationOptions: NUMBER_RELATION_OPTIONS
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
import {NUMBER_RELATION_OPTIONS} from './relation-options';
|
||||||
|
import {formatNumber} from 'ghost-admin/helpers/format-number';
|
||||||
|
|
||||||
|
export const EMAIL_OPENED_COUNT_FILTER = {
|
||||||
|
label: 'Emails opened (all time)',
|
||||||
|
name: 'email_opened_count',
|
||||||
|
columnLabel: 'Email opened count',
|
||||||
|
valueType: 'number',
|
||||||
|
relationOptions: NUMBER_RELATION_OPTIONS,
|
||||||
|
getColumnValue: (member) => {
|
||||||
|
return {
|
||||||
|
text: formatNumber(member.emailOpenedCount)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
16
ghost/admin/app/components/members/filters/email-opened.js
Normal file
16
ghost/admin/app/components/members/filters/email-opened.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import {MATCH_RELATION_OPTIONS} from './relation-options';
|
||||||
|
|
||||||
|
export const EMAIL_OPENED_FILTER = {
|
||||||
|
label: 'Opened email',
|
||||||
|
name: 'opened_emails.post_id',
|
||||||
|
valueType: 'string',
|
||||||
|
resource: 'email',
|
||||||
|
relationOptions: MATCH_RELATION_OPTIONS,
|
||||||
|
columnLabel: 'Opened email',
|
||||||
|
setting: 'emailTrackOpens',
|
||||||
|
getColumnValue: (member, filter) => {
|
||||||
|
return {
|
||||||
|
text: filter.resource?.title ?? ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
15
ghost/admin/app/components/members/filters/email-received.js
Normal file
15
ghost/admin/app/components/members/filters/email-received.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import {MATCH_RELATION_OPTIONS} from './relation-options';
|
||||||
|
|
||||||
|
export const EMAIL_RECEIVED_FILTER = {
|
||||||
|
label: 'Received email',
|
||||||
|
name: 'emails.post_id',
|
||||||
|
valueType: 'string',
|
||||||
|
resource: 'email',
|
||||||
|
relationOptions: MATCH_RELATION_OPTIONS,
|
||||||
|
columnLabel: 'Received email',
|
||||||
|
getColumnValue: (member, filter) => {
|
||||||
|
return {
|
||||||
|
text: filter.resource?.title ?? ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
8
ghost/admin/app/components/members/filters/email.js
Normal file
8
ghost/admin/app/components/members/filters/email.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import {CONTAINS_RELATION_OPTIONS} from './relation-options';
|
||||||
|
|
||||||
|
export const EMAIL_FILTER = {
|
||||||
|
label: 'Email',
|
||||||
|
name: 'email',
|
||||||
|
valueType: 'string',
|
||||||
|
relationOptions: CONTAINS_RELATION_OPTIONS
|
||||||
|
};
|
22
ghost/admin/app/components/members/filters/index.js
Normal file
22
ghost/admin/app/components/members/filters/index.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
export * from './name';
|
||||||
|
export * from './email';
|
||||||
|
export * from './label';
|
||||||
|
export * from './subscribed';
|
||||||
|
export * from './last-seen';
|
||||||
|
export * from './created-at';
|
||||||
|
export * from './signup-attribution';
|
||||||
|
export * from './tier';
|
||||||
|
export * from './status';
|
||||||
|
export * from './plan-interval';
|
||||||
|
export * from './subscription-status';
|
||||||
|
export * from './subscription-start-date';
|
||||||
|
export * from './next-billing-date';
|
||||||
|
export * from './subscription-attribution';
|
||||||
|
export * from './email-count';
|
||||||
|
export * from './email-opened';
|
||||||
|
export * from './email-clicked';
|
||||||
|
export * from './email-opened-count';
|
||||||
|
export * from './email-open-rate';
|
||||||
|
export * from './email-clicked';
|
||||||
|
export * from './email-received';
|
||||||
|
export * from './audience-feedback';
|
15
ghost/admin/app/components/members/filters/label.js
Normal file
15
ghost/admin/app/components/members/filters/label.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import {MATCH_RELATION_OPTIONS} from './relation-options';
|
||||||
|
|
||||||
|
export const LABEL_FILTER = {
|
||||||
|
label: 'Label',
|
||||||
|
name: 'label',
|
||||||
|
valueType: 'array',
|
||||||
|
columnLabel: 'Label',
|
||||||
|
relationOptions: MATCH_RELATION_OPTIONS,
|
||||||
|
getColumnValue: (member) => {
|
||||||
|
return {
|
||||||
|
class: 'gh-members-list-labels',
|
||||||
|
text: (member.labels ?? []).map(label => label.name).join(', ')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
13
ghost/admin/app/components/members/filters/last-seen.js
Normal file
13
ghost/admin/app/components/members/filters/last-seen.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {DATE_RELATION_OPTIONS} from './relation-options';
|
||||||
|
import {getDateColumnValue} from './columns/date-column';
|
||||||
|
|
||||||
|
export const LAST_SEEN_FILTER = {
|
||||||
|
label: 'Last seen',
|
||||||
|
name: 'last_seen_at',
|
||||||
|
valueType: 'date',
|
||||||
|
columnLabel: 'Last seen at',
|
||||||
|
relationOptions: DATE_RELATION_OPTIONS,
|
||||||
|
getColumnValue: (member, filter) => {
|
||||||
|
return getDateColumnValue(member.lastSeenAtUTC, filter);
|
||||||
|
}
|
||||||
|
};
|
8
ghost/admin/app/components/members/filters/name.js
Normal file
8
ghost/admin/app/components/members/filters/name.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import {CONTAINS_RELATION_OPTIONS} from './relation-options';
|
||||||
|
|
||||||
|
export const NAME_FILTER = {
|
||||||
|
label: 'Name',
|
||||||
|
name: 'name',
|
||||||
|
valueType: 'string',
|
||||||
|
relationOptions: CONTAINS_RELATION_OPTIONS
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
import {DATE_RELATION_OPTIONS} from './relation-options';
|
||||||
|
import {getDateColumnValue} from './columns/date-column';
|
||||||
|
import {mostRecentlyUpdated} from 'ghost-admin/helpers/most-recently-updated';
|
||||||
|
|
||||||
|
export const NEXT_BILLING_DATE_FILTER = {
|
||||||
|
label: 'Next billing date',
|
||||||
|
name: 'subscriptions.current_period_end',
|
||||||
|
valueType: 'date',
|
||||||
|
columnLabel: 'Next billing date',
|
||||||
|
relationOptions: DATE_RELATION_OPTIONS,
|
||||||
|
getColumnValue: (member, filter) => {
|
||||||
|
const subscription = mostRecentlyUpdated(member.subscriptions);
|
||||||
|
return getDateColumnValue(subscription?.current_period_end, filter);
|
||||||
|
}
|
||||||
|
};
|
24
ghost/admin/app/components/members/filters/plan-interval.js
Normal file
24
ghost/admin/app/components/members/filters/plan-interval.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import {MATCH_RELATION_OPTIONS} from './relation-options';
|
||||||
|
import {capitalizeFirstLetter} from 'ghost-admin/helpers/capitalize-first-letter';
|
||||||
|
import {mostRecentlyUpdated} from 'ghost-admin/helpers/most-recently-updated';
|
||||||
|
|
||||||
|
export const PLAN_INTERVAL_FILTER = {
|
||||||
|
label: 'Billing period',
|
||||||
|
name: 'subscriptions.plan_interval',
|
||||||
|
columnLabel: 'Billing period',
|
||||||
|
relationOptions: MATCH_RELATION_OPTIONS,
|
||||||
|
valueType: 'options',
|
||||||
|
options: [
|
||||||
|
{label: 'Monthly', name: 'month'},
|
||||||
|
{label: 'Yearly', name: 'year'}
|
||||||
|
],
|
||||||
|
getColumnValue: (member) => {
|
||||||
|
const subscription = mostRecentlyUpdated(member.subscriptions);
|
||||||
|
if (!subscription) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: capitalizeFirstLetter(subscription.price?.interval)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const CONTAINS_RELATION_OPTIONS = [
|
||||||
|
{label: 'is', name: 'is'},
|
||||||
|
{label: 'contains', name: 'contains'},
|
||||||
|
{label: 'does not contain', name: 'does-not-contain'},
|
||||||
|
{label: 'starts with', name: 'starts-with'},
|
||||||
|
{label: 'ends with', name: 'ends-with'}
|
||||||
|
];
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const DATE_RELATION_OPTIONS = [
|
||||||
|
{label: 'before', name: 'is-less'},
|
||||||
|
{label: 'on or before', name: 'is-or-less'},
|
||||||
|
{label: 'after', name: 'is-greater'},
|
||||||
|
{label: 'on or after', name: 'is-or-greater'}
|
||||||
|
];
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './contains';
|
||||||
|
export * from './match';
|
||||||
|
export * from './date';
|
||||||
|
export * from './number';
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const MATCH_RELATION_OPTIONS = [
|
||||||
|
{label: 'is', name: 'is'},
|
||||||
|
{label: 'is not', name: 'is-not'}
|
||||||
|
];
|
|
@ -0,0 +1,5 @@
|
||||||
|
export const NUMBER_RELATION_OPTIONS = [
|
||||||
|
{label: 'is', name: 'is'},
|
||||||
|
{label: 'is greater than', name: 'is-greater'},
|
||||||
|
{label: 'is less than', name: 'is-less'}
|
||||||
|
];
|
|
@ -0,0 +1,16 @@
|
||||||
|
import {MATCH_RELATION_OPTIONS} from './relation-options';
|
||||||
|
|
||||||
|
export const SIGNUP_ATTRIBUTION_FILTER = {
|
||||||
|
label: 'Signed up on post/page',
|
||||||
|
name: 'signup',
|
||||||
|
valueType: 'string',
|
||||||
|
resource: 'post',
|
||||||
|
relationOptions: MATCH_RELATION_OPTIONS,
|
||||||
|
columnLabel: 'Signed up on',
|
||||||
|
setting: 'membersTrackSources',
|
||||||
|
getColumnValue: (member, filter) => {
|
||||||
|
return {
|
||||||
|
text: filter.resource?.title ?? ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
13
ghost/admin/app/components/members/filters/status.js
Normal file
13
ghost/admin/app/components/members/filters/status.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MATCH_RELATION_OPTIONS} from './relation-options';
|
||||||
|
|
||||||
|
export const STATUS_FILTER = {
|
||||||
|
label: 'Member status',
|
||||||
|
name: 'status',
|
||||||
|
relationOptions: MATCH_RELATION_OPTIONS,
|
||||||
|
valueType: 'options',
|
||||||
|
options: [
|
||||||
|
{label: 'Paid', name: 'paid'},
|
||||||
|
{label: 'Free', name: 'free'},
|
||||||
|
{label: 'Complimentary', name: 'comped'}
|
||||||
|
]
|
||||||
|
};
|
18
ghost/admin/app/components/members/filters/subscribed.js
Normal file
18
ghost/admin/app/components/members/filters/subscribed.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import {MATCH_RELATION_OPTIONS} from './relation-options';
|
||||||
|
|
||||||
|
export const SUBSCRIBED_FILTER = {
|
||||||
|
label: 'Newsletter subscription',
|
||||||
|
name: 'subscribed',
|
||||||
|
columnLabel: 'Subscribed',
|
||||||
|
relationOptions: MATCH_RELATION_OPTIONS,
|
||||||
|
valueType: 'options',
|
||||||
|
options: [
|
||||||
|
{label: 'Subscribed', name: 'true'},
|
||||||
|
{label: 'Unsubscribed', name: 'false'}
|
||||||
|
],
|
||||||
|
getColumnValue: (member) => {
|
||||||
|
return {
|
||||||
|
text: member.subscribed ? 'Yes' : 'No'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,16 @@
|
||||||
|
import {MATCH_RELATION_OPTIONS} from './relation-options';
|
||||||
|
|
||||||
|
export const SUBSCRIPTION_ATTRIBUTION_FILTER = {
|
||||||
|
label: 'Subscription started on post/page',
|
||||||
|
name: 'conversion',
|
||||||
|
valueType: 'string',
|
||||||
|
resource: 'post',
|
||||||
|
relationOptions: MATCH_RELATION_OPTIONS,
|
||||||
|
columnLabel: 'Subscription started on',
|
||||||
|
setting: 'membersTrackSources',
|
||||||
|
getColumnValue: (member, filter) => {
|
||||||
|
return {
|
||||||
|
text: filter.resource?.title ?? ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
import {DATE_RELATION_OPTIONS} from './relation-options';
|
||||||
|
import {getDateColumnValue} from './columns/date-column';
|
||||||
|
import {mostRecentlyUpdated} from 'ghost-admin/helpers/most-recently-updated';
|
||||||
|
|
||||||
|
export const SUBSCRIPTION_START_DATE_FILTER = {
|
||||||
|
label: 'Paid start date',
|
||||||
|
name: 'subscriptions.start_date',
|
||||||
|
valueType: 'date',
|
||||||
|
columnLabel: 'Paid start date',
|
||||||
|
relationOptions: DATE_RELATION_OPTIONS,
|
||||||
|
getColumnValue: (member, filter) => {
|
||||||
|
const subscription = mostRecentlyUpdated(member.subscriptions);
|
||||||
|
return getDateColumnValue(subscription?.start_date, filter);
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,29 @@
|
||||||
|
import {MATCH_RELATION_OPTIONS} from './relation-options';
|
||||||
|
import {capitalizeFirstLetter} from 'ghost-admin/helpers/capitalize-first-letter';
|
||||||
|
import {mostRecentlyUpdated} from 'ghost-admin/helpers/most-recently-updated';
|
||||||
|
|
||||||
|
export const SUBSCRIPTION_STATUS_FILTER = {
|
||||||
|
label: 'Stripe subscription status',
|
||||||
|
name: 'subscriptions.status',
|
||||||
|
columnLabel: 'Subscription Status',
|
||||||
|
relationOptions: MATCH_RELATION_OPTIONS,
|
||||||
|
valueType: 'options',
|
||||||
|
options: [
|
||||||
|
{label: 'Active', name: 'active'},
|
||||||
|
{label: 'Trialing', name: 'trialing'},
|
||||||
|
{label: 'Canceled', name: 'canceled'},
|
||||||
|
{label: 'Unpaid', name: 'unpaid'},
|
||||||
|
{label: 'Past Due', name: 'past_due'},
|
||||||
|
{label: 'Incomplete', name: 'incomplete'},
|
||||||
|
{label: 'Incomplete - Expired', name: 'incomplete_expired'}
|
||||||
|
],
|
||||||
|
getColumnValue: (member) => {
|
||||||
|
const subscription = mostRecentlyUpdated(member.subscriptions);
|
||||||
|
if (!subscription) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: capitalizeFirstLetter(subscription.status)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
15
ghost/admin/app/components/members/filters/tier.js
Normal file
15
ghost/admin/app/components/members/filters/tier.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import {MATCH_RELATION_OPTIONS} from './relation-options';
|
||||||
|
|
||||||
|
export const TIER_FILTER = {
|
||||||
|
label: 'Membership tier',
|
||||||
|
name: 'tier',
|
||||||
|
valueType: 'array',
|
||||||
|
columnLabel: 'Membership tier',
|
||||||
|
relationOptions: MATCH_RELATION_OPTIONS,
|
||||||
|
getColumnValue: (member) => {
|
||||||
|
return {
|
||||||
|
class: 'gh-members-list-labels',
|
||||||
|
text: (member.tiers ?? []).map(label => label.name).join(', ')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,98 +1,15 @@
|
||||||
{{#if (eq this.columnName 'label')}}
|
<LinkTo @route="member" @model={{@member}} class="gh-list-data middarkgrey f8" data-test-table-data={{this.columnName}}>
|
||||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data wrap middarkgrey f8" data-test-table-data={{this.columnName}}>
|
{{#if this.columnValue}}
|
||||||
<span class="gh-members-list-labels">{{this.labels}}</span>
|
<div class={{this.columnValue.class}}>
|
||||||
</LinkTo>
|
{{#if this.columnValue.icon}}
|
||||||
|
{{svg-jar this.columnValue.icon}}
|
||||||
{{else if (eq this.columnName 'tier')}}
|
{{/if}}
|
||||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data wrap middarkgrey f8" data-test-table-data={{this.columnName}}>
|
<span>{{this.columnValue.text}}</span>
|
||||||
<span class="gh-members-list-labels">{{this.tiers}}</span>
|
{{#if this.columnValue.subtext}}
|
||||||
</LinkTo>
|
<div class="midlightgrey {{this.columnValue.subtextClass}}">{{this.columnValue.subtext}}</div>
|
||||||
|
{{/if}}
|
||||||
{{else if (eq this.columnName 'last_seen_at')}}
|
</div>
|
||||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data middarkgrey f8" data-test-table-data={{this.columnName}}>
|
{{else}}
|
||||||
{{#if (not (is-empty @member.lastSeenAtUTC))}}
|
<span class="midlightgrey">-</span>
|
||||||
{{moment-format (moment-site-tz @member.lastSeenAtUTC) "DD MMM YYYY"}}
|
{{/if}}
|
||||||
<div class="midlightgrey gh-members-list-subscribed-moment">{{moment-from-now @member.lastSeenAtUTC}}</div>
|
</LinkTo>
|
||||||
{{else}}
|
|
||||||
<span class="midlightgrey">-</span>
|
|
||||||
{{/if}}
|
|
||||||
</LinkTo>
|
|
||||||
|
|
||||||
{{else if (eq this.columnName 'email_count')}}
|
|
||||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data middarkgrey f8" data-test-table-data={{this.columnName}}>
|
|
||||||
{{#if (not (is-empty @member.emailCount))}}
|
|
||||||
<span>{{@member.emailCount}}</span>
|
|
||||||
{{else}}
|
|
||||||
<span class="midlightgrey">-</span>
|
|
||||||
{{/if}}
|
|
||||||
</LinkTo>
|
|
||||||
|
|
||||||
{{else if (eq this.columnName 'email_opened_count')}}
|
|
||||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data middarkgrey f8" data-test-table-data={{this.columnName}}>
|
|
||||||
{{#if (not (is-empty @member.emailOpenedCount))}}
|
|
||||||
<span>{{@member.emailOpenedCount}}</span>
|
|
||||||
{{else}}
|
|
||||||
<span class="midlightgrey">-</span>
|
|
||||||
{{/if}}
|
|
||||||
</LinkTo>
|
|
||||||
|
|
||||||
{{else if (eq this.columnName 'subscribed')}}
|
|
||||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data middarkgrey f8" data-test-table-data={{this.columnName}}>
|
|
||||||
{{#if (not (is-empty @member.subscribed))}}
|
|
||||||
<span>{{if @member.subscribed "Yes" "No"}}</span>
|
|
||||||
{{else}}
|
|
||||||
<span class="midlightgrey">-</span>
|
|
||||||
{{/if}}
|
|
||||||
</LinkTo>
|
|
||||||
|
|
||||||
{{else if (eq this.columnName 'subscriptions.status')}}
|
|
||||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data middarkgrey f8" data-test-table-data={{this.columnName}}>
|
|
||||||
{{#if (not (is-empty this.mostRecentSubscription.status))}}
|
|
||||||
<span>{{capitalize this.mostRecentSubscription.status}}</span>
|
|
||||||
{{else}}
|
|
||||||
<span class="midlightgrey">-</span>
|
|
||||||
{{/if}}
|
|
||||||
</LinkTo>
|
|
||||||
|
|
||||||
{{else if (eq this.columnName 'subscriptions.plan_interval')}}
|
|
||||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data middarkgrey f8" data-test-table-data={{this.columnName}}>
|
|
||||||
{{#if (not (is-empty this.mostRecentSubscription.price.interval))}}
|
|
||||||
<span>{{capitalize this.mostRecentSubscription.price.interval}}</span>
|
|
||||||
{{else}}
|
|
||||||
<span class="midlightgrey">-</span>
|
|
||||||
{{/if}}
|
|
||||||
</LinkTo>
|
|
||||||
|
|
||||||
{{else if (eq this.columnName 'subscriptions.start_date')}}
|
|
||||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data middarkgrey f8" data-test-table-data={{this.columnName}}>
|
|
||||||
{{#if (not (is-empty this.mostRecentSubscription.start_date))}}
|
|
||||||
{{moment-format (moment-site-tz this.mostRecentSubscription.start_date) "DD MMM YYYY"}}
|
|
||||||
<div class="midlightgrey gh-members-list-subscribed-moment">{{moment-from-now this.mostRecentSubscription.start_date}}</div>
|
|
||||||
{{else}}
|
|
||||||
<span class="midlightgrey">-</span>
|
|
||||||
{{/if}}
|
|
||||||
</LinkTo>
|
|
||||||
|
|
||||||
{{else if (eq this.columnName 'subscriptions.current_period_end')}}
|
|
||||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data middarkgrey f8" data-test-table-data={{this.columnName}}>
|
|
||||||
{{#if (not (is-empty this.mostRecentSubscription.current_period_end))}}
|
|
||||||
{{moment-format (moment-site-tz this.mostRecentSubscription.current_period_end) "DD MMM YYYY"}}
|
|
||||||
<div class="midlightgrey gh-members-list-subscribed-moment">{{moment-from-now this.mostRecentSubscription.current_period_end}}</div>
|
|
||||||
{{else}}
|
|
||||||
<span class="midlightgrey">-</span>
|
|
||||||
{{/if}}
|
|
||||||
</LinkTo>
|
|
||||||
{{else}}
|
|
||||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data middarkgrey f8" data-test-table-data={{this.columnName}}>
|
|
||||||
{{#if this.columnValue}}
|
|
||||||
<div class={{this.columnValue.class}}>
|
|
||||||
{{#if this.columnValue.icon}}
|
|
||||||
{{svg-jar this.columnValue.icon}}
|
|
||||||
{{/if}}
|
|
||||||
<span>{{this.columnValue.text}}</span>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<span class="midlightgrey">-</span>
|
|
||||||
{{/if}}
|
|
||||||
</LinkTo>
|
|
||||||
{{/if}}
|
|
||||||
|
|
|
@ -1,26 +1,10 @@
|
||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import {get} from '@ember/object';
|
|
||||||
import {mostRecentlyUpdated} from 'ghost-admin/helpers/most-recently-updated';
|
|
||||||
|
|
||||||
export default class MembersListItemColumn extends Component {
|
export default class MembersListItemColumn extends Component {
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
get labels() {
|
|
||||||
const labelData = get(this.args.member, 'labels') || [];
|
|
||||||
return labelData.map(label => label.name).join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
get tiers() {
|
|
||||||
const tierData = get(this.args.member, 'tiers') || [];
|
|
||||||
return tierData.map(tier => tier.name).join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
get mostRecentSubscription() {
|
|
||||||
return mostRecentlyUpdated(get(this.args.member, 'subscriptions'));
|
|
||||||
}
|
|
||||||
|
|
||||||
get columnName() {
|
get columnName() {
|
||||||
return this.args.filterColumn.name;
|
return this.args.filterColumn.name;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,15 +29,13 @@
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if @newsletterEnabled}}
|
{{#if @newsletterEnabled}}
|
||||||
{{#if (feature "emailAnalytics")}}
|
<LinkTo @route="member" @model={{@member}} class="gh-list-data middarkgrey f8 {{unless @member.name "gh-members-list-open-rate-noname"}}" data-test-table-data="open-rate">
|
||||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data middarkgrey f8 {{unless @member.name "gh-members-list-open-rate-noname"}}" data-test-table-data="open-rate">
|
{{#if (not (is-empty @member.emailOpenRate))}}
|
||||||
{{#if (not (is-empty @member.emailOpenRate))}}
|
<span>{{@member.emailOpenRate}}%</span>
|
||||||
<span>{{@member.emailOpenRate}}%</span>
|
{{else}}
|
||||||
{{else}}
|
<span class="midlightgrey">N/A</span>
|
||||||
<span class="midlightgrey">N/A</span>
|
{{/if}}
|
||||||
{{/if}}
|
</LinkTo>
|
||||||
</LinkTo>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data middarkgrey f8 {{unless @member.name "gh-members-geolocation-noname"}}" data-test-table-data="location">
|
<LinkTo @route="member" @model={{@member}} class="gh-list-data middarkgrey f8 {{unless @member.name "gh-members-geolocation-noname"}}" data-test-table-data="location">
|
||||||
|
|
|
@ -176,14 +176,19 @@ export default class MembersController extends Controller {
|
||||||
return this.availableFilters.flatMap((filter) => {
|
return this.availableFilters.flatMap((filter) => {
|
||||||
if (filter.properties?.getColumns) {
|
if (filter.properties?.getColumns) {
|
||||||
return filter.properties?.getColumns(filter).map((c) => {
|
return filter.properties?.getColumns(filter).map((c) => {
|
||||||
return {...c, name: filter.type};
|
return {
|
||||||
|
label: filter.properties.columnLabel, // default value if not provided
|
||||||
|
...c,
|
||||||
|
name: filter.type
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (filter.properties?.columnLabel) {
|
if (filter.properties?.columnLabel) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: filter.type,
|
name: filter.type,
|
||||||
label: filter.properties.columnLabel
|
label: filter.properties.columnLabel,
|
||||||
|
getValue: filter.properties.getColumnValue ? (member => filter.properties.getColumnValue(member, filter)) : null
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,6 +155,7 @@ p.gh-members-list-email {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
|
white-space: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-members-list-feedback{
|
.gh-members-list-feedback{
|
||||||
|
|
|
@ -129,7 +129,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{this.listHeader}}</th>
|
<th>{{this.listHeader}}</th>
|
||||||
<th data-test-table-column="status">Status</th>
|
<th data-test-table-column="status">Status</th>
|
||||||
{{#if (not-eq this.settings.editorDefaultEmailRecipients "disabled")}}
|
{{#if (and (not-eq this.settings.editorDefaultEmailRecipients "disabled") this.settings.emailTrackOpens)}}
|
||||||
<th data-test-table-column="email_open_rate">Open rate</th>
|
<th data-test-table-column="email_open_rate">Open rate</th>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<th data-test-table-column="location">Location</th>
|
<th data-test-table-column="location">Location</th>
|
||||||
|
@ -142,12 +142,12 @@
|
||||||
<VerticalCollection @tagName="tbody" @items={{this.members}} @key="id" @containerSelector=".gh-list-scrolling" @estimateHeight={{69}} @staticHeight={{true}} @bufferSize={{20}} as |member|>
|
<VerticalCollection @tagName="tbody" @items={{this.members}} @key="id" @containerSelector=".gh-list-scrolling" @estimateHeight={{69}} @staticHeight={{true}} @bufferSize={{20}} as |member|>
|
||||||
{{#if member.is_loading}}
|
{{#if member.is_loading}}
|
||||||
<Members::ListItemLoading
|
<Members::ListItemLoading
|
||||||
@newsletterEnabled={{not-eq this.settings.editorDefaultEmailRecipients "disabled"}}
|
@newsletterEnabled={{and (not-eq this.settings.editorDefaultEmailRecipients "disabled") this.settings.emailTrackOpens}}
|
||||||
@filterColumns={{this.filterColumns}}
|
@filterColumns={{this.filterColumns}}
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
<Members::ListItem
|
<Members::ListItem
|
||||||
@newsletterEnabled={{not-eq this.settings.editorDefaultEmailRecipients "disabled"}}
|
@newsletterEnabled={{and (not-eq this.settings.editorDefaultEmailRecipients "disabled") this.settings.emailTrackOpens}}
|
||||||
@member={{member.content}}
|
@member={{member.content}}
|
||||||
@filterColumns={{this.filterColumns}}
|
@filterColumns={{this.filterColumns}}
|
||||||
data-test-member={{member.id}}
|
data-test-member={{member.id}}
|
||||||
|
|
Loading…
Add table
Reference in a new issue