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:
Simon Backx 2022-11-10 11:05:12 +01:00 committed by GitHub
parent c2dfb2b579
commit b821c84b9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 481 additions and 441 deletions

View File

@ -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

View File

@ -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'
};
}
}
]
};

View File

@ -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'
};
}

View 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
};

View 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 ?? ''
};
}
};

View 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)
};
}
};

View File

@ -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
};

View File

@ -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)
};
}
};

View 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 ?? ''
};
}
};

View 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 ?? ''
};
}
};

View 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
};

View 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';

View 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(', ')
};
}
};

View 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);
}
};

View 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
};

View File

@ -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);
}
};

View 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)
};
}
};

View File

@ -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'}
];

View File

@ -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'}
];

View File

@ -0,0 +1,4 @@
export * from './contains';
export * from './match';
export * from './date';
export * from './number';

View File

@ -0,0 +1,4 @@
export const MATCH_RELATION_OPTIONS = [
{label: 'is', name: 'is'},
{label: 'is not', name: 'is-not'}
];

View File

@ -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'}
];

View File

@ -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 ?? ''
};
}
};

View 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'}
]
};

View 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'
};
}
};

View File

@ -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 ?? ''
};
}
};

View File

@ -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);
}
};

View File

@ -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)
};
}
};

View 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(', ')
};
}
};

View File

@ -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>

View File

@ -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;
}

View File

@ -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">

View File

@ -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
}
];
}

View File

@ -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{

View File

@ -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}}