From da49dc492217bff4387c32cf235d93cf0d005ed3 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Fri, 7 May 2021 10:02:19 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Added=20default=20newsletter=20reci?= =?UTF-8?q?pients=20setting=20(#1946)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../components/gh-members-email-setting.hbs | 5 + .../components/gh-members-segment-count.hbs | 7 ++ .../components/gh-members-segment-count.js | 31 ++++++ .../components/gh-members-segment-select.hbs | 15 +++ .../components/gh-members-segment-select.js | 62 +++++++++++ .../app/components/gh-publishmenu-draft.hbs | 45 ++------ .../app/components/gh-publishmenu-draft.js | 75 +++---------- .../components/gh-publishmenu-scheduled.hbs | 68 ++++-------- .../components/gh-publishmenu-scheduled.js | 43 ------- ghost/admin/app/components/gh-publishmenu.hbs | 15 +-- ghost/admin/app/components/gh-publishmenu.js | 105 ++++++++++-------- .../app/components/gh-token-input/trigger.hbs | 2 +- .../settings/default-email-recipients.hbs | 71 ++++++++++++ .../settings/default-email-recipients.js | 88 +++++++++++++++ .../controllers/settings/members-access.js | 2 + .../app/controllers/settings/members-email.js | 8 ++ ghost/admin/app/models/setting.js | 12 +- ghost/admin/app/services/settings.js | 2 +- .../app/styles/components/power-select.css | 12 ++ ghost/admin/app/templates/editor.hbs | 1 - .../app/templates/settings/members-email.hbs | 3 +- .../app/transforms/members-segment-string.js | 29 +++++ 22 files changed, 450 insertions(+), 251 deletions(-) create mode 100644 ghost/admin/app/components/gh-members-segment-count.hbs create mode 100644 ghost/admin/app/components/gh-members-segment-count.js create mode 100644 ghost/admin/app/components/gh-members-segment-select.hbs create mode 100644 ghost/admin/app/components/gh-members-segment-select.js create mode 100644 ghost/admin/app/components/settings/default-email-recipients.hbs create mode 100644 ghost/admin/app/components/settings/default-email-recipients.js create mode 100644 ghost/admin/app/transforms/members-segment-string.js diff --git a/ghost/admin/app/components/gh-members-email-setting.hbs b/ghost/admin/app/components/gh-members-email-setting.hbs index 66e094915b..6ac10823d7 100644 --- a/ghost/admin/app/components/gh-members-email-setting.hbs +++ b/ghost/admin/app/components/gh-members-email-setting.hbs @@ -16,6 +16,11 @@

Email

+ +
diff --git a/ghost/admin/app/components/gh-members-segment-count.hbs b/ghost/admin/app/components/gh-members-segment-count.hbs new file mode 100644 index 0000000000..94fbfb1f8b --- /dev/null +++ b/ghost/admin/app/components/gh-members-segment-count.hbs @@ -0,0 +1,7 @@ + + {{format-number this.segmentTotal}} / {{format-number this.total}} members + \ No newline at end of file diff --git a/ghost/admin/app/components/gh-members-segment-count.js b/ghost/admin/app/components/gh-members-segment-count.js new file mode 100644 index 0000000000..3dbf1d9c1b --- /dev/null +++ b/ghost/admin/app/components/gh-members-segment-count.js @@ -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; + } +} diff --git a/ghost/admin/app/components/gh-members-segment-select.hbs b/ghost/admin/app/components/gh-members-segment-select.hbs new file mode 100644 index 0000000000..6561f8f15e --- /dev/null +++ b/ghost/admin/app/components/gh-members-segment-select.hbs @@ -0,0 +1,15 @@ + + {{option.name}} + + + \ No newline at end of file diff --git a/ghost/admin/app/components/gh-members-segment-select.js b/ghost/admin/app/components/gh-members-segment-select.js new file mode 100644 index 0000000000..2e73944050 --- /dev/null +++ b/ghost/admin/app/components/gh-members-segment-select.js @@ -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; + } +} diff --git a/ghost/admin/app/components/gh-publishmenu-draft.hbs b/ghost/admin/app/components/gh-publishmenu-draft.hbs index 302abb2508..4d62822a0b 100644 --- a/ghost/admin/app/components/gh-publishmenu-draft.hbs +++ b/ghost/admin/app/components/gh-publishmenu-draft.hbs @@ -29,52 +29,21 @@
- {{#if (and this.canSendEmail showSendEmail)}} + {{#if this.canSendEmail}}
- {{#if this.backgroundLoader.isRunning}} -
- {{else if this.isSendingEmailLimited}} + {{#if this.isSendingEmailLimited}}

{{html-safe this.sendingEmailLimitError}}

{{else}}
-
-

Free members {{this.freeMemberCountLabel}}

-
- -
-
-
-

Paid members {{this.paidMemberCountLabel}}

-
- -
-
+
{{/if}} diff --git a/ghost/admin/app/components/gh-publishmenu-draft.js b/ghost/admin/app/components/gh-publishmenu-draft.js index 81bd8e14d9..b07d410cf1 100644 --- a/ghost/admin/app/components/gh-publishmenu-draft.js +++ b/ghost/admin/app/components/gh-publishmenu-draft.js @@ -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); } }, diff --git a/ghost/admin/app/components/gh-publishmenu-scheduled.hbs b/ghost/admin/app/components/gh-publishmenu-scheduled.hbs index 6dd720b424..13a78b610b 100644 --- a/ghost/admin/app/components/gh-publishmenu-scheduled.hbs +++ b/ghost/admin/app/components/gh-publishmenu-scheduled.hbs @@ -28,55 +28,25 @@
- {{#if (and canSendEmail showSendEmail)}} - {{#unless this.post.email}} -
-
- {{#if this.backgroundLoader.isRunning}} -
- {{else if this.isSendingEmailLimited}} -

{{html-safe this.sendingEmailLimitError}}

- {{else}} -
- -
-
-

Free members

-
- -
-
-
-

Paid members

-
- -
-
-
+ {{#if this.canSendEmail}} +
+
+ {{#if this.isSendingEmailLimited}} +

{{html-safe this.sendingEmailLimitError}}

+ {{else}} +
+ + +
+
- {{/if}} -
-
- {{/unless}} +
+ {{/if}} +
+
{{/if}}
diff --git a/ghost/admin/app/components/gh-publishmenu-scheduled.js b/ghost/admin/app/components/gh-publishmenu-scheduled.js index af3ca7c7b7..4bba770315 100644 --- a/ghost/admin/app/components/gh-publishmenu-scheduled.js +++ b/ghost/admin/app/components/gh-publishmenu-scheduled.js @@ -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'); diff --git a/ghost/admin/app/components/gh-publishmenu.hbs b/ghost/admin/app/components/gh-publishmenu.hbs index 8683cf4aa7..4ed8cf8fc9 100644 --- a/ghost/admin/app/components/gh-publishmenu.hbs +++ b/ghost/admin/app/components/gh-publishmenu.hbs @@ -8,17 +8,15 @@ + @setSaveType={{action "setSaveType"}} /> {{else if (eq this.displayState "scheduled")}} {{/if}} diff --git a/ghost/admin/app/components/gh-publishmenu.js b/ghost/admin/app/components/gh-publishmenu.js index afdedfa0a3..ad0d6bfd08 100644 --- a/ghost/admin/app/components/gh-publishmenu.js +++ b/ghost/admin/app/components/gh-publishmenu.js @@ -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'); diff --git a/ghost/admin/app/components/gh-token-input/trigger.hbs b/ghost/admin/app/components/gh-token-input/trigger.hbs index a3a9a203af..df7d7275b8 100644 --- a/ghost/admin/app/components/gh-token-input/trigger.hbs +++ b/ghost/admin/app/components/gh-token-input/trigger.hbs @@ -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 diff --git a/ghost/admin/app/components/settings/default-email-recipients.hbs b/ghost/admin/app/components/settings/default-email-recipients.hbs new file mode 100644 index 0000000000..b98efeb60f --- /dev/null +++ b/ghost/admin/app/components/settings/default-email-recipients.hbs @@ -0,0 +1,71 @@ +
+

+
+

Default newsletter recipients

+

Who do you usually want to send emails to?

+
+ +

+
+ {{#liquid-if @expanded}} +
+
+
+
+
+
Disabled
+
+
+
+
+
+
Match post access level
+
+
+
+
+
+
Nobody
+
+
+
+
+
+
All members
+
+
+
+
+
+
Free members
+
+
+
+
+
+
Paid members
+
+
+
+
+ {{/liquid-if}} +
+
\ No newline at end of file diff --git a/ghost/admin/app/components/settings/default-email-recipients.js b/ghost/admin/app/components/settings/default-email-recipients.js new file mode 100644 index 0000000000..c2c15d13e2 --- /dev/null +++ b/ghost/admin/app/components/settings/default-email-recipients.js @@ -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'); + } +} diff --git a/ghost/admin/app/controllers/settings/members-access.js b/ghost/admin/app/controllers/settings/members-access.js index 28689eb06a..7f7052f36f 100644 --- a/ghost/admin/app/controllers/settings/members-access.js +++ b/ghost/admin/app/controllers/settings/members-access.js @@ -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; diff --git a/ghost/admin/app/controllers/settings/members-email.js b/ghost/admin/app/controllers/settings/members-email.js index c740456e16..54ff546c64 100644 --- a/ghost/admin/app/controllers/settings/members-email.js +++ b/ghost/admin/app/controllers/settings/members-email.js @@ -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(); diff --git a/ghost/admin/app/models/setting.js b/ghost/admin/app/models/setting.js index 86f3b98584..b84f0367d1 100644 --- a/ghost/admin/app/models/setting.js +++ b/ghost/admin/app/models/setting.js @@ -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') }); diff --git a/ghost/admin/app/services/settings.js b/ghost/admin/app/services/settings.js index 1028652b02..f4007bf14d 100644 --- a/ghost/admin/app/services/settings.js +++ b/ghost/admin/app/services/settings.js @@ -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; diff --git a/ghost/admin/app/styles/components/power-select.css b/ghost/admin/app/styles/components/power-select.css index c47755de09..0c9699b8bc 100644 --- a/ghost/admin/app/styles/components/power-select.css +++ b/ghost/admin/app/styles/components/power-select.css @@ -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? */ diff --git a/ghost/admin/app/templates/editor.hbs b/ghost/admin/app/templates/editor.hbs index f0daeda95e..4df971397c 100644 --- a/ghost/admin/app/templates/editor.hbs +++ b/ghost/admin/app/templates/editor.hbs @@ -50,7 +50,6 @@ @saveTask={{this.save}} @setSaveType={{action "setSaveType"}} @onOpen={{action "cancelAutosave"}} - @backgroundTask={{this.backgroundLoader}} @memberCount={{this.memberCount}} /> {{/if}} diff --git a/ghost/admin/app/templates/settings/members-email.hbs b/ghost/admin/app/templates/settings/members-email.hbs index 170f349478..7a112f3818 100644 --- a/ghost/admin/app/templates/settings/members-email.hbs +++ b/ghost/admin/app/templates/settings/members-email.hbs @@ -21,10 +21,11 @@ {{#if this.session.user.isOwner}}
{{/if}} diff --git a/ghost/admin/app/transforms/members-segment-string.js b/ghost/admin/app/transforms/members-segment-string.js new file mode 100644 index 0000000000..365d5aa13d --- /dev/null +++ b/ghost/admin/app/transforms/members-segment-string.js @@ -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; + } +}