diff --git a/ghost/admin/README.md b/ghost/admin/README.md index 73a49b566b..04fa03e8ed 100644 --- a/ghost/admin/README.md +++ b/ghost/admin/README.md @@ -12,7 +12,7 @@ Run all tests in the browser by running `yarn dev` in the Ghost monorepo and vis --- -Tip: You can use `await this.pauseTest()` in your tests to temporarily pause the execution of browser tests. Use the browser console to inspect and debug the DOM, then resume tests by running `resumeTest()` directly in the browser console ([docs](https://guides.emberjs.com/v3.28.0/testing/testing-application/#toc_debugging-your-tests)) +Tip: You can use `this.timeout(0); await this.pauseTest();` in your tests to temporarily pause the execution of browser tests. Use the browser console to inspect and debug the DOM, then resume tests by running `resumeTest()` directly in the browser console ([docs](https://guides.emberjs.com/v3.28.0/testing/testing-application/#toc_debugging-your-tests)) ### Running tests in the CLI diff --git a/ghost/admin/app/components/members/filter.js b/ghost/admin/app/components/members/filter.js index 6993d1c4e7..46da839176 100644 --- a/ghost/admin/app/components/members/filter.js +++ b/ghost/admin/app/components/members/filter.js @@ -137,6 +137,10 @@ class Filter { return this.properties.options ?? []; } + get group() { + return this.properties.group; + } + get isValid() { if (Array.isArray(this.value)) { return !!this.value.length; diff --git a/ghost/admin/app/components/members/filters/created-at.js b/ghost/admin/app/components/members/filters/created-at.js index 9ded43af78..f857771115 100644 --- a/ghost/admin/app/components/members/filters/created-at.js +++ b/ghost/admin/app/components/members/filters/created-at.js @@ -1,8 +1,8 @@ import {DATE_RELATION_OPTIONS} from './relation-options'; export const CREATED_AT_FILTER = { - label: 'Created', - name: 'created_at', - valueType: 'date', + label: 'Created', + name: 'created_at', + valueType: 'date', relationOptions: DATE_RELATION_OPTIONS }; diff --git a/ghost/admin/app/components/members/filters/email-clicked.js b/ghost/admin/app/components/members/filters/email-clicked.js index d24798b197..c78dae0c41 100644 --- a/ghost/admin/app/components/members/filters/email-clicked.js +++ b/ghost/admin/app/components/members/filters/email-clicked.js @@ -1,10 +1,10 @@ import {MATCH_RELATION_OPTIONS} from './relation-options'; export const EMAIL_CLICKED_FILTER = { - label: 'Clicked email', - name: 'clicked_links.post_id', - valueType: 'string', - resource: 'email', + label: 'Clicked email', + name: 'clicked_links.post_id', + valueType: 'string', + resource: 'email', relationOptions: MATCH_RELATION_OPTIONS, columnLabel: 'Clicked email', setting: 'emailTrackClicks', diff --git a/ghost/admin/app/components/members/filters/email-count.js b/ghost/admin/app/components/members/filters/email-count.js index ef8396c574..03a0a4242f 100644 --- a/ghost/admin/app/components/members/filters/email-count.js +++ b/ghost/admin/app/components/members/filters/email-count.js @@ -2,10 +2,10 @@ 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', + label: 'Emails sent (all time)', + name: 'email_count', + columnLabel: 'Email count', + valueType: 'number', relationOptions: NUMBER_RELATION_OPTIONS, getColumnValue: (member) => { return { diff --git a/ghost/admin/app/components/members/filters/email-open-rate.js b/ghost/admin/app/components/members/filters/email-open-rate.js index 5990199a4d..6361239b05 100644 --- a/ghost/admin/app/components/members/filters/email-open-rate.js +++ b/ghost/admin/app/components/members/filters/email-open-rate.js @@ -1,9 +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', + label: 'Open rate (all time)', + name: 'email_open_rate', + valueType: 'number', setting: 'emailTrackOpens', relationOptions: NUMBER_RELATION_OPTIONS }; diff --git a/ghost/admin/app/components/members/filters/email-opened-count.js b/ghost/admin/app/components/members/filters/email-opened-count.js index ff4d2e3363..dece4042ad 100644 --- a/ghost/admin/app/components/members/filters/email-opened-count.js +++ b/ghost/admin/app/components/members/filters/email-opened-count.js @@ -2,10 +2,10 @@ 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', + label: 'Emails opened (all time)', + name: 'email_opened_count', + columnLabel: 'Email opened count', + valueType: 'number', relationOptions: NUMBER_RELATION_OPTIONS, getColumnValue: (member) => { return { diff --git a/ghost/admin/app/components/members/filters/email-opened.js b/ghost/admin/app/components/members/filters/email-opened.js index 5f284e16a5..d9535523aa 100644 --- a/ghost/admin/app/components/members/filters/email-opened.js +++ b/ghost/admin/app/components/members/filters/email-opened.js @@ -1,10 +1,10 @@ import {MATCH_RELATION_OPTIONS} from './relation-options'; export const EMAIL_OPENED_FILTER = { - label: 'Opened email', - name: 'opened_emails.post_id', - valueType: 'string', - resource: 'email', + label: 'Opened email', + name: 'opened_emails.post_id', + valueType: 'string', + resource: 'email', relationOptions: MATCH_RELATION_OPTIONS, columnLabel: 'Opened email', setting: 'emailTrackOpens', diff --git a/ghost/admin/app/components/members/filters/email.js b/ghost/admin/app/components/members/filters/email.js index 3645374c34..1aa094a395 100644 --- a/ghost/admin/app/components/members/filters/email.js +++ b/ghost/admin/app/components/members/filters/email.js @@ -1,8 +1,8 @@ import {CONTAINS_RELATION_OPTIONS} from './relation-options'; export const EMAIL_FILTER = { - label: 'Email', + label: 'Email', name: 'email', - valueType: 'string', + valueType: 'string', relationOptions: CONTAINS_RELATION_OPTIONS }; diff --git a/ghost/admin/app/components/members/filters/label.js b/ghost/admin/app/components/members/filters/label.js index 7538d60df5..857845a2db 100644 --- a/ghost/admin/app/components/members/filters/label.js +++ b/ghost/admin/app/components/members/filters/label.js @@ -1,10 +1,10 @@ import {MATCH_RELATION_OPTIONS} from './relation-options'; export const LABEL_FILTER = { - label: 'Label', - name: 'label', - valueType: 'array', - columnLabel: 'Label', + label: 'Label', + name: 'label', + valueType: 'array', + columnLabel: 'Label', relationOptions: MATCH_RELATION_OPTIONS, getColumnValue: (member) => { return { diff --git a/ghost/admin/app/components/members/filters/last-seen.js b/ghost/admin/app/components/members/filters/last-seen.js index 22eb950daf..d0667dbda3 100644 --- a/ghost/admin/app/components/members/filters/last-seen.js +++ b/ghost/admin/app/components/members/filters/last-seen.js @@ -2,10 +2,10 @@ 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', + 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); diff --git a/ghost/admin/app/components/members/filters/name.js b/ghost/admin/app/components/members/filters/name.js index 875d50e5fa..c5571d5567 100644 --- a/ghost/admin/app/components/members/filters/name.js +++ b/ghost/admin/app/components/members/filters/name.js @@ -1,8 +1,8 @@ import {CONTAINS_RELATION_OPTIONS} from './relation-options'; export const NAME_FILTER = { - label: 'Name', - name: 'name', - valueType: 'string', + label: 'Name', + name: 'name', + valueType: 'string', relationOptions: CONTAINS_RELATION_OPTIONS }; diff --git a/ghost/admin/app/components/members/filters/offers.js b/ghost/admin/app/components/members/filters/offers.js index 0039f04c19..854c46e4e7 100644 --- a/ghost/admin/app/components/members/filters/offers.js +++ b/ghost/admin/app/components/members/filters/offers.js @@ -1,7 +1,7 @@ import {MATCH_RELATION_OPTIONS} from './relation-options'; export const OFFERS_FILTER = { - label: 'Offers', + label: 'Offers', name: 'offer_redemptions', group: 'Subscription', relationOptions: MATCH_RELATION_OPTIONS, diff --git a/ghost/admin/app/components/members/filters/signup-attribution.js b/ghost/admin/app/components/members/filters/signup-attribution.js index 49a396e071..6bcf2b2762 100644 --- a/ghost/admin/app/components/members/filters/signup-attribution.js +++ b/ghost/admin/app/components/members/filters/signup-attribution.js @@ -1,10 +1,10 @@ import {MATCH_RELATION_OPTIONS} from './relation-options'; export const SIGNUP_ATTRIBUTION_FILTER = { - label: 'Signed up on post/page', - name: 'signup', - valueType: 'string', - resource: 'post', + label: 'Signed up on post/page', + name: 'signup', + valueType: 'string', + resource: 'post', relationOptions: MATCH_RELATION_OPTIONS, columnLabel: 'Signed up on', setting: 'membersTrackSources', diff --git a/ghost/admin/app/components/members/filters/status.js b/ghost/admin/app/components/members/filters/status.js index e6d31dd697..08cbbef1a9 100644 --- a/ghost/admin/app/components/members/filters/status.js +++ b/ghost/admin/app/components/members/filters/status.js @@ -1,8 +1,8 @@ import {MATCH_RELATION_OPTIONS} from './relation-options'; export const STATUS_FILTER = { - label: 'Member status', - name: 'status', + label: 'Member status', + name: 'status', relationOptions: MATCH_RELATION_OPTIONS, valueType: 'options', options: [ diff --git a/ghost/admin/app/components/members/filters/subscription-attribution.js b/ghost/admin/app/components/members/filters/subscription-attribution.js index 84688dac5d..373b51c0b3 100644 --- a/ghost/admin/app/components/members/filters/subscription-attribution.js +++ b/ghost/admin/app/components/members/filters/subscription-attribution.js @@ -1,10 +1,10 @@ import {MATCH_RELATION_OPTIONS} from './relation-options'; export const SUBSCRIPTION_ATTRIBUTION_FILTER = { - label: 'Subscription started on post/page', - name: 'conversion', - valueType: 'string', - resource: 'post', + label: 'Subscription started on post/page', + name: 'conversion', + valueType: 'string', + resource: 'post', relationOptions: MATCH_RELATION_OPTIONS, columnLabel: 'Subscription started on', setting: 'membersTrackSources', diff --git a/ghost/admin/app/components/members/filters/tier.js b/ghost/admin/app/components/members/filters/tier.js index b219d0fc4f..bbd1c00e2d 100644 --- a/ghost/admin/app/components/members/filters/tier.js +++ b/ghost/admin/app/components/members/filters/tier.js @@ -1,10 +1,10 @@ import {MATCH_RELATION_OPTIONS} from './relation-options'; export const TIER_FILTER = { - label: 'Membership tier', - name: 'tier_id', - valueType: 'array', - columnLabel: 'Membership tier', + label: 'Membership tier', + name: 'tier_id', + valueType: 'array', + columnLabel: 'Membership tier', relationOptions: MATCH_RELATION_OPTIONS, getColumnValue: (member) => { return { diff --git a/ghost/admin/app/controllers/members.js b/ghost/admin/app/controllers/members.js index 1270b5a06a..9279418d23 100644 --- a/ghost/admin/app/controllers/members.js +++ b/ghost/admin/app/controllers/members.js @@ -209,8 +209,44 @@ export default class MembersController extends Controller { return uniqueColumns.splice(0, 2); // Maximum 2 columns } - get isMultiFiltered() { - return this.isFiltered && this.filters.length >= 2; + /* Due to a limitation with NQL when multiple member filters are used in combination, we currently have a safeguard around member bulk deletion. + * Member bulk deletion is not permitted when: + * 1) Multiple newsletters exist, and 2 or more newsletter filters are in use + * 2) If any of the following Stripe filters are used, even once: + * - Billing period + * - Stripe subscription status + * - Paid start date + * - Next billing date + * - Subscription started on post/page + * - Offers + * + * See issue https://linear.app/tryghost/issue/ENG-1484 for more context + */ + get isBulkDeletePermitted() { + if (!this.isFiltered) { + return false; + } + + const newsletterFilters = this.filters.filter(f => f.group === 'Newsletters'); + + if (newsletterFilters && newsletterFilters.length >= 2) { + return false; + } + + const stripeFilters = this.filters.filter(f => [ + 'subscriptions.plan_interval', + 'subscriptions.status', + 'subscriptions.start_date', + 'subscriptions.current_period_end', + 'conversion', + 'offer_redemptions' + ].includes(f.type)); + + if (stripeFilters && stripeFilters.length >= 1) { + return false; + } + + return true; } includeTierQuery() { diff --git a/ghost/admin/app/templates/members.hbs b/ghost/admin/app/templates/members.hbs index d72c91a904..c965f8389b 100644 --- a/ghost/admin/app/templates/members.hbs +++ b/ghost/admin/app/templates/members.hbs @@ -104,14 +104,14 @@ {{/if}} - {{#unless this.isMultiFiltered}} + {{#if this.isBulkDeletePermitted}}