mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-23 10:53:34 +03:00
✨ Added default newsletter recipients setting (#1946)
refs https://github.com/TryGhost/Team/issues/496 reqs https://github.com/TryGhost/Ghost/pull/12925 The publish menu was meant to default to matching post visibility but that wasn't working consistently and didn't make sense for sites which don't email every post to their members. A "Default newsletter recipients" option has been added to the "Email newsletter" settings screen and the publish menu updated to reflect the option. The free/paid toggles in the publish menu have also been swapped out for a multi-select style component that will cater to more complex member segmentation.
This commit is contained in:
parent
09b50995ec
commit
da49dc4922
@ -16,6 +16,11 @@
|
||||
<div class="gh-main-section">
|
||||
<h4 class="gh-main-section-header small bn">Email</h4>
|
||||
<section class="gh-expandable">
|
||||
<Settings::DefaultEmailRecipients
|
||||
@expanded={{this.emailRecipientsExpanded}}
|
||||
@toggleExpansion={{this.toggleEmailRecipientsExpansion}}
|
||||
/>
|
||||
|
||||
<div class="gh-expandable-block">
|
||||
<div class="gh-expandable-header">
|
||||
<div>
|
||||
|
7
ghost/admin/app/components/gh-members-segment-count.hbs
Normal file
7
ghost/admin/app/components/gh-members-segment-count.hbs
Normal file
@ -0,0 +1,7 @@
|
||||
<span
|
||||
class="segment-totals"
|
||||
{{did-insert (perform this.fetchTotalsTask)}}
|
||||
{{did-update (perform this.fetchSegmentTotalTask) @segment}}
|
||||
>
|
||||
{{format-number this.segmentTotal}} / {{format-number this.total}} members
|
||||
</span>
|
31
ghost/admin/app/components/gh-members-segment-count.js
Normal file
31
ghost/admin/app/components/gh-members-segment-count.js
Normal file
@ -0,0 +1,31 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task, taskGroup} from 'ember-concurrency-decorators';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class GhMembersSegmentCountComponent extends Component {
|
||||
@service store;
|
||||
|
||||
@tracked total = 0;
|
||||
@tracked segmentTotal = 0;
|
||||
|
||||
@taskGroup fetchTasks;
|
||||
|
||||
@task({group: 'fetchTasks'})
|
||||
*fetchTotalsTask() {
|
||||
this.fetchSegmentTotalTask.perform();
|
||||
|
||||
const members = yield this.store.query('member', {limit: 1});
|
||||
this.total = members.meta.pagination.total;
|
||||
}
|
||||
|
||||
@task({group: 'fetchTasks'})
|
||||
*fetchSegmentTotalTask() {
|
||||
if (!this.args.segment) {
|
||||
return this.segmentTotal = 0;
|
||||
}
|
||||
|
||||
const members = yield this.store.query('member', {limit: 1, filter: this.args.segment});
|
||||
this.segmentTotal = members.meta.pagination.total;
|
||||
}
|
||||
}
|
15
ghost/admin/app/components/gh-members-segment-select.hbs
Normal file
15
ghost/admin/app/components/gh-members-segment-select.hbs
Normal file
@ -0,0 +1,15 @@
|
||||
<GhTokenInput
|
||||
@options={{this.options}}
|
||||
@selected={{this.selectedOptions}}
|
||||
@disabled={{or @disabled this.fetchOptionsTask.isRunning}}
|
||||
@optionsComponent="power-select/options"
|
||||
@allowCreation={{false}}
|
||||
@renderInPlace={{this.renderInPlace}}
|
||||
@onChange={{this.setSegment}}
|
||||
@disabled={{@disabled}}
|
||||
as |option|
|
||||
>
|
||||
{{option.name}}
|
||||
</GhTokenInput>
|
||||
|
||||
<GhMembersSegmentCount @segment={{@segment}} />
|
62
ghost/admin/app/components/gh-members-segment-select.js
Normal file
62
ghost/admin/app/components/gh-members-segment-select.js
Normal file
@ -0,0 +1,62 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency-decorators';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class GhMembersSegmentSelect extends Component {
|
||||
@service store;
|
||||
|
||||
@tracked options = [];
|
||||
|
||||
get renderInPlace() {
|
||||
return this.args.renderInPlace === undefined ? false : this.args.renderInPlace;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.fetchOptionsTask.perform();
|
||||
}
|
||||
|
||||
get flatOptions() {
|
||||
const options = [];
|
||||
|
||||
function getOptions(option) {
|
||||
if (option.options) {
|
||||
return option.options.forEach(getOptions);
|
||||
}
|
||||
|
||||
options.push(option);
|
||||
}
|
||||
|
||||
this.options.forEach(getOptions);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
get selectedOptions() {
|
||||
const segments = (this.args.segment || '').split(',');
|
||||
return this.flatOptions.filter(option => segments.includes(option.segment));
|
||||
}
|
||||
|
||||
@action
|
||||
setSegment(options) {
|
||||
const segment = options.mapBy('segment').join(',') || null;
|
||||
this.args.onChange?.(segment);
|
||||
}
|
||||
|
||||
@task
|
||||
*fetchOptionsTask() {
|
||||
const options = yield [{
|
||||
name: 'Free members',
|
||||
segment: 'status:free',
|
||||
class: 'segment-status'
|
||||
}, {
|
||||
name: 'Paid members',
|
||||
segment: 'status:-free', // paid & comped
|
||||
class: 'segment-status'
|
||||
}];
|
||||
|
||||
this.options = options;
|
||||
}
|
||||
}
|
@ -29,52 +29,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if (and this.canSendEmail showSendEmail)}}
|
||||
{{#if this.canSendEmail}}
|
||||
<div class="gh-publishmenu-section">
|
||||
<div class="gh-publishmenu-email">
|
||||
{{#if this.backgroundLoader.isRunning}}
|
||||
<div class="gh-loading-spinner" style="zoom: 50%"></div>
|
||||
{{else if this.isSendingEmailLimited}}
|
||||
{{#if this.isSendingEmailLimited}}
|
||||
<p class="gh-box gh-box-alert">{{html-safe this.sendingEmailLimitError}}</p>
|
||||
{{else}}
|
||||
<div class="gh-publishmenu-email-label {{if this.disableEmailOption "pe-none"}}">
|
||||
<label class="gh-publishmenu-radio-label mb3 {{if this.disableEmailOption "midgrey"}}">Send by email to</label>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="gh-publishmenu-send-to-option">
|
||||
<p>Free members <span class="gh-publishmenu-emailcount">{{this.freeMemberCountLabel}}</span></p>
|
||||
<div class="for-switch small" {{action "toggleSendEmailWhenPublished" "free" bubbles="false"}}>
|
||||
<label class="switch" for="send-email-to-free">
|
||||
<input
|
||||
id="send-email-to-free"
|
||||
type="checkbox"
|
||||
checked={{this.sendEmailToFreeMembersWhenPublished}}
|
||||
disabled={{this.disableEmailOption}}
|
||||
class="gh-input post-settings-featured"
|
||||
onclick={{action "toggleSendEmailWhenPublished" value="free"}}
|
||||
data-test-checkbox="free-members"
|
||||
>
|
||||
<span class="input-toggle-component"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-publishmenu-send-to-option">
|
||||
<p>Paid members <span class="gh-publishmenu-emailcount">{{this.paidMemberCountLabel}}</span></p>
|
||||
<div class="for-switch small" {{action "toggleSendEmailWhenPublished" "paid" bubbles="false"}}>
|
||||
<label class="switch" for="send-email-to-paid">
|
||||
<input
|
||||
id="send-email-to-paid"
|
||||
type="checkbox"
|
||||
checked={{this.sendEmailToPaidMembersWhenPublished}}
|
||||
disabled={{this.disableEmailOption}}
|
||||
class="gh-input post-settings-featured"
|
||||
onclick={{action "toggleSendEmailWhenPublished" value="paid"}}
|
||||
data-test-checkbox="paid-members"
|
||||
>
|
||||
<span class="input-toggle-component"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<GhMembersSegmentSelect
|
||||
@segment={{this.recipientsSegment}}
|
||||
@onChange={{action "setSendEmailWhenPublished"}}
|
||||
@renderInPlace={{true}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
@ -1,11 +1,23 @@
|
||||
import Component from '@ember/component';
|
||||
import moment from 'moment';
|
||||
import {computed} from '@ember/object';
|
||||
import {formatNumber} from 'ghost-admin/helpers/format-number';
|
||||
import {isEmpty} from '@ember/utils';
|
||||
import {or} from '@ember/object/computed';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
const MEMBERS_SEGMENT_MAP = [{
|
||||
name: 'all',
|
||||
segment: 'status:free,status:-free'
|
||||
}, {
|
||||
name: 'free',
|
||||
segment: 'status:free'
|
||||
}, {
|
||||
name: 'paid',
|
||||
segment: 'status:-free'
|
||||
}, {
|
||||
name: 'none',
|
||||
segment: null
|
||||
}];
|
||||
|
||||
export default Component.extend({
|
||||
feature: service(),
|
||||
settings: service(),
|
||||
@ -19,50 +31,11 @@ export default Component.extend({
|
||||
_publishedAtBlogTZ: null,
|
||||
|
||||
'data-test-publishmenu-draft': true,
|
||||
showSendEmail: or('session.user.isOwner', 'session.user.isAdmin', 'session.user.isEditor'),
|
||||
|
||||
disableEmailOption: computed('memberCount', function () {
|
||||
return (this.get('session.user.isOwnerOrAdmin') && this.memberCount === 0);
|
||||
}),
|
||||
|
||||
disableFreeMemberCheckbox: computed('freeMemberCount', function () {
|
||||
return (this.get('session.user.isOwnerOrAdmin') && this.freeMemberCount === 0);
|
||||
}),
|
||||
|
||||
disablePaidMemberCheckbox: computed('paidMemberCount', function () {
|
||||
return (this.get('session.user.isOwnerOrAdmin') && this.paidMemberCount === 0);
|
||||
}),
|
||||
|
||||
freeMemberCountLabel: computed('freeMemberCount', function () {
|
||||
if (this.get('freeMemberCount') !== undefined) {
|
||||
return `(${formatNumber(this.get('freeMemberCount'))})`;
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
|
||||
paidMemberCountLabel: computed('freeMemberCount', function () {
|
||||
if (this.get('freeMemberCount') !== undefined) {
|
||||
return `(${formatNumber(this.get('paidMemberCount'))})`;
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
|
||||
canSendEmail: computed('post.{isPost,email}', 'settings.{mailgunApiKey,mailgunDomain,mailgunBaseUrl}', 'config.mailgunIsConfigured', function () {
|
||||
let mailgunIsConfigured = this.get('settings.mailgunApiKey') && this.get('settings.mailgunDomain') && this.get('settings.mailgunBaseUrl') || this.get('config.mailgunIsConfigured');
|
||||
let isPost = this.post.isPost;
|
||||
let hasSentEmail = !!this.post.email;
|
||||
|
||||
return mailgunIsConfigured && isPost && !hasSentEmail;
|
||||
}),
|
||||
|
||||
sendEmailToFreeMembersWhenPublished: computed('sendEmailWhenPublished', function () {
|
||||
return ['free', 'all'].includes(this.sendEmailWhenPublished);
|
||||
}),
|
||||
|
||||
sendEmailToPaidMembersWhenPublished: computed('sendEmailWhenPublished', function () {
|
||||
return ['paid', 'all'].includes(this.sendEmailWhenPublished);
|
||||
}),
|
||||
|
||||
didInsertElement() {
|
||||
this.post.set('publishedAtBlogTZ', this.get('post.publishedAtUTC'));
|
||||
this.send('setSaveType', 'publish');
|
||||
@ -115,23 +88,9 @@ export default Component.extend({
|
||||
return post.validate();
|
||||
},
|
||||
|
||||
toggleSendEmailWhenPublished(type) {
|
||||
let isFree = this.get('sendEmailToFreeMembersWhenPublished');
|
||||
let isPaid = this.get('sendEmailToPaidMembersWhenPublished');
|
||||
if (type === 'free') {
|
||||
isFree = !isFree;
|
||||
} else if (type === 'paid') {
|
||||
isPaid = !isPaid;
|
||||
}
|
||||
if (isFree && isPaid) {
|
||||
this.setSendEmailWhenPublished('all');
|
||||
} else if (isFree && !isPaid) {
|
||||
this.setSendEmailWhenPublished('free');
|
||||
} else if (!isFree && isPaid) {
|
||||
this.setSendEmailWhenPublished('paid');
|
||||
} else if (!isFree && !isPaid) {
|
||||
this.setSendEmailWhenPublished('none');
|
||||
}
|
||||
setSendEmailWhenPublished(segment) {
|
||||
const segmentName = MEMBERS_SEGMENT_MAP.findBy('segment', segment).name;
|
||||
this.setSendEmailWhenPublished(segmentName);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -28,55 +28,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{#if (and canSendEmail showSendEmail)}}
|
||||
{{#unless this.post.email}}
|
||||
<section class="gh-publishmenu-section">
|
||||
<div class="gh-publishmenu-email">
|
||||
{{#if this.backgroundLoader.isRunning}}
|
||||
<div class="gh-loading-spinner" style="zoom: 50%"></div>
|
||||
{{else if this.isSendingEmailLimited}}
|
||||
<p>{{html-safe this.sendingEmailLimitError}}</p>
|
||||
{{else}}
|
||||
<div class="gh-publishmenu-email-label">
|
||||
<label class="gh-publishmenu-radio-label mb3 pe-none">Send by email to</label>
|
||||
<div class="form-group">
|
||||
<div class="gh-publishmenu-send-to-option gh-publishmenu-checkbox-disabled">
|
||||
<p>Free members</p>
|
||||
<div class="for-switch small">
|
||||
<label class="switch" for="send-email-to-free">
|
||||
<input
|
||||
id="send-email-to-free"
|
||||
type="checkbox"
|
||||
checked={{this.sendEmailToFreeMembersWhenPublished}}
|
||||
disabled={{this.disableEmailOption}}
|
||||
class="gh-input post-settings-featured"
|
||||
data-test-checkbox="free-members"
|
||||
>
|
||||
<span class="input-toggle-component"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-publishmenu-send-to-option gh-publishmenu-checkbox-disabled">
|
||||
<p>Paid members</p>
|
||||
<div class="for-switch small">
|
||||
<label class="switch" for="send-email-to-paid">
|
||||
<input
|
||||
id="send-email-to-paid"
|
||||
type="checkbox"
|
||||
checked={{this.sendEmailToPaidMembersWhenPublished}}
|
||||
disabled={{this.disableEmailOption}}
|
||||
class="gh-input post-settings-featured"
|
||||
data-test-checkbox="paid-members"
|
||||
>
|
||||
<span class="input-toggle-component"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#if this.canSendEmail}}
|
||||
<section class="gh-publishmenu-section">
|
||||
<div class="gh-publishmenu-email">
|
||||
{{#if this.isSendingEmailLimited}}
|
||||
<p>{{html-safe this.sendingEmailLimitError}}</p>
|
||||
{{else}}
|
||||
<div class="gh-publishmenu-email-label {{if this.disableEmailOption "pe-none"}}">
|
||||
<label class="gh-publishmenu-radio-label mb3 {{if this.disableEmailOption "midgrey"}}">Send by email to</label>
|
||||
|
||||
<div class="form-group">
|
||||
<GhMembersSegmentSelect
|
||||
@segment={{this.recipientsSegment}}
|
||||
@disabled={{true}}
|
||||
@renderInPlace={{true}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
{{/unless}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
@ -1,8 +1,6 @@
|
||||
import Component from '@ember/component';
|
||||
import moment from 'moment';
|
||||
import {computed} from '@ember/object';
|
||||
import {equal, or} from '@ember/object/computed';
|
||||
import {formatNumber} from 'ghost-admin/helpers/format-number';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
@ -21,47 +19,6 @@ export default Component.extend({
|
||||
|
||||
'data-test-publishmenu-scheduled': true,
|
||||
|
||||
disableEmailOption: equal('memberCount', 0),
|
||||
showSendEmail: or('session.user.isOwner', 'session.user.isAdmin', 'session.user.isEditor'),
|
||||
|
||||
disableFreeMemberCheckbox: computed('freeMemberCount', function () {
|
||||
return (this.get('session.user.isOwnerOrAdmin') && this.freeMemberCount === 0);
|
||||
}),
|
||||
|
||||
disablePaidMemberCheckbox: computed('paidMemberCount', function () {
|
||||
return (this.get('session.user.isOwnerOrAdmin') && this.paidMemberCount === 0);
|
||||
}),
|
||||
|
||||
freeMemberCountLabel: computed('freeMemberCount', function () {
|
||||
if (this.get('freeMemberCount') !== undefined) {
|
||||
return `(${formatNumber(this.get('freeMemberCount'))})`;
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
|
||||
paidMemberCountLabel: computed('freeMemberCount', function () {
|
||||
if (this.get('freeMemberCount') !== undefined) {
|
||||
return `(${formatNumber(this.get('paidMemberCount'))})`;
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
|
||||
canSendEmail: computed('post.{isPost,email}', 'settings.{mailgunApiKey,mailgunDomain,mailgunBaseUrl}', 'config.mailgunIsConfigured', function () {
|
||||
let mailgunIsConfigured = this.get('settings.mailgunApiKey') && this.get('settings.mailgunDomain') && this.get('settings.mailgunBaseUrl') || this.get('config.mailgunIsConfigured');
|
||||
let isPost = this.post.isPost;
|
||||
let hasSentEmail = !!this.post.email;
|
||||
|
||||
return mailgunIsConfigured && isPost && !hasSentEmail;
|
||||
}),
|
||||
|
||||
sendEmailToFreeMembersWhenPublished: computed('post.emailRecipientFilter', function () {
|
||||
return ['free', 'all'].includes(this.post.emailRecipientFilter);
|
||||
}),
|
||||
|
||||
sendEmailToPaidMembersWhenPublished: computed('post.emailRecipientFilter', function () {
|
||||
return ['paid', 'all'].includes(this.post.emailRecipientFilter);
|
||||
}),
|
||||
|
||||
timeToPublished: computed('post.publishedAtUTC', 'clock.second', function () {
|
||||
let publishedAtUTC = this.get('post.publishedAtUTC');
|
||||
|
||||
|
@ -8,17 +8,15 @@
|
||||
<GhPublishmenuPublished
|
||||
@post={{this.post}}
|
||||
@saveType={{this.saveType}}
|
||||
@setSaveType={{action "setSaveType"}}
|
||||
@backgroundTask={{this.backgroundTask}} />
|
||||
@setSaveType={{action "setSaveType"}} />
|
||||
|
||||
{{else if (eq this.displayState "scheduled")}}
|
||||
<GhPublishmenuScheduled
|
||||
@post={{this.post}}
|
||||
@saveType={{this.saveType}}
|
||||
@isClosing={{this.isClosing}}
|
||||
@memberCount={{this.memberCount}}
|
||||
@paidMemberCount={{this.paidMemberCount}}
|
||||
@freeMemberCount={{this.freeMemberCount}}
|
||||
@canSendEmail={{this.canSendEmail}}
|
||||
@recipientsSegment={{this.recipientsSegment}}
|
||||
@setSaveType={{action "setSaveType"}}
|
||||
@setTypedDateError={{action (mut this.typedDateError)}}
|
||||
@isSendingEmailLimited={{this.isSendingEmailLimited}}
|
||||
@ -30,12 +28,9 @@
|
||||
@saveType={{this.saveType}}
|
||||
@setSaveType={{action "setSaveType"}}
|
||||
@setTypedDateError={{action (mut this.typedDateError)}}
|
||||
@canSendEmail={{this.canSendEmail}}
|
||||
@recipientsSegment={{this.recipientsSegment}}
|
||||
@setSendEmailWhenPublished={{action "setSendEmailWhenPublished"}}
|
||||
@backgroundTask={{this.backgroundTask}}
|
||||
@memberCount={{this.memberCount}}
|
||||
@paidMemberCount={{this.paidMemberCount}}
|
||||
@freeMemberCount={{this.freeMemberCount}}
|
||||
@sendEmailWhenPublished={{this.sendEmailWhenPublished}}
|
||||
@isSendingEmailLimited={{this.isSendingEmailLimited}}
|
||||
@sendingEmailLimitError={{this.sendingEmailLimitError}} />
|
||||
{{/if}}
|
||||
|
@ -9,6 +9,20 @@ import {task, timeout} from 'ember-concurrency';
|
||||
const CONFIRM_EMAIL_POLL_LENGTH = 1000;
|
||||
const CONFIRM_EMAIL_MAX_POLL_LENGTH = 15 * 1000;
|
||||
|
||||
const MEMBERS_SEGMENT_MAP = [{
|
||||
name: 'all',
|
||||
segment: 'status:free,status:-free'
|
||||
}, {
|
||||
name: 'free',
|
||||
segment: 'status:free'
|
||||
}, {
|
||||
name: 'paid',
|
||||
segment: 'status:-free'
|
||||
}, {
|
||||
name: 'none',
|
||||
segment: null
|
||||
}];
|
||||
|
||||
export default Component.extend({
|
||||
clock: service(),
|
||||
feature: service(),
|
||||
@ -18,7 +32,6 @@ export default Component.extend({
|
||||
store: service(),
|
||||
limit: service(),
|
||||
|
||||
backgroundTask: null,
|
||||
classNames: 'gh-publishmenu',
|
||||
displayState: 'draft',
|
||||
post: null,
|
||||
@ -41,12 +54,22 @@ export default Component.extend({
|
||||
|
||||
hasEmailPermission: or('session.user.isOwner', 'session.user.isAdmin', 'session.user.isEditor'),
|
||||
|
||||
canSendEmail: computed('post.{isPost,email}', 'settings.{mailgunApiKey,mailgunDomain,mailgunBaseUrl}', 'config.mailgunIsConfigured', function () {
|
||||
let mailgunIsConfigured = this.get('settings.mailgunApiKey') && this.get('settings.mailgunDomain') && this.get('settings.mailgunBaseUrl') || this.get('config.mailgunIsConfigured');
|
||||
canSendEmail: computed('hasEmailPermission', 'post.{isPost,email}', 'settings.{editorDefaultEmailRecipients,membersSignupAccess,mailgunIsConfigured}', 'config.mailgunIsConfigured', function () {
|
||||
let isDisabled = this.settings.get('editorDefaultEmailRecipients') === 'disabled' || this.settings.get('membersSignupAccess') === 'none';
|
||||
let mailgunIsConfigured = this.settings.get('mailgunIsConfigured') || this.config.get('mailgunIsConfigured');
|
||||
let isPost = this.post.isPost;
|
||||
let hasSentEmail = !!this.post.email;
|
||||
|
||||
return mailgunIsConfigured && isPost && !hasSentEmail;
|
||||
return this.hasEmailPermission &&
|
||||
!isDisabled &&
|
||||
mailgunIsConfigured &&
|
||||
isPost &&
|
||||
!hasSentEmail;
|
||||
}),
|
||||
|
||||
recipientsSegment: computed('sendEmailWhenPublished', function () {
|
||||
const segmentName = this.sendEmailWhenPublished;
|
||||
return MEMBERS_SEGMENT_MAP.findBy('name', segmentName).segment;
|
||||
}),
|
||||
|
||||
postState: computed('post.{isPublished,isScheduled}', 'forcePublishedMenu', function () {
|
||||
@ -131,6 +154,26 @@ export default Component.extend({
|
||||
return buttonText;
|
||||
}),
|
||||
|
||||
defaultEmailRecipients: computed('settings.{editorDefaultEmailRecipients,editorDefaultEmailRecipientsFilter}', 'post.visibility', function () {
|
||||
const defaultEmailRecipients = this.settings.get('editorDefaultEmailRecipients');
|
||||
|
||||
if (defaultEmailRecipients === 'disabled' || defaultEmailRecipients === 'none') {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
if (defaultEmailRecipients === 'visibility') {
|
||||
if (this.post.visibility === 'public' || this.post.visibility === 'members') {
|
||||
return 'all';
|
||||
}
|
||||
|
||||
if (this.post.visibility === 'paid') {
|
||||
return 'paid';
|
||||
}
|
||||
}
|
||||
|
||||
return MEMBERS_SEGMENT_MAP.findBy('segment', this.settings.get('editorDefaultEmailRecipientsFilter')).name;
|
||||
}),
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
@ -151,17 +194,8 @@ export default Component.extend({
|
||||
}
|
||||
|
||||
this._postStatus = this.postStatus;
|
||||
if (this.postStatus === 'draft' && this.canSendEmail && this.hasEmailPermission) {
|
||||
// Set default newsletter recipients
|
||||
if (this.post.visibility === 'public' || this.post.visibility === 'members') {
|
||||
this.set('sendEmailWhenPublished', 'all');
|
||||
} else {
|
||||
this.set('sendEmailWhenPublished', 'paid');
|
||||
}
|
||||
}
|
||||
|
||||
this.setDefaultSendEmailWhenPublished();
|
||||
this.checkIsSendingEmailLimited();
|
||||
this.countPaidMembers();
|
||||
},
|
||||
|
||||
actions: {
|
||||
@ -187,6 +221,9 @@ export default Component.extend({
|
||||
this._cachePublishedAtBlogTZ();
|
||||
this.set('isClosing', false);
|
||||
this.get('post.errors').clear();
|
||||
|
||||
this.setDefaultSendEmailWhenPublished();
|
||||
|
||||
if (this.onOpen) {
|
||||
this.onOpen();
|
||||
}
|
||||
@ -215,12 +252,14 @@ export default Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
countPaidMembers: action(function () {
|
||||
// TODO: remove editor conditional once editors can query member counts
|
||||
if (!this.session.get('user.isEditor') && this.canSendEmail) {
|
||||
this.countPaidMembersTask.perform();
|
||||
setDefaultSendEmailWhenPublished() {
|
||||
if (this.postStatus === 'draft' && this.canSendEmail) {
|
||||
// Set default newsletter recipients
|
||||
this.set('sendEmailWhenPublished', this.defaultEmailRecipients);
|
||||
} else {
|
||||
this.set('sendEmailWhenPublished', this.post.emailRecipientFilter);
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
checkIsSendingEmailLimited: action(function () {
|
||||
if (this.limit.limiter && this.limit.limiter.isLimited('emails')) {
|
||||
@ -231,34 +270,6 @@ export default Component.extend({
|
||||
}
|
||||
}),
|
||||
|
||||
countPaidMembersTask: task(function* () {
|
||||
const result = yield this.store.query('member', {filter: 'subscribed:true+status:-free', limit: 1, page: 1});
|
||||
const paidMemberCount = result.meta.pagination.total;
|
||||
const freeMemberCount = this.memberCount - paidMemberCount;
|
||||
this.set('paidMemberCount', paidMemberCount);
|
||||
this.set('freeMemberCount', freeMemberCount);
|
||||
|
||||
if (this.postStatus === 'draft' && this.canSendEmail && this.hasEmailPermission) {
|
||||
// Set default newsletter recipients
|
||||
if (this.isSendingEmailLimited) {
|
||||
this.set('sendEmailWhenPublished', 'none');
|
||||
} else if (this.post.visibility === 'public' || this.post.visibility === 'members') {
|
||||
if (paidMemberCount > 0 && freeMemberCount > 0) {
|
||||
this.set('sendEmailWhenPublished', 'all');
|
||||
} else if (!paidMemberCount && freeMemberCount > 0) {
|
||||
this.set('sendEmailWhenPublished', 'free');
|
||||
} else if (!freeMemberCount && paidMemberCount > 0) {
|
||||
this.set('sendEmailWhenPublished', 'paid');
|
||||
} else if (!freeMemberCount && !paidMemberCount) {
|
||||
this.set('sendEmailWhenPublished', 'none');
|
||||
}
|
||||
} else {
|
||||
const type = paidMemberCount > 0 ? 'paid' : 'none';
|
||||
this.set('sendEmailWhenPublished', type);
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
checkIsSendingEmailLimitedTask: task(function* () {
|
||||
try {
|
||||
yield this.limit.limiter.errorIfWouldGoOverLimit('emails');
|
||||
|
@ -10,7 +10,7 @@
|
||||
{{#each @select.selected as |opt idx|}}
|
||||
{{#component (or @extra.tokenComponent "draggable-object")
|
||||
tagName="li"
|
||||
class="ember-power-select-multiple-option"
|
||||
class=(concat "ember-power-select-multiple-option" (if opt.class (concat " token-" opt.class)))
|
||||
select=@select
|
||||
content=(readonly opt)
|
||||
idx=idx
|
||||
|
@ -0,0 +1,71 @@
|
||||
<div class="gh-expandable-block">
|
||||
<h4 class="gh-expandable-header">
|
||||
<div>
|
||||
<h4 class="gh-expandable-title">Default newsletter recipients</h4>
|
||||
<p class="gh-expandable-description">Who do you usually want to send emails to?</p>
|
||||
</div>
|
||||
<button type="button" class="gh-btn" {{on "click" @toggleExpansion}} data-test-toggle="post-access"><span>{{if @expanded "Close" "Expand"}}</span></button>
|
||||
</h4>
|
||||
<div class="gh-expandable-content">
|
||||
{{#liquid-if @expanded}}
|
||||
<div class="flex flex-column w-50">
|
||||
<div class="{{if this.isDisabled "disabled-overlay"}}">
|
||||
<div
|
||||
class="gh-radio {{if this.isDisabledSelected "active"}}"
|
||||
{{on "click" (fn this.setDefaultEmailRecipients "disabled")}}
|
||||
>
|
||||
<div class="gh-radio-button"></div>
|
||||
<div class="gh-radio-content">
|
||||
<div class="gh-radio-label">Disabled</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gh-radio {{if this.isVisibilitySelected "active"}}"
|
||||
{{on "click" (fn this.setDefaultEmailRecipients "visibility")}}
|
||||
>
|
||||
<div class="gh-radio-button"></div>
|
||||
<div class="gh-radio-content">
|
||||
<div class="gh-radio-label">Match post access level</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gh-radio {{if this.isNobodySelected "active"}}"
|
||||
{{on "click" (fn this.setDefaultEmailRecipients "none")}}
|
||||
>
|
||||
<div class="gh-radio-button"></div>
|
||||
<div class="gh-radio-content">
|
||||
<div class="gh-radio-label">Nobody</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gh-radio {{if this.isAllSelected "active"}}"
|
||||
{{on "click" (fn this.setDefaultEmailRecipients "all")}}
|
||||
>
|
||||
<div class="gh-radio-button"></div>
|
||||
<div class="gh-radio-content">
|
||||
<div class="gh-radio-label">All members</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gh-radio {{if this.isFreeSelected "active"}}"
|
||||
{{on "click" (fn this.setDefaultEmailRecipients "free")}}
|
||||
>
|
||||
<div class="gh-radio-button"></div>
|
||||
<div class="gh-radio-content">
|
||||
<div class="gh-radio-label">Free members</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gh-radio {{if this.isPaidSelected "active"}}"
|
||||
{{on "click" (fn this.setDefaultEmailRecipients "paid")}}
|
||||
>
|
||||
<div class="gh-radio-button"></div>
|
||||
<div class="gh-radio-content">
|
||||
<div class="gh-radio-label">Paid members</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/liquid-if}}
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,88 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class SettingsDefaultEmailRecipientsComponent extends Component {
|
||||
@service settings;
|
||||
|
||||
@tracked segmentSelected = false;
|
||||
|
||||
get isDisabled() {
|
||||
return this.settings.get('membersSignupAccess') === 'none';
|
||||
}
|
||||
|
||||
get isDisabledSelected() {
|
||||
return this.isDisabled ||
|
||||
this.settings.get('editorDefaultEmailRecipients') === 'disabled';
|
||||
}
|
||||
|
||||
get isVisibilitySelected() {
|
||||
return !this.isDisabled &&
|
||||
this.settings.get('editorDefaultEmailRecipients') === 'visibility';
|
||||
}
|
||||
|
||||
get isNobodySelected() {
|
||||
return !this.isDisabled &&
|
||||
!this.segmentSelected &&
|
||||
this.settings.get('editorDefaultEmailRecipients') === 'filter' &&
|
||||
this.settings.get('editorDefaultEmailRecipientsFilter') === null;
|
||||
}
|
||||
|
||||
get isAllSelected() {
|
||||
return !this.isDisabled &&
|
||||
!this.segmentSelected &&
|
||||
this.settings.get('editorDefaultEmailRecipients') === 'filter' &&
|
||||
this.settings.get('editorDefaultEmailRecipientsFilter') === 'status:free,status:-free';
|
||||
}
|
||||
|
||||
get isFreeSelected() {
|
||||
return !this.isDisabled &&
|
||||
!this.segmentSelected &&
|
||||
this.settings.get('editorDefaultEmailRecipients') === 'filter' &&
|
||||
this.settings.get('editorDefaultEmailRecipientsFilter') === 'status:free';
|
||||
}
|
||||
|
||||
get isPaidSelected() {
|
||||
return !this.isDisabled &&
|
||||
!this.segmentSelected &&
|
||||
this.settings.get('editorDefaultEmailRecipients') === 'filter' &&
|
||||
this.settings.get('editorDefaultEmailRecipientsFilter') === 'status:-free';
|
||||
}
|
||||
|
||||
get isSegmentSelected() {
|
||||
const isCustomSegment = this.settings.get('editorDefaultEmailRecipients') === 'filter' &&
|
||||
!this.isNobodySelected &&
|
||||
!this.isAllSelected &&
|
||||
!this.isFreeSelected &&
|
||||
!this.isPaidSelected;
|
||||
|
||||
return !this.isDisabled && (this.segmentSelected || isCustomSegment);
|
||||
}
|
||||
|
||||
@action
|
||||
setDefaultEmailRecipients(value) {
|
||||
if (['disabled', 'visibility'].includes(value)) {
|
||||
this.settings.set('editorDefaultEmailRecipients', value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === 'none') {
|
||||
this.settings.set('editorDefaultEmailRecipientsFilter', null);
|
||||
}
|
||||
|
||||
if (value === 'all') {
|
||||
this.settings.set('editorDefaultEmailRecipientsFilter', 'status:free,status:-free');
|
||||
}
|
||||
|
||||
if (value === 'free') {
|
||||
this.settings.set('editorDefaultEmailRecipientsFilter', 'status:free');
|
||||
}
|
||||
|
||||
if (value === 'paid') {
|
||||
this.settings.set('editorDefaultEmailRecipientsFilter', 'status:-free');
|
||||
}
|
||||
|
||||
this.settings.set('editorDefaultEmailRecipients', 'filter');
|
||||
}
|
||||
}
|
@ -7,6 +7,8 @@ import {tracked} from '@glimmer/tracking';
|
||||
export default class MembersAccessController extends Controller {
|
||||
@service settings;
|
||||
|
||||
queryParams = ['signupAccessOpen', 'postAccessOpen']
|
||||
|
||||
@tracked showLeaveSettingsModal = false;
|
||||
@tracked signupAccessOpen = false;
|
||||
@tracked postAccessOpen = false;
|
||||
|
@ -9,11 +9,14 @@ export default class MembersEmailController extends Controller {
|
||||
@service session;
|
||||
@service settings;
|
||||
|
||||
queryParams = ['emailRecipientsOpen']
|
||||
|
||||
// from/supportAddress are set here so that they can be reset to saved values on save
|
||||
// to avoid it looking like they've been saved when they have a separate update process
|
||||
@tracked fromAddress = '';
|
||||
@tracked supportAddress = '';
|
||||
|
||||
@tracked emailRecipientsOpen = false;
|
||||
@tracked showLeaveSettingsModal = false;
|
||||
|
||||
@action
|
||||
@ -21,6 +24,11 @@ export default class MembersEmailController extends Controller {
|
||||
this[property] = email;
|
||||
}
|
||||
|
||||
@action
|
||||
toggleEmailRecipientsOpen() {
|
||||
this.emailRecipientsOpen = !this.emailRecipientsOpen;
|
||||
}
|
||||
|
||||
leaveRoute(transition) {
|
||||
if (this.settings.get('hasDirtyAttributes')) {
|
||||
transition.abort();
|
||||
|
@ -1,6 +1,7 @@
|
||||
/* eslint-disable camelcase */
|
||||
import Model, {attr} from '@ember-data/model';
|
||||
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||
import {and} from '@ember/object/computed';
|
||||
|
||||
export default Model.extend(ValidationEngine, {
|
||||
validationType: 'setting',
|
||||
@ -51,8 +52,8 @@ export default Model.extend(ValidationEngine, {
|
||||
/**
|
||||
* Members settings
|
||||
*/
|
||||
defaultContentVisibility: attr('string'),
|
||||
membersSignupAccess: attr('string'),
|
||||
defaultContentVisibility: attr('string'),
|
||||
membersFromAddress: attr('string'),
|
||||
membersSupportAddress: attr('string'),
|
||||
membersReplyAddress: attr('string'),
|
||||
@ -79,5 +80,12 @@ export default Model.extend(ValidationEngine, {
|
||||
* OAuth settings
|
||||
*/
|
||||
oauthClientId: attr('string'),
|
||||
oauthClientSecret: attr('string')
|
||||
oauthClientSecret: attr('string'),
|
||||
/**
|
||||
* Editor settings
|
||||
*/
|
||||
editorDefaultEmailRecipients: attr('string'),
|
||||
editorDefaultEmailRecipientsFilter: attr('members-segment-string'),
|
||||
|
||||
mailgunIsConfigured: and('mailgunApiKey', 'mailgunDomain', 'mailgunBaseUrl')
|
||||
});
|
||||
|
@ -27,7 +27,7 @@ export default Service.extend(_ProxyMixin, ValidationEngine, {
|
||||
_loadSettings() {
|
||||
if (!this._loadingPromise) {
|
||||
this._loadingPromise = this.store
|
||||
.queryRecord('setting', {group: 'site,theme,private,members,portal,newsletter,email,amp,labs,slack,unsplash,views,firstpromoter,oauth'})
|
||||
.queryRecord('setting', {group: 'site,theme,private,members,portal,newsletter,email,amp,labs,slack,unsplash,views,firstpromoter,oauth,editor'})
|
||||
.then((settings) => {
|
||||
this._loadingPromise = null;
|
||||
return settings;
|
||||
|
@ -256,6 +256,18 @@
|
||||
fill: var(--black);
|
||||
}
|
||||
|
||||
/* Segment input */
|
||||
|
||||
.token-segment-status {
|
||||
background: color-mod(var(--blue) alpha(0.1));
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.token-segment-status svg path {
|
||||
stroke: var(--blue);
|
||||
fill: var(--blue);
|
||||
}
|
||||
|
||||
/* Inside settings / Mailgun region */
|
||||
/* TODO: make these general styles? */
|
||||
|
||||
|
@ -50,7 +50,6 @@
|
||||
@saveTask={{this.save}}
|
||||
@setSaveType={{action "setSaveType"}}
|
||||
@onOpen={{action "cancelAutosave"}}
|
||||
@backgroundTask={{this.backgroundLoader}}
|
||||
@memberCount={{this.memberCount}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
|
@ -21,10 +21,11 @@
|
||||
{{#if this.session.user.isOwner}}
|
||||
<div class="gh-setting-liquid-section">
|
||||
<GhMembersEmailSetting
|
||||
@settings={{this.settings}}
|
||||
@fromAddress={{this.fromAddress}}
|
||||
@supportAddress={{this.supportAddress}}
|
||||
@setEmailAddress={{this.setEmailAddress}}
|
||||
@emailRecipientsExpanded={{this.emailRecipientsOpen}}
|
||||
@toggleEmailRecipientsExpansion={{this.toggleEmailRecipientsOpen}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
29
ghost/admin/app/transforms/members-segment-string.js
Normal file
29
ghost/admin/app/transforms/members-segment-string.js
Normal file
@ -0,0 +1,29 @@
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
|
||||
// the members segment supports `'none'` and `'all'` as special-case options
|
||||
// but that doesn't map well for options in our token select inputs so we
|
||||
// expand/convert them here to make usage elsewhere easier
|
||||
|
||||
export default class MembersSegmentStringTransform extends Transform {
|
||||
deserialize(serialized) {
|
||||
if (serialized === 'all') {
|
||||
return 'status:free,status:-free';
|
||||
}
|
||||
if (serialized === 'none') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return serialized;
|
||||
}
|
||||
|
||||
serialize(deserialized) {
|
||||
if (deserialized === 'status:free,status:-free') {
|
||||
return 'all';
|
||||
}
|
||||
if (!deserialized) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
return deserialized;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user