mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-27 10:42:45 +03:00
✨ Added specific newsletter support for bulk unsubscribes (#15742)
closes https://github.com/TryGhost/Team/issues/2013 Added support to bulk unsubscribe a selected (filtered) list on members from specific, selected newsletters.
This commit is contained in:
parent
548a3c7b93
commit
e0787b4e83
@ -43,11 +43,28 @@
|
||||
{{#if countFetcher.isLoading}}
|
||||
<GhLoadingSpinner />
|
||||
{{else}}
|
||||
{{#if this.hasMultipleNewsletters }}
|
||||
<p>
|
||||
Which newsletter should these
|
||||
<strong data-test-text="unsubscribe-count">{{gh-pluralize countFetcher.count "member"}}</strong> be unsubscribed from?
|
||||
</p>
|
||||
<OneWaySelect
|
||||
@value={{this.selectedNewsletterId}}
|
||||
@options={{this.newsletterList}}
|
||||
@optionLabelPath="name"
|
||||
@optionValuePath="value"
|
||||
@optionTargetPath="value"
|
||||
@update={{this.setSelectedNewsletter}}
|
||||
data-test-select="members-single-newsletter-unsubscribe"
|
||||
/>
|
||||
{{else}}
|
||||
<p>
|
||||
You're about to unsubscribe
|
||||
<strong data-test-text="unsubscribe-count">{{gh-pluralize countFetcher.count "member"}}</strong> from email newsletters.
|
||||
Are you sure?
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
{{else}}
|
||||
@ -62,6 +79,7 @@
|
||||
<span>Close</span>
|
||||
</button>
|
||||
{{else}}
|
||||
{{#if this.hasMultipleNewsletters}}
|
||||
<button class="gh-btn" data-test-button="cancel" type="button" {{on "click" @close}}>
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
@ -74,6 +92,21 @@
|
||||
@class="gh-btn gh-btn-red gh-btn-icon"
|
||||
data-test-button="confirm"
|
||||
/>
|
||||
|
||||
{{else}}
|
||||
<button class="gh-btn" data-test-button="cancel" type="button" {{on "click" @close}}>
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
|
||||
<GhTaskButton
|
||||
@disabled={{this.isDisabled}}
|
||||
@buttonText="Unsubscribe members"
|
||||
@successText="Unsubscribed"
|
||||
@task={{this.bulkUnsubscribeTask}}
|
||||
@class="gh-btn gh-btn-red gh-btn-icon"
|
||||
data-test-button="confirm"
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
|
@ -7,10 +7,13 @@ import {tracked} from '@glimmer/tracking';
|
||||
export default class BulkUnsubscribeMembersModal extends Component {
|
||||
@service ajax;
|
||||
@service ghostPaths;
|
||||
@service store;
|
||||
|
||||
@tracked error;
|
||||
@tracked response;
|
||||
|
||||
@tracked selectedNewsletterId = null;
|
||||
|
||||
get isDisabled() {
|
||||
return !this.args.data.query;
|
||||
}
|
||||
@ -19,27 +22,62 @@ export default class BulkUnsubscribeMembersModal extends Component {
|
||||
return !!(this.error || this.response);
|
||||
}
|
||||
|
||||
get hasMultipleNewsletters() {
|
||||
const newsletters = this.store.peekAll('newsletter');
|
||||
const activeNewsletters = newsletters.filter(newsletter => newsletter.status !== 'archived');
|
||||
if (activeNewsletters.length <= 1) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
get newsletterList() {
|
||||
const newsletters = this.store.peekAll('newsletter');
|
||||
const activeNewsletters = newsletters.filter(newsletter => newsletter.status !== 'archived');
|
||||
let list = [{
|
||||
name: 'All newsletters',
|
||||
value: 'all'
|
||||
}];
|
||||
activeNewsletters.forEach((newsletter) => {
|
||||
list.push({
|
||||
name: newsletter.name,
|
||||
value: newsletter.id
|
||||
});
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
@action
|
||||
setLabel(label) {
|
||||
this.selectedLabel = label;
|
||||
}
|
||||
|
||||
@action
|
||||
setSelectedNewsletter(newsletter) {
|
||||
if (newsletter === 'all') {
|
||||
this.selectedNewsletterId = null;
|
||||
} else {
|
||||
this.selectedNewsletterId = newsletter;
|
||||
}
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*bulkUnsubscribeTask() {
|
||||
try {
|
||||
const query = new URLSearchParams(this.args.data.query);
|
||||
let args = this.args.data.query;
|
||||
const query = new URLSearchParams(args);
|
||||
const removeLabelUrl = `${this.ghostPaths.url.api('members/bulk')}?${query}`;
|
||||
const response = yield this.ajax.put(removeLabelUrl, {
|
||||
data: {
|
||||
bulk: {
|
||||
action: 'unsubscribe',
|
||||
meta: {}
|
||||
}
|
||||
const response = yield this.ajax.put(removeLabelUrl, {data: {
|
||||
bulk: {
|
||||
action: 'unsubscribe',
|
||||
newsletter: (this.selectedNewsletterId ? this.selectedNewsletterId : null),
|
||||
meta: {}
|
||||
}
|
||||
});
|
||||
}});
|
||||
|
||||
this.args.data.onComplete?.();
|
||||
|
||||
|
||||
this.response = response?.bulk?.meta;
|
||||
|
||||
return true;
|
||||
|
@ -101,7 +101,6 @@ module.exports = function (Bookshelf) {
|
||||
*/
|
||||
bulkDestroy: function bulkDestroy(data, tableName, options = {}) {
|
||||
tableName = tableName || this.prototype.tableName;
|
||||
|
||||
return del(Bookshelf.knex, tableName, data, options);
|
||||
}
|
||||
});
|
||||
|
9
ghost/core/core/server/models/member-newsletter.js
Normal file
9
ghost/core/core/server/models/member-newsletter.js
Normal file
@ -0,0 +1,9 @@
|
||||
const ghostBookshelf = require('./base');
|
||||
|
||||
const MemberNewsletter = ghostBookshelf.Model.extend({
|
||||
tableName: 'members_newsletters'
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
MemberNewsletter: ghostBookshelf.model('MemberNewsletter', MemberNewsletter)
|
||||
};
|
@ -177,6 +177,7 @@ function createApiInstance(config) {
|
||||
StripeCustomer: models.MemberStripeCustomer,
|
||||
StripeCustomerSubscription: models.StripeCustomerSubscription,
|
||||
Member: models.Member,
|
||||
MemberNewsletter: models.MemberNewsletter,
|
||||
MemberCancelEvent: models.MemberCancelEvent,
|
||||
MemberSubscribeEvent: models.MemberSubscribeEvent,
|
||||
MemberPaidSubscriptionEvent: models.MemberPaidSubscriptionEvent,
|
||||
|
@ -468,6 +468,21 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API Bulk operations Can bulk unsubscribe members from specific newsletter 1: [body] 1`] = `
|
||||
Object {
|
||||
"bulk": Object {
|
||||
"meta": Object {
|
||||
"errors": Array [],
|
||||
"stats": Object {
|
||||
"successful": 4,
|
||||
"unsuccessful": 0,
|
||||
},
|
||||
"unsuccessfulData": Array [],
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API Bulk operations Can bulk unsubscribe members with deprecated subscribed filter (actual) 1: [body] 1`] = `
|
||||
Object {
|
||||
"bulk": Object {
|
||||
|
@ -2617,6 +2617,38 @@ describe('Members API Bulk operations', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('Can bulk unsubscribe members from specific newsletter', async function () {
|
||||
const member = fixtureManager.get('members', 4);
|
||||
const newsletterCount = 2;
|
||||
|
||||
const model = await models.Member.findOne({id: member.id}, {withRelated: 'newsletters'});
|
||||
should(model.relations.newsletters.models.length).equal(newsletterCount, 'This test requires a member with 2 or more newsletters');
|
||||
|
||||
await agent
|
||||
.put(`/members/bulk/?all=true`)
|
||||
.body({bulk: {
|
||||
action: 'unsubscribe',
|
||||
newsletter: model.relations.newsletters.models[0].id,
|
||||
meta: {}
|
||||
}})
|
||||
.expectStatus(200)
|
||||
.matchBodySnapshot({
|
||||
bulk: {
|
||||
meta: {
|
||||
stats: {
|
||||
successful: 4,
|
||||
unsuccessful: 0
|
||||
},
|
||||
unsuccessfulData: [],
|
||||
errors: []
|
||||
}
|
||||
}
|
||||
});
|
||||
const updatedModel = await models.Member.findOne({id: member.id}, {withRelated: 'newsletters'});
|
||||
// ensure they were unsubscribed from the single 'chosen' newsletter
|
||||
should(updatedModel.relations.newsletters.models.length).equal(newsletterCount - 1);
|
||||
});
|
||||
|
||||
it('Can bulk unsubscribe members with deprecated subscribed filter', async function () {
|
||||
await agent
|
||||
.put(`/members/bulk/?filter=subscribed:false`)
|
||||
|
@ -38,6 +38,7 @@ module.exports = function MembersAPI({
|
||||
StripeCustomer,
|
||||
StripeCustomerSubscription,
|
||||
Member,
|
||||
MemberNewsletter,
|
||||
MemberCancelEvent,
|
||||
MemberSubscribeEvent,
|
||||
MemberLoginEvent,
|
||||
@ -86,6 +87,7 @@ module.exports = function MembersAPI({
|
||||
labsService,
|
||||
productRepository,
|
||||
Member,
|
||||
MemberNewsletter,
|
||||
MemberCancelEvent,
|
||||
MemberSubscribeEventModel: MemberSubscribeEvent,
|
||||
MemberPaidSubscriptionEvent,
|
||||
|
@ -28,6 +28,7 @@ module.exports = class MemberRepository {
|
||||
/**
|
||||
* @param {object} deps
|
||||
* @param {any} deps.Member
|
||||
* @param {any} deps.MemberNewsletter
|
||||
* @param {any} deps.MemberCancelEvent
|
||||
* @param {any} deps.MemberSubscribeEventModel
|
||||
* @param {any} deps.MemberEmailChangeEvent
|
||||
@ -46,6 +47,7 @@ module.exports = class MemberRepository {
|
||||
*/
|
||||
constructor({
|
||||
Member,
|
||||
MemberNewsletter,
|
||||
MemberCancelEvent,
|
||||
MemberSubscribeEventModel,
|
||||
MemberEmailChangeEvent,
|
||||
@ -63,6 +65,7 @@ module.exports = class MemberRepository {
|
||||
newslettersService
|
||||
}) {
|
||||
this._Member = Member;
|
||||
this._MemberNewsletter = MemberNewsletter;
|
||||
this._MemberCancelEvent = MemberCancelEvent;
|
||||
this._MemberSubscribeEvent = MemberSubscribeEventModel;
|
||||
this._MemberEmailChangeEvent = MemberEmailChangeEvent;
|
||||
@ -718,7 +721,6 @@ module.exports = class MemberRepository {
|
||||
// Include mongoTransformer to apply subscribed:{true|false} => newsletter relation mapping
|
||||
Object.assign(filterOptions, _.pick(options, ['filter', 'search', 'mongoTransformer']));
|
||||
}
|
||||
|
||||
const memberRows = await this._Member.getFilteredCollectionQuery(filterOptions)
|
||||
.select('members.id')
|
||||
.distinct();
|
||||
@ -726,9 +728,20 @@ module.exports = class MemberRepository {
|
||||
const memberIds = memberRows.map(row => row.id);
|
||||
|
||||
if (data.action === 'unsubscribe') {
|
||||
return await this._Member.bulkDestroy(memberIds, 'members_newsletters', {column: 'member_id'});
|
||||
const hasNewsletterSelected = (Object.prototype.hasOwnProperty.call(data, 'newsletter') && data.newsletter !== null);
|
||||
if (hasNewsletterSelected) {
|
||||
const membersArr = memberIds.join(',');
|
||||
const unsubscribeRows = await this._MemberNewsletter.getFilteredCollectionQuery({
|
||||
filter: `newsletter_id:${data.newsletter}+member_id:[${membersArr}]`
|
||||
});
|
||||
const toUnsubscribe = unsubscribeRows.map(row => row.id);
|
||||
|
||||
return await this._MemberNewsletter.bulkDestroy(toUnsubscribe);
|
||||
}
|
||||
if (!hasNewsletterSelected) {
|
||||
return await this._Member.bulkDestroy(memberIds, 'members_newsletters', {column: 'member_id'});
|
||||
}
|
||||
}
|
||||
|
||||
if (data.action === 'removeLabel') {
|
||||
const membersLabelsRows = await this._Member.getLabelRelations({
|
||||
labelId: data.meta.label.id,
|
||||
|
Loading…
Reference in New Issue
Block a user