Cleaned up 'Filter by email disabled' GA feature flag (#20554)

no issue

- "Filter by email disabled" feature has been released to GA in [Ghost
v5.74.0](https://github.com/TryGhost/Ghost/releases/tag/v5.74.0)
(commit: 32d0d2b293)
- cf. [Project
details](https://www.notion.so/ghost/Filter-by-email-disabled-2a73f5da5e8b46bcaacb944bd98e0674?pvs=4)
This commit is contained in:
Sag 2024-07-09 12:11:26 +02:00 committed by GitHub
parent f87a7dcd18
commit 8b45af3458
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 171 additions and 180 deletions

View File

@ -1,126 +1,54 @@
import {MATCH_RELATION_OPTIONS} from './relation-options';
export const SUBSCRIBED_FILTER = ({newsletters, feature, group}) => {
if (feature.filterEmailDisabled) {
return {
label: newsletters.length > 1 ? 'All newsletters' : 'Newsletter subscription',
name: 'subscribed',
columnLabel: 'Subscribed',
relationOptions: MATCH_RELATION_OPTIONS,
valueType: 'options',
group: newsletters.length > 1 ? 'Newsletters' : group,
// Only show the filter for multiple newsletters if feature flag is enabled
feature: newsletters.length > 1 ? 'filterEmailDisabled' : undefined,
buildNqlFilter: (flt) => {
const relation = flt.relation;
const value = flt.value;
if (value === 'email-disabled') {
if (relation === 'is') {
return '(email_disabled:1)';
}
return '(email_disabled:0)';
}
if (relation === 'is') {
if (value === 'subscribed') {
return '(subscribed:true+email_disabled:0)';
}
return '(subscribed:false+email_disabled:0)';
}
// relation === 'is-not'
if (value === 'subscribed') {
return '(subscribed:false,email_disabled:1)';
}
return '(subscribed:true,email_disabled:1)';
},
parseNqlFilter: (flt) => {
const comparator = flt.$and || flt.$or; // $or for legacy filter backwards compatibility
if (!comparator || comparator.length !== 2) {
const filter = flt;
if (filter && filter.email_disabled !== undefined) {
if (filter.email_disabled) {
return {
value: 'email-disabled',
relation: 'is'
};
}
return {
value: 'email-disabled',
relation: 'is-not'
};
}
return;
}
if (comparator[0].subscribed === undefined || comparator[1].email_disabled === undefined) {
return;
}
const usedOr = flt.$or !== undefined;
const subscribed = comparator[0].subscribed;
if (usedOr) {
// Is not
return {
value: !subscribed ? 'subscribed' : 'unsubscribed',
relation: 'is-not'
};
}
return {
value: subscribed ? 'subscribed' : 'unsubscribed',
relation: 'is'
};
},
options: [
{label: newsletters.length > 1 ? 'Subscribed to at least one' : 'Subscribed', name: 'subscribed'},
{label: newsletters.length > 1 ? 'Unsubscribed from all' : 'Unsubscribed', name: 'unsubscribed'},
{label: 'Email disabled', name: 'email-disabled'}
],
getColumnValue: (member) => {
if (member.emailSuppression && member.emailSuppression.suppressed) {
return {
text: 'Email disabled'
};
}
return member.newsletters.length > 0 ? {
text: 'Subscribed'
} : {
text: 'Unsubscribed'
};
}
};
}
if (newsletters.length > 1) {
// Disable
// Only show the filter for multiple newsletters if feature flag is enabled
return [];
}
export const SUBSCRIBED_FILTER = ({newsletters, group}) => {
return {
label: 'Newsletter subscription',
label: newsletters.length > 1 ? 'All newsletters' : 'Newsletter subscription',
name: 'subscribed',
columnLabel: 'Subscribed',
relationOptions: MATCH_RELATION_OPTIONS,
valueType: 'options',
group: group,
group: newsletters.length > 1 ? 'Newsletters' : group,
buildNqlFilter: (flt) => {
const relation = flt.relation;
const value = flt.value;
return (relation === 'is' && value === 'true') || (relation === 'is-not' && value === 'false')
? '(subscribed:true+email_disabled:0)'
: '(subscribed:false,email_disabled:1)';
if (value === 'email-disabled') {
if (relation === 'is') {
return '(email_disabled:1)';
}
return '(email_disabled:0)';
}
if (relation === 'is') {
if (value === 'subscribed') {
return '(subscribed:true+email_disabled:0)';
}
return '(subscribed:false+email_disabled:0)';
}
// relation === 'is-not'
if (value === 'subscribed') {
return '(subscribed:false,email_disabled:1)';
}
return '(subscribed:true,email_disabled:1)';
},
parseNqlFilter: (flt) => {
const comparator = flt.$and || flt.$or;
const comparator = flt.$and || flt.$or; // $or for legacy filter backwards compatibility
if (!comparator || comparator.length !== 2) {
const filter = flt;
if (filter && filter.email_disabled !== undefined) {
if (filter.email_disabled) {
return {
value: 'email-disabled',
relation: 'is'
};
}
return {
value: 'email-disabled',
relation: 'is-not'
};
}
return;
}
@ -128,31 +56,44 @@ export const SUBSCRIBED_FILTER = ({newsletters, feature, group}) => {
return;
}
const usedOr = flt.$or !== undefined;
const subscribed = comparator[0].subscribed;
if (usedOr) {
// Is not
return {
value: !subscribed ? 'subscribed' : 'unsubscribed',
relation: 'is-not'
};
}
return {
value: subscribed ? 'true' : 'false',
value: subscribed ? 'subscribed' : 'unsubscribed',
relation: 'is'
};
},
options: [
{label: 'Subscribed', name: 'true'},
{label: 'Unsubscribed', name: 'false'}
{label: newsletters.length > 1 ? 'Subscribed to at least one' : 'Subscribed', name: 'subscribed'},
{label: newsletters.length > 1 ? 'Unsubscribed from all' : 'Unsubscribed', name: 'unsubscribed'},
{label: 'Email disabled', name: 'email-disabled'}
],
getColumnValue: (member, flt) => {
const relation = flt.relation;
const value = flt.value;
getColumnValue: (member) => {
if (member.emailSuppression && member.emailSuppression.suppressed) {
return {
text: 'Email disabled'
};
}
return {
text: (relation === 'is' && value === 'true') || (relation === 'is-not' && value === 'false')
? 'Subscribed'
: 'Unsubscribed'
return member.newsletters.length > 0 ? {
text: 'Subscribed'
} : {
text: 'Unsubscribed'
};
}
};
};
export const NEWSLETTERS_FILTERS = ({newsletters, group, feature}) => {
export const NEWSLETTERS_FILTERS = ({newsletters, group}) => {
if (newsletters.length <= 1) {
return [];
}
@ -210,12 +151,10 @@ export const NEWSLETTERS_FILTERS = ({newsletters, group, feature}) => {
const relation = flt.relation;
const value = flt.value;
if (feature.filterEmailDisabled) {
if (member.emailSuppression && member.emailSuppression.suppressed) {
return {
text: 'Email disabled'
};
}
if (member.emailSuppression && member.emailSuppression.suppressed) {
return {
text: 'Email disabled'
};
}
return {

View File

@ -76,7 +76,6 @@ export default class FeatureService extends Service {
@feature('tipsAndDonations') tipsAndDonations;
@feature('recommendations') recommendations;
@feature('lexicalIndicators') lexicalIndicators;
@feature('filterEmailDisabled') filterEmailDisabled;
@feature('adminXDemo') adminXDemo;
@feature('ActivityPub') ActivityPub;
@feature('internalLinking') internalLinking;

View File

@ -197,41 +197,10 @@ describe('Acceptance: Members filtering', function () {
expect(findAll('[data-test-list="members-list-item"]').length, '# of filtered member rows').to.equal(1);
});
it('can filter by specific newsletter subscription', async function () {
// add some members to filters
const newsletter = this.server.create('newsletter', {status: 'active', slug: 'test-newsletter'});
this.server.createList('newsletter', 4);
this.server.createList('tier', 4);
this.server.createList('member', 4, {subscribed: false});
await visit('/members');
expect(findAll('[data-test-list="members-list-item"]').length, '# of initial member rows')
.to.equal(4);
await click('[data-test-button="members-filter-actions"]');
// make sure newsletters are in the filter dropdown
const newslettersCount = this.server.schema.newsletters.all().models.length;
let options = this.element.querySelectorAll('option');
let matchingOptions = [...options].filter(option => option.value.includes('newsletters.slug'));
expect(matchingOptions).to.have.length(newslettersCount);
await visit('/');
await visit('/members');
// add some members with tiers
const tier = this.server.create('tier');
const member = this.server.create('member', {tiers: [tier], subscribed: true});
member.update({newsletters: [newsletter]});
this.server.createList('member', 4, {subscribed: false});
await visit('/members?filter=' + encodeURIComponent(`newsletters.slug:${newsletter.slug}`));
// only 1 member is subscribed so we should only see 1 row
expect(findAll('[data-test-list="members-list-item"]').length, '# of initial member rows')
.to.equal(1);
});
it('can filter by newsletter subscription', async function () {
// add some members to filter
it('can filter by newsletter subscription when there is only one newsletter', async function () {
// Create a single newsletter
this.server.createList('newsletter', 1);
// Add some members to filter
this.server.createList('member', 3, {subscribed: true, email_disabled: 0});
this.server.createList('member', 4, {subscribed: false, email_disabled: 0});
this.server.createList('member', 1, {subscribed: true, email_disabled: 1});
@ -255,18 +224,25 @@ describe('Acceptance: Members filtering', function () {
// has the right values
const valueOptions = findAll(`${filterSelector} [data-test-select="members-filter-value"] option`);
expect(valueOptions).to.have.length(2);
expect(valueOptions[0]).to.have.value('true');
expect(valueOptions[1]).to.have.value('false');
expect(valueOptions).to.have.length(3);
expect(valueOptions[0]).to.have.value('subscribed');
expect(valueOptions[1]).to.have.value('unsubscribed');
expect(valueOptions[2]).to.have.value('email-disabled');
// applies default filter immediately
expect(findAll('[data-test-list="members-list-item"]').length, '# of filtered member rows - true')
// applies default filter subscribed immediately
expect(findAll('[data-test-list="members-list-item"]').length, '# of filtered member rows - subscribed')
.to.equal(3);
// can change filter
await fillIn(`${filterSelector} [data-test-select="members-filter-value"]`, 'false');
expect(findAll('[data-test-list="members-list-item"]').length, '# of filtered member rows - false')
.to.equal(5);
// can change filter to unsubscribed
await fillIn(`${filterSelector} [data-test-select="members-filter-value"]`, 'unsubscribed');
expect(findAll('[data-test-list="members-list-item"]').length, '# of filtered member rows - unsubscribed')
.to.equal(4);
expect(find('[data-test-table-column="subscribed"]')).to.exist;
// can change filter to email-disabled
await fillIn(`${filterSelector} [data-test-select="members-filter-value"]`, 'email-disabled');
expect(findAll('[data-test-list="members-list-item"]').length, '# of filtered member rows - email-disabled')
.to.equal(1);
expect(find('[data-test-table-column="subscribed"]')).to.exist;
// can delete filter
@ -275,21 +251,99 @@ describe('Acceptance: Members filtering', function () {
expect(findAll('[data-test-list="members-list-item"]').length, '# of filtered member rows after delete')
.to.equal(8);
// Can set filter by path
// Can set filter to 'subscribed' by path
await visit('/');
await visit('/members?filter=' + encodeURIComponent('(subscribed:true+email_disabled:0)'));
expect(findAll('[data-test-list="members-list-item"]').length, '# of filtered member rows - true - from URL')
expect(findAll('[data-test-list="members-list-item"]').length, '# of filtered member rows - subscribed - from URL')
.to.equal(3);
await click('[data-test-button="members-filter-actions"]');
expect(find(`${filterSelector} [data-test-select="members-filter-value"]`)).to.have.value('true');
expect(find(`${filterSelector} [data-test-select="members-filter-value"]`)).to.have.value('subscribed');
// Can set filter by path
// Can set filter to 'unsubscribed' by path
await visit('/');
await visit('/members?filter=' + encodeURIComponent('(subscribed:false,email_disabled:1)'));
expect(findAll('[data-test-list="members-list-item"]').length, '# of filtered member rows - false - from URL')
.to.equal(5);
await visit('/members?filter=' + encodeURIComponent('(subscribed:false+email_disabled:0)'));
expect(findAll('[data-test-list="members-list-item"]').length, '# of filtered member rows - unsubscribed - from URL')
.to.equal(4);
await click('[data-test-button="members-filter-actions"]');
expect(find(`${filterSelector} [data-test-select="members-filter-value"]`)).to.have.value('false');
expect(find(`${filterSelector} [data-test-select="members-filter-value"]`)).to.have.value('unsubscribed');
// Can set filter to 'email-disabled' by path
await visit('/');
await visit('/members?filter=' + encodeURIComponent('(email_disabled:1)'));
expect(findAll('[data-test-list="members-list-item"]').length, '# of filtered member rows - email-disabled - from URL')
.to.equal(1);
await click('[data-test-button="members-filter-actions"]');
expect(find(`${filterSelector} [data-test-select="members-filter-value"]`)).to.have.value('email-disabled');
});
it('can filter by specific newsletter subscription when there are multiple newsletters', async function () {
// Create:
// - 1 subscribed member to newsletter
// - 1 subscribed member to newsletter with email disabled
// - 4 unsubscribed members
const newsletter = this.server.create('newsletter', {status: 'active', slug: 'test-newsletter'});
const tier = this.server.create('tier');
const subscribedMember = this.server.create('member', {tiers: [tier], subscribed: true, email_disabled: 0});
subscribedMember.update({newsletters: [newsletter]});
const emailDisabledMember = this.server.create('member', {tiers: [tier], subscribed: true, email_disabled: 1});
emailDisabledMember.update({newsletters: [newsletter]});
this.server.createList('member', 4, {subscribed: false, email_disabled: 0});
// Test initial member count
await visit('/members');
expect(findAll('[data-test-list="members-list-item"]').length, '# of initial member rows')
.to.equal(6);
// Test newsletters options are in the filter dropdown
await click('[data-test-button="members-filter-actions"]');
const newslettersCount = this.server.schema.newsletters.all().models.length;
let options = this.element.querySelectorAll('option');
let matchingOptions = [...options].filter(option => option.value.includes('newsletters.slug'));
expect(matchingOptions).to.have.length(newslettersCount);
const filterSelector = `[data-test-members-filter="0"]`;
// Select first newsletter
await fillIn(`${filterSelector} [data-test-select="members-filter"]`, `newsletters.slug:${newsletter.slug}`);
// Test that the filter has the right operators
const operatorOptions = findAll(`${filterSelector} [data-test-select="members-filter-operator"] option`);
expect(operatorOptions[0]).to.have.value('is');
expect(operatorOptions[1]).to.have.value('is-not');
// Test that the filter has the right operators
const valueOptions = findAll(`${filterSelector} [data-test-select="members-filter-value"] option`);
expect(valueOptions[0]).to.have.value('true');
expect(valueOptions[1]).to.have.value('false');
// applies default filter subscribed immediately, and only count subscribed members without email disabled
expect(findAll('[data-test-list="members-list-item"]').length, '# of filtered member rows - subscribed')
.to.equal(1);
// can change filter to unsubscribed
await fillIn(`${filterSelector} [data-test-select="members-filter-value"]`, 'false');
expect(findAll('[data-test-list="members-list-item"]').length, '# of filtered member rows - unsubscribed')
.to.equal(5);
// can delete filter
await click('[data-test-delete-members-filter="0"]');
expect(findAll('[data-test-list="members-list-item"]').length, '# of filtered member rows after delete')
.to.equal(6);
// Can filter members subscribed to that newsletter by path
await visit('/');
await visit('/members?filter=' + encodeURIComponent(`newsletters.slug:${newsletter.slug}+email_disabled:0`));
expect(findAll('[data-test-list="members-list-item"]').length, '# of initial member rows')
.to.equal(1);
// Can filter members unsubscribed to that newsletter by path
await visit('/');
await visit('/members?filter=' + encodeURIComponent(`newsletters.slug:-${newsletter.slug},email_disabled:1`));
expect(findAll('[data-test-list="members-list-item"]').length, '# of initial member rows')
.to.equal(5);
});
it('can filter by member status', async function () {

View File

@ -22,7 +22,6 @@ const GA_FEATURES = [
'signupForm',
'recommendations',
'listUnsubscribeHeader',
'filterEmailDisabled',
'newEmailAddresses',
'internalLinking'
];

View File

@ -1155,7 +1155,7 @@ exports[`Settings API Edit Can edit a setting 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "4559",
"content-length": "4530",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,