mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-24 06:35:49 +03: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
@ -1,6 +1,7 @@
|
||||
import Component from '@glimmer/component';
|
||||
import moment from 'moment-timezone';
|
||||
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 {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
@ -11,329 +12,50 @@ function escapeNqlString(value) {
|
||||
return '\'' + value.replace(/'/g, '\\\'') + '\'';
|
||||
}
|
||||
|
||||
const MATCH_RELATION_OPTIONS = [
|
||||
{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,
|
||||
const FILTER_GROUPS = [
|
||||
{
|
||||
label: 'Email',
|
||||
name: 'email',
|
||||
group: 'Basic',
|
||||
valueType: 'string',
|
||||
relationOptions: CONTAINS_RELATION_OPTIONS
|
||||
},
|
||||
{
|
||||
label: 'Label',
|
||||
name: 'label',
|
||||
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'}
|
||||
name: 'Basic',
|
||||
filters: [
|
||||
NAME_FILTER,
|
||||
EMAIL_FILTER,
|
||||
LABEL_FILTER,
|
||||
SUBSCRIBED_FILTER,
|
||||
LAST_SEEN_FILTER,
|
||||
CREATED_AT_FILTER,
|
||||
SIGNUP_ATTRIBUTION_FILTER
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Last seen',
|
||||
name: 'last_seen_at',
|
||||
group: 'Basic',
|
||||
valueType: 'date',
|
||||
columnLabel: 'Last seen at',
|
||||
relationOptions: DATE_RELATION_OPTIONS
|
||||
},
|
||||
{
|
||||
label: 'Created',
|
||||
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'}
|
||||
name: 'Subscription',
|
||||
filters: [
|
||||
TIER_FILTER,
|
||||
STATUS_FILTER,
|
||||
PLAN_INTERVAL_FILTER,
|
||||
SUBSCRIPTION_STATUS_FILTER,
|
||||
SUBSCRIPTION_START_DATE_FILTER,
|
||||
NEXT_BILLING_DATE_FILTER,
|
||||
SUBSCRIPTION_ATTRIBUTION_FILTER
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Billing period',
|
||||
name: 'subscriptions.plan_interval',
|
||||
group: 'Subscription',
|
||||
columnLabel: 'Billing period',
|
||||
relationOptions: MATCH_RELATION_OPTIONS,
|
||||
valueType: 'options',
|
||||
options: [
|
||||
{label: 'Monthly', name: 'month'},
|
||||
{label: 'Yearly', name: 'year'}
|
||||
]
|
||||
},
|
||||
{
|
||||
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'
|
||||
};
|
||||
}
|
||||
}
|
||||
name: 'Email',
|
||||
filters: [
|
||||
EMAIL_COUNT_FILTER,
|
||||
EMAIL_OPENED_COUNT_FILTER,
|
||||
EMAIL_OPEN_RATE_FILTER,
|
||||
EMAIL_RECEIVED_FILTER,
|
||||
EMAIL_OPENED_FILTER,
|
||||
EMAIL_CLICKED_FILTER,
|
||||
AUDIENCE_FEEDBACK_FILTER
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const FILTER_PROPERTIES = FILTER_GROUPS.flatMap(group => group.filters.map((f) => {
|
||||
f.group = group.name;
|
||||
return f;
|
||||
}));
|
||||
|
||||
class Filter {
|
||||
@tracked value;
|
||||
@tracked relation;
|
||||
@ -413,6 +135,7 @@ export default class MembersFilter extends Component {
|
||||
@service session;
|
||||
@service settings;
|
||||
@service store;
|
||||
@service membersUtils;
|
||||
|
||||
@tracked filters = new TrackedArray([
|
||||
new Filter({
|
||||
@ -422,10 +145,11 @@ export default class MembersFilter extends Component {
|
||||
|
||||
get availableFilterProperties() {
|
||||
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
|
||||
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
|
||||
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 wrap middarkgrey f8" data-test-table-data={{this.columnName}}>
|
||||
<span class="gh-members-list-labels">{{this.labels}}</span>
|
||||
</LinkTo>
|
||||
|
||||
{{else if (eq this.columnName 'tier')}}
|
||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data wrap middarkgrey f8" data-test-table-data={{this.columnName}}>
|
||||
<span class="gh-members-list-labels">{{this.tiers}}</span>
|
||||
</LinkTo>
|
||||
|
||||
{{else if (eq this.columnName 'last_seen_at')}}
|
||||
<LinkTo @route="member" @model={{@member}} class="gh-list-data middarkgrey f8" data-test-table-data={{this.columnName}}>
|
||||
{{#if (not (is-empty @member.lastSeenAtUTC))}}
|
||||
{{moment-format (moment-site-tz @member.lastSeenAtUTC) "DD MMM YYYY"}}
|
||||
<div class="midlightgrey gh-members-list-subscribed-moment">{{moment-from-now @member.lastSeenAtUTC}}</div>
|
||||
{{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}}
|
||||
<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>
|
||||
{{#if this.columnValue.subtext}}
|
||||
<div class="midlightgrey {{this.columnValue.subtextClass}}">{{this.columnValue.subtext}}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else}}
|
||||
<span class="midlightgrey">-</span>
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
|
@ -1,26 +1,10 @@
|
||||
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 {
|
||||
constructor(...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() {
|
||||
return this.args.filterColumn.name;
|
||||
}
|
||||
|
@ -29,15 +29,13 @@
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
{{#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">
|
||||
{{#if (not (is-empty @member.emailOpenRate))}}
|
||||
<span>{{@member.emailOpenRate}}%</span>
|
||||
{{else}}
|
||||
<span class="midlightgrey">N/A</span>
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
<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))}}
|
||||
<span>{{@member.emailOpenRate}}%</span>
|
||||
{{else}}
|
||||
<span class="midlightgrey">N/A</span>
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
|
||||
<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) => {
|
||||
if (filter.properties?.getColumns) {
|
||||
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) {
|
||||
return [
|
||||
{
|
||||
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;
|
||||
max-width: 300px;
|
||||
min-width: 220px;
|
||||
white-space: wrap;
|
||||
}
|
||||
|
||||
.gh-members-list-feedback{
|
||||
|
@ -129,7 +129,7 @@
|
||||
<tr>
|
||||
<th>{{this.listHeader}}</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>
|
||||
{{/if}}
|
||||
<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|>
|
||||
{{#if member.is_loading}}
|
||||
<Members::ListItemLoading
|
||||
@newsletterEnabled={{not-eq this.settings.editorDefaultEmailRecipients "disabled"}}
|
||||
@newsletterEnabled={{and (not-eq this.settings.editorDefaultEmailRecipients "disabled") this.settings.emailTrackOpens}}
|
||||
@filterColumns={{this.filterColumns}}
|
||||
/>
|
||||
{{else}}
|
||||
<Members::ListItem
|
||||
@newsletterEnabled={{not-eq this.settings.editorDefaultEmailRecipients "disabled"}}
|
||||
@newsletterEnabled={{and (not-eq this.settings.editorDefaultEmailRecipients "disabled") this.settings.emailTrackOpens}}
|
||||
@member={{member.content}}
|
||||
@filterColumns={{this.filterColumns}}
|
||||
data-test-member={{member.id}}
|
||||
|
Loading…
Reference in New Issue
Block a user