mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 22:43:30 +03:00
da49dc4922
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.
434 lines
14 KiB
JavaScript
434 lines
14 KiB
JavaScript
import Component from '@ember/component';
|
|
import EmailFailedError from 'ghost-admin/errors/email-failed-error';
|
|
import {action} from '@ember/object';
|
|
import {computed} from '@ember/object';
|
|
import {or, reads} from '@ember/object/computed';
|
|
import {inject as service} from '@ember/service';
|
|
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(),
|
|
settings: service(),
|
|
config: service(),
|
|
session: service(),
|
|
store: service(),
|
|
limit: service(),
|
|
|
|
classNames: 'gh-publishmenu',
|
|
displayState: 'draft',
|
|
post: null,
|
|
postStatus: 'draft',
|
|
runningText: null,
|
|
saveTask: null,
|
|
sendEmailWhenPublished: 'none',
|
|
typedDateError: null,
|
|
isSendingEmailLimited: false,
|
|
sendingEmailLimitError: '',
|
|
|
|
_publishedAtBlogTZ: null,
|
|
_previousStatus: null,
|
|
|
|
isClosing: null,
|
|
|
|
onClose() {},
|
|
|
|
forcePublishedMenu: reads('post.pastScheduledTime'),
|
|
|
|
hasEmailPermission: or('session.user.isOwner', 'session.user.isAdmin', 'session.user.isEditor'),
|
|
|
|
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 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 () {
|
|
if (this.forcePublishedMenu || this.get('post.isPublished')) {
|
|
return 'published';
|
|
} else if (this.get('post.isScheduled')) {
|
|
return 'scheduled';
|
|
} else {
|
|
return 'draft';
|
|
}
|
|
}),
|
|
|
|
triggerText: computed('postState', function () {
|
|
let state = this.postState;
|
|
|
|
if (state === 'published') {
|
|
return 'Update';
|
|
} else if (state === 'scheduled') {
|
|
return 'Scheduled';
|
|
} else {
|
|
return 'Publish';
|
|
}
|
|
}),
|
|
|
|
_runningText: computed('postState', 'saveType', function () {
|
|
let saveType = this.saveType;
|
|
let postState = this.postState;
|
|
let runningText;
|
|
|
|
if (postState === 'draft') {
|
|
runningText = saveType === 'publish' ? 'Publishing' : 'Scheduling';
|
|
}
|
|
|
|
if (postState === 'published') {
|
|
runningText = saveType === 'publish' ? 'Updating' : 'Unpublishing';
|
|
}
|
|
|
|
if (postState === 'scheduled') {
|
|
runningText = saveType === 'schedule' ? 'Rescheduling' : 'Unscheduling';
|
|
}
|
|
|
|
return runningText || 'Publishing';
|
|
}),
|
|
|
|
buttonText: computed('postState', 'saveType', function () {
|
|
let saveType = this.saveType;
|
|
let postState = this.postState;
|
|
let buttonText;
|
|
|
|
if (postState === 'draft') {
|
|
buttonText = saveType === 'publish' ? 'Publish' : 'Schedule';
|
|
}
|
|
|
|
if (postState === 'published') {
|
|
buttonText = saveType === 'publish' ? 'Update' : 'Unpublish';
|
|
}
|
|
|
|
if (postState === 'scheduled') {
|
|
buttonText = saveType === 'schedule' ? 'Reschedule' : 'Unschedule';
|
|
}
|
|
|
|
return buttonText || 'Publish';
|
|
}),
|
|
|
|
successText: computed('_previousStatus', 'postState', function () {
|
|
let postState = this.postState;
|
|
let previousStatus = this._previousStatus;
|
|
let buttonText;
|
|
|
|
if (previousStatus === 'draft') {
|
|
buttonText = postState === 'published' ? 'Published' : 'Scheduled';
|
|
}
|
|
|
|
if (previousStatus === 'published') {
|
|
buttonText = postState === 'draft' ? 'Unpublished' : 'Updated';
|
|
}
|
|
|
|
if (previousStatus === 'scheduled') {
|
|
buttonText = postState === 'draft' ? 'Unscheduled' : 'Rescheduled';
|
|
}
|
|
|
|
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);
|
|
|
|
// update the displayState based on the post status but only after a
|
|
// save has finished to avoid swapping the menu prematurely and triggering
|
|
// calls to `setSaveType` due to the component re-rendering
|
|
// TODO: we should have a better way of dealing with this where we don't
|
|
// rely on the side-effect of component rendering calling setSaveType
|
|
let postStatus = this.postStatus;
|
|
if (postStatus !== this._postStatus) {
|
|
if (this.get('saveTask.isRunning')) {
|
|
this.get('saveTask.last').then(() => {
|
|
this.set('displayState', postStatus);
|
|
});
|
|
} else {
|
|
this.set('displayState', postStatus);
|
|
}
|
|
}
|
|
|
|
this._postStatus = this.postStatus;
|
|
this.setDefaultSendEmailWhenPublished();
|
|
this.checkIsSendingEmailLimited();
|
|
},
|
|
|
|
actions: {
|
|
setSaveType(saveType) {
|
|
let post = this.post;
|
|
|
|
this.set('saveType', saveType);
|
|
|
|
if (saveType === 'draft') {
|
|
post.set('statusScratch', 'draft');
|
|
} else if (saveType === 'schedule') {
|
|
post.set('statusScratch', 'scheduled');
|
|
} else if (saveType === 'publish') {
|
|
post.set('statusScratch', 'published');
|
|
}
|
|
},
|
|
|
|
setSendEmailWhenPublished(sendEmailWhenPublished) {
|
|
this.set('sendEmailWhenPublished', sendEmailWhenPublished);
|
|
},
|
|
|
|
open() {
|
|
this._cachePublishedAtBlogTZ();
|
|
this.set('isClosing', false);
|
|
this.get('post.errors').clear();
|
|
|
|
this.setDefaultSendEmailWhenPublished();
|
|
|
|
if (this.onOpen) {
|
|
this.onOpen();
|
|
}
|
|
},
|
|
|
|
close(dropdown, e) {
|
|
// don't close the menu if the datepicker popup or confirm modal is clicked
|
|
if (e) {
|
|
let onDatepicker = !!e.target.closest('.ember-power-datepicker-content');
|
|
let onModal = !!e.target.closest('.fullscreen-modal-container');
|
|
|
|
if (onDatepicker || onModal) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!this._skipDropdownCloseCleanup) {
|
|
this._cleanup();
|
|
}
|
|
this._skipDropdownCloseCleanup = false;
|
|
|
|
this.onClose();
|
|
this.set('isClosing', true);
|
|
|
|
return true;
|
|
}
|
|
},
|
|
|
|
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')) {
|
|
this.checkIsSendingEmailLimitedTask.perform();
|
|
} else {
|
|
this.set('isSendingEmailLimited', false);
|
|
this.set('sendingEmailLimitError', null);
|
|
}
|
|
}),
|
|
|
|
checkIsSendingEmailLimitedTask: task(function* () {
|
|
try {
|
|
yield this.limit.limiter.errorIfWouldGoOverLimit('emails');
|
|
|
|
this.set('isSendingEmailLimited', false);
|
|
this.set('sendingEmailLimitError', null);
|
|
} catch (error) {
|
|
this.set('isSendingEmailLimited', true);
|
|
this.set('sendingEmailLimitError', error.message);
|
|
this.set('sendEmailWhenPublished', 'none');
|
|
}
|
|
}),
|
|
|
|
// action is required because <GhFullscreenModal> only uses actions
|
|
confirmEmailSend: action(function () {
|
|
return this._confirmEmailSend.perform();
|
|
}),
|
|
|
|
_confirmEmailSend: task(function* () {
|
|
this.sendEmailConfirmed = true;
|
|
let post = yield this.save.perform();
|
|
|
|
// simulate a validation error if saving failed so that the confirm
|
|
// modal can react accordingly
|
|
if (!post || post.errors.length) {
|
|
throw null;
|
|
}
|
|
|
|
let pollTimeout = 0;
|
|
if (post.email && post.email.status !== 'submitted') {
|
|
while (pollTimeout < CONFIRM_EMAIL_MAX_POLL_LENGTH) {
|
|
yield timeout(CONFIRM_EMAIL_POLL_LENGTH);
|
|
post = yield post.reload();
|
|
|
|
if (post.email.status === 'submitted') {
|
|
break;
|
|
}
|
|
if (post.email.status === 'failed') {
|
|
throw new EmailFailedError(post.email.error);
|
|
}
|
|
}
|
|
}
|
|
|
|
return post;
|
|
}),
|
|
|
|
retryEmailSend: action(function () {
|
|
return this._retryEmailSend.perform();
|
|
}),
|
|
|
|
_retryEmailSend: task(function* () {
|
|
if (!this.post.email) {
|
|
return;
|
|
}
|
|
|
|
let email = yield this.post.email.retry();
|
|
|
|
let pollTimeout = 0;
|
|
if (email && email.status !== 'submitted') {
|
|
while (pollTimeout < CONFIRM_EMAIL_POLL_LENGTH) {
|
|
yield timeout(CONFIRM_EMAIL_POLL_LENGTH);
|
|
email = yield email.reload();
|
|
|
|
if (email.status === 'submitted') {
|
|
break;
|
|
}
|
|
if (email.status === 'failed') {
|
|
throw new EmailFailedError(email.error);
|
|
}
|
|
}
|
|
}
|
|
|
|
return email;
|
|
}),
|
|
|
|
openEmailConfirmationModal: action(function (dropdown) {
|
|
if (dropdown) {
|
|
this._skipDropdownCloseCleanup = true;
|
|
dropdown.actions.close();
|
|
}
|
|
this.set('showEmailConfirmationModal', true);
|
|
}),
|
|
|
|
closeEmailConfirmationModal: action(function () {
|
|
this.set('showEmailConfirmationModal', false);
|
|
this._cleanup();
|
|
}),
|
|
|
|
save: task(function* ({dropdown} = {}) {
|
|
let {
|
|
post,
|
|
sendEmailWhenPublished,
|
|
sendEmailConfirmed,
|
|
saveType,
|
|
typedDateError
|
|
} = this;
|
|
|
|
// don't allow save if an invalid schedule date is present
|
|
if (typedDateError) {
|
|
return false;
|
|
}
|
|
|
|
// validate publishedAtBlog to avoid an alert when saving for already displayed errors
|
|
// important to do this before opening email confirmation modal too
|
|
try {
|
|
yield post.validate({property: 'publishedAtBlog'});
|
|
} catch (error) {
|
|
// re-throw if we don't have a validation error
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
post.status === 'draft' &&
|
|
!post.email && // email sent previously
|
|
sendEmailWhenPublished && sendEmailWhenPublished !== 'none' &&
|
|
!sendEmailConfirmed // set once confirmed so normal save happens
|
|
) {
|
|
this.openEmailConfirmationModal(dropdown);
|
|
return;
|
|
}
|
|
|
|
this.sendEmailConfirmed = false;
|
|
|
|
// runningText needs to be declared before the other states change during the
|
|
// save action.
|
|
this.set('runningText', this._runningText);
|
|
this.set('_previousStatus', this.get('post.status'));
|
|
this.setSaveType(saveType);
|
|
|
|
try {
|
|
// will show alert for non-date related failed validations
|
|
post = yield this.saveTask.perform({sendEmailWhenPublished});
|
|
|
|
this._cachePublishedAtBlogTZ();
|
|
return post;
|
|
} catch (error) {
|
|
// re-throw if we don't have a validation error
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
}),
|
|
|
|
_cachePublishedAtBlogTZ() {
|
|
this._publishedAtBlogTZ = this.get('post.publishedAtBlogTZ');
|
|
},
|
|
|
|
_cleanup() {
|
|
this.set('showConfirmEmailModal', false);
|
|
|
|
// when closing the menu we reset the publishedAtBlogTZ date so that the
|
|
// unsaved changes made to the scheduled date aren't reflected in the PSM
|
|
this.post.set('publishedAtBlogTZ', this._publishedAtBlogTZ);
|
|
|
|
this.post.set('statusScratch', null);
|
|
this.post.validate();
|
|
}
|
|
});
|