Added newsletter management for member (#2336)

refs https://github.com/TryGhost/Team/issues/1492

Allows site owners to manage member's newsletter preference directly in Admin.

- also adds `visibility` property for newsletter model
- updates members test model to include `visibility`
This commit is contained in:
Rishabh Garg 2022-04-14 20:10:04 +05:30 committed by GitHub
parent b49db57a90
commit e240506c6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 117 additions and 1 deletions

View File

@ -90,6 +90,14 @@
</div>
</div>
{{#if (feature "multipleNewsletters")}}
<Member::NewsletterPreference
@member={{this.member}}
@newsletters={{this.newslettersList}}
@setMemberNewsletters={{this.setMemberNewsletters}}
/>
{{/if}}
{{#if this.isStripeConnected}}
<h4 class="gh-main-section-header small bn">Subscriptions</h4>

View File

@ -22,6 +22,7 @@ export default class extends Component {
@tracked showMemberProductModal = false;
@tracked productsList;
@tracked newslettersList;
get canShowStripeInfo() {
return !this.member.get('isNew') && this.membersUtils.isStripeEnabled;
@ -110,6 +111,9 @@ export default class extends Component {
@action
setup() {
this.fetchProducts.perform();
if (this.feature.get('multipleNewsletters')) {
this.fetchNewsletters.perform();
}
}
@action
@ -122,6 +126,11 @@ export default class extends Component {
this.member.set('labels', labels);
}
@action
setMemberNewsletters(newsletters) {
this.member.set('newsletters', newsletters);
}
@action
closeMemberProductModal() {
this.showMemberProductModal = false;
@ -197,4 +206,15 @@ export default class extends Component {
*fetchProducts() {
this.productsList = yield this.store.query('product', {filter: 'type:paid+active:true', include: 'monthly_price,yearly_price'});
}
@task({drop: true})
*fetchNewsletters() {
this.newslettersList = yield this.store.query('newsletter', {filter: 'status:active'});
if (this.member.get('isNew')) {
const defaultNewsletters = this.newslettersList.filter((newsletter) => {
return newsletter.subscribeOnSignup && newsletter.visibility === 'members';
});
this.setMemberNewsletters(defaultNewsletters);
}
}
}

View File

@ -0,0 +1,24 @@
<h4 class="gh-main-section-header small bn">Newsletters</h4>
<div class="gh-main-section-content grey">
{{#each this.newsletters as |newsletter|}}
<div class="flex justify-between items-center">
<div>
<h4 class="gh-setting-title m">{{newsletter.name}}</h4>
<p class="gh-setting-desc">{{newsletter.description}}</p>
</div>
<div class="for-switch">
<label class="switch" for={{newsletter.forId}}>
<Input
@checked={{newsletter.subscribed}}
@type="checkbox"
id={{newsletter.forId}}
name="subscribed"
data-test-checkbox="member-subscribed"
{{on "click" (fn this.updateNewsletterPreference newsletter)}}
/>
<span class="input-toggle-component"></span>
</label>
</div>
</div>
{{/each}}
</div>

View File

@ -0,0 +1,46 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {tracked} from '@glimmer/tracking';
export default class MembersNewsletterPreference extends Component {
@tracked filterValue;
constructor(...args) {
super(...args);
}
get newsletters() {
if (this.args.newsletters?.length > 0) {
return this.args.newsletters.map((d) => {
return {
name: d.name,
description: d.description,
subscribed: !!this.args.member?.get('newsletters')?.find((n) => {
return n.id === d.id;
}),
id: d.id,
forId: `${d.id}-checkbox`
};
});
}
return [];
}
@action
updateNewsletterPreference(newsletter, event) {
let updatedNewsletters = [];
const selectedNewsletter = this.args.newsletters.find((d) => {
return d.id === newsletter.id;
});
if (!event.target.checked) {
updatedNewsletters = this.args.member.newsletters.filter((d) => {
return d.id !== newsletter.id;
});
} else {
updatedNewsletters = this.args.member.newsletters.filter((d) => {
return d.id !== newsletter.id;
}).concat(selectedNewsletter);
}
this.args.setMemberNewsletters(updatedNewsletters);
}
}

View File

@ -21,6 +21,7 @@ export default Model.extend(ValidationEngine, {
emailOpenRate: attr('number'),
products: attr('member-product'),
newsletters: hasMany('newsletter', {embedded: 'always', async: false}),
labels: hasMany('label', {embedded: 'always', async: false}),
emailRecipients: hasMany('emailRecipient', {async: true}),

View File

@ -15,6 +15,7 @@ export default class Newsletter extends Model.extend(ValidationEngine) {
@attr({defaultValue: 'active'}) status;
@attr({defaultValue: ''}) recipientFilter;
@attr({defaultValue: false}) subscribeOnSignup;
@attr({defaultValue: 'members'}) visibility;
@attr({defaultValue: 0}) sortOrder;
// Design-related properties

View File

@ -7,7 +7,8 @@ export default class MemberSerializer extends ApplicationSerializer.extend(Embed
createdAtUTC: {key: 'created_at'},
lastSeenAtUTC: {key: 'last_seen_at'},
labels: {embedded: 'always'},
emailRecipients: {embedded: 'always'}
emailRecipients: {embedded: 'always'},
newsletters: {embedded: 'always'}
};
serialize(/*snapshot, options*/) {

View File

@ -59,5 +59,19 @@ export default BaseValidator.create({
}
model.hasValidated.addObject('senderReplyTo');
},
visibility(model) {
if (isBlank(model.visibility)) {
model.errors.add('visibility', 'Please enter visibility.');
this.invalidate();
}
if (!validator.isIn(model.senderReplyTo, ['members', 'paid'])) {
model.errors.add('visibility', 'Can only be set to "members" or "paid".');
this.invalidate();
}
model.hasValidated.addObject('visibility');
}
});

View File

@ -4,5 +4,6 @@ export default Model.extend({
labels: hasMany(),
emailRecipients: hasMany(),
products: hasMany(),
newsletters: hasMany(),
subscriptions: hasMany()
});