mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-26 04:13:30 +03:00
🎨 Added confirmation dialog any time a post/page will be published
refs https://github.com/TryGhost/Team/issues/1169 Previously we were only showing a confirmation dialog if a publish action would trigger an email which was inconsistent and did not instil confidence when publishing. - replaced old `modal-confirm-email-send` modal with the newer-style `modals/editor/confirm-publish` component - updated to handle standard publish in addition to email publish - updated copy - added "error" state when attempting to send email-only post to no members - updated publish menu `save` task to open the confirm modal when going from `draft` to `published` or `scheduled` - underlying save with email polling moved to `_saveTask` so it can be re-used across `save` task (when not publishing) and when confirming from the modal
This commit is contained in:
parent
fc00dc1abc
commit
98b5506d64
@ -66,21 +66,6 @@
|
|||||||
</GhBasicDropdown>
|
</GhBasicDropdown>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if this.showEmailConfirmationModal}}
|
|
||||||
<GhFullscreenModal
|
|
||||||
@modal="confirm-email-send"
|
|
||||||
@model={{hash
|
|
||||||
sendEmailWhenPublished=this.sendEmailWhenPublished
|
|
||||||
isScheduled=(eq this.saveType "schedule")
|
|
||||||
emailOnly=this.emailOnly
|
|
||||||
retryEmailSend=this.retryEmailSend
|
|
||||||
}}
|
|
||||||
@confirm={{this.confirmEmailSend}}
|
|
||||||
@close={{this.closeEmailConfirmationModal}}
|
|
||||||
@modifier="action wide"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{!--
|
{{!--
|
||||||
Workaround to have an always-shown element to attach key handlers to.
|
Workaround to have an always-shown element to attach key handlers to.
|
||||||
TODO: Move onto main element once converted to a glimmer component
|
TODO: Move onto main element once converted to a glimmer component
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Component from '@ember/component';
|
import Component from '@ember/component';
|
||||||
import EmailFailedError from 'ghost-admin/errors/email-failed-error';
|
import EmailFailedError from 'ghost-admin/errors/email-failed-error';
|
||||||
import {action} from '@ember/object';
|
import {bind} from '@ember/runloop';
|
||||||
import {computed} from '@ember/object';
|
import {computed} from '@ember/object';
|
||||||
import {or, reads} from '@ember/object/computed';
|
import {or, reads} from '@ember/object/computed';
|
||||||
import {schedule} from '@ember/runloop';
|
import {schedule} from '@ember/runloop';
|
||||||
@ -12,12 +12,13 @@ const CONFIRM_EMAIL_MAX_POLL_LENGTH = 15 * 1000;
|
|||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
clock: service(),
|
clock: service(),
|
||||||
feature: service(),
|
|
||||||
settings: service(),
|
|
||||||
config: service(),
|
config: service(),
|
||||||
session: service(),
|
feature: service(),
|
||||||
store: service(),
|
|
||||||
limit: service(),
|
limit: service(),
|
||||||
|
modals: service(),
|
||||||
|
session: service(),
|
||||||
|
settings: service(),
|
||||||
|
store: service(),
|
||||||
|
|
||||||
classNames: 'gh-publishmenu',
|
classNames: 'gh-publishmenu',
|
||||||
displayState: 'draft',
|
displayState: 'draft',
|
||||||
@ -297,7 +298,7 @@ export default Component.extend({
|
|||||||
|
|
||||||
// wait for actions to be triggered by the focusout/blur before saving
|
// wait for actions to be triggered by the focusout/blur before saving
|
||||||
schedule('actions', this, function () {
|
schedule('actions', this, function () {
|
||||||
this.send('setSaveType', 'published');
|
this.send('setSaveType', 'publish');
|
||||||
this.save.perform();
|
this.save.perform();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -336,98 +337,15 @@ export default Component.extend({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 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();
|
|
||||||
}),
|
|
||||||
|
|
||||||
reloadSettingsTask: task(function* () {
|
reloadSettingsTask: task(function* () {
|
||||||
yield this.settings.reload();
|
yield this.settings.reload();
|
||||||
}),
|
}),
|
||||||
|
|
||||||
save: task(function* ({dropdown} = {}) {
|
save: task(function* (options = {}) {
|
||||||
let {
|
const {post, saveType} = this;
|
||||||
post,
|
|
||||||
emailOnly,
|
|
||||||
sendEmailWhenPublished,
|
|
||||||
sendEmailConfirmed,
|
|
||||||
saveType,
|
|
||||||
typedDateError,
|
|
||||||
distributionAction
|
|
||||||
} = this;
|
|
||||||
|
|
||||||
// don't allow save if an invalid schedule date is present
|
// don't allow save if an invalid schedule date is present
|
||||||
if (typedDateError) {
|
if (this.typedDateError) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,18 +361,66 @@ export default Component.extend({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
// always opens publish confirmation if post will be published/scheduled
|
||||||
post.status === 'draft' &&
|
if (post.status === 'draft' && (saveType === 'publish' || saveType === 'schedule')) {
|
||||||
!post.email && // email sent previously
|
if (options.dropdown) {
|
||||||
sendEmailWhenPublished && sendEmailWhenPublished !== 'none' &&
|
this._skipDropdownCloseCleanup = true;
|
||||||
distributionAction !== 'publish' &&
|
options.dropdown.actions.close();
|
||||||
!sendEmailConfirmed // set once confirmed so normal save happens
|
}
|
||||||
) {
|
|
||||||
this.openEmailConfirmationModal(dropdown);
|
return yield this.modals.open('modals/editor/confirm-publish', {
|
||||||
|
post: this.post,
|
||||||
|
emailOnly: this.emailOnly,
|
||||||
|
sendEmailWhenPublished: this.sendEmailWhenPublished,
|
||||||
|
isScheduled: saveType === 'schedule',
|
||||||
|
confirm: this.saveWithConfirmedPublish.perform,
|
||||||
|
retryEmailSend: this.retryEmailSendTask.perform
|
||||||
|
}, {
|
||||||
|
beforeClose: bind(this, this._cleanup)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return yield this._saveTask.perform(options);
|
||||||
|
}),
|
||||||
|
|
||||||
|
saveWithConfirmedPublish: task(function* () {
|
||||||
|
return yield this._saveTask.perform();
|
||||||
|
}),
|
||||||
|
|
||||||
|
retryEmailSendTask: task(function* () {
|
||||||
|
if (!this.post.email) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendEmailConfirmed = false;
|
let email = yield this.post.email.retry();
|
||||||
|
|
||||||
|
let pollTimeout = 0;
|
||||||
|
if (email && email.status !== 'submitted') {
|
||||||
|
while (pollTimeout < CONFIRM_EMAIL_MAX_POLL_LENGTH) {
|
||||||
|
yield timeout(CONFIRM_EMAIL_POLL_LENGTH);
|
||||||
|
pollTimeout += CONFIRM_EMAIL_POLL_LENGTH;
|
||||||
|
|
||||||
|
email = yield email.reload();
|
||||||
|
|
||||||
|
if (email.status === 'submitted') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (email.status === 'failed') {
|
||||||
|
throw new EmailFailedError(email.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return email;
|
||||||
|
}),
|
||||||
|
|
||||||
|
_saveTask: task(function* () {
|
||||||
|
let {
|
||||||
|
post,
|
||||||
|
emailOnly,
|
||||||
|
sendEmailWhenPublished,
|
||||||
|
saveType
|
||||||
|
} = this;
|
||||||
|
|
||||||
// runningText needs to be declared before the other states change during the
|
// runningText needs to be declared before the other states change during the
|
||||||
// save action.
|
// save action.
|
||||||
@ -467,6 +433,28 @@ export default Component.extend({
|
|||||||
post = yield this.saveTask.perform({sendEmailWhenPublished, emailOnly});
|
post = yield this.saveTask.perform({sendEmailWhenPublished, emailOnly});
|
||||||
|
|
||||||
this._cachePublishedAtBlogTZ();
|
this._cachePublishedAtBlogTZ();
|
||||||
|
|
||||||
|
if (sendEmailWhenPublished && sendEmailWhenPublished !== 'none') {
|
||||||
|
let pollTimeout = 0;
|
||||||
|
if (post.email && post.email.status !== 'submitted') {
|
||||||
|
while (pollTimeout < CONFIRM_EMAIL_MAX_POLL_LENGTH) {
|
||||||
|
yield timeout(CONFIRM_EMAIL_POLL_LENGTH);
|
||||||
|
pollTimeout += 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._cleanup();
|
||||||
|
|
||||||
return post;
|
return post;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// re-throw if we don't have a validation error
|
// re-throw if we don't have a validation error
|
||||||
@ -482,7 +470,6 @@ export default Component.extend({
|
|||||||
|
|
||||||
_cleanup() {
|
_cleanup() {
|
||||||
this.set('distributionAction', 'publish_send');
|
this.set('distributionAction', 'publish_send');
|
||||||
this.set('showConfirmEmailModal', false);
|
|
||||||
|
|
||||||
// when closing the menu we reset the publishedAtBlogTZ date so that the
|
// 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
|
// unsaved changes made to the scheduled date aren't reflected in the PSM
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
{{#unless this.errorMessage}}
|
|
||||||
<header class="modal-header" data-test-modal="delete-user">
|
|
||||||
<h1>Ready to go? Here’s what happens next</h1>
|
|
||||||
</header>
|
|
||||||
<button class="close" title="Close" {{on "click" this.closeModal}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
|
|
||||||
|
|
||||||
<div class="modal-body" {{did-insert this.countRecipients}}>
|
|
||||||
{{#if this.countRecipientsTask.isRunning}}
|
|
||||||
<div class="flex flex-column items-center">
|
|
||||||
<div class="gh-loading-spinner"></div>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<p>
|
|
||||||
Your post will be delivered to
|
|
||||||
<strong>{{this.memberCount}}</strong>
|
|
||||||
{{#if this.model.emailOnly}}
|
|
||||||
but it will <strong>not</strong>
|
|
||||||
{{else}}
|
|
||||||
and will
|
|
||||||
{{/if}}
|
|
||||||
be published on your site{{#if this.model.isScheduled}} at the scheduled time{{/if}}. Sound good?
|
|
||||||
</p>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button {{on "click" this.closeModal}} class="gh-btn" data-test-button="cancel-publish-and-email">
|
|
||||||
<span>Cancel</span>
|
|
||||||
</button>
|
|
||||||
{{#if this.model.isScheduled}}
|
|
||||||
<GhTaskButton
|
|
||||||
@disabled={{this.countRecipientsTask.isRunning}}
|
|
||||||
@buttonText="Schedule"
|
|
||||||
@runningText="Scheduling..."
|
|
||||||
@task={{this.confirmAndCheckErrorTask}}
|
|
||||||
@class="gh-btn gh-btn-black gh-btn-icon"
|
|
||||||
data-test-button="confirm-publish-and-email"
|
|
||||||
/>
|
|
||||||
{{else}}
|
|
||||||
<GhTaskButton
|
|
||||||
@disabled={{this.countRecipientsTask.isRunning}}
|
|
||||||
@buttonText={{if this.model.emailOnly "Send" "Publish and send"}}
|
|
||||||
@runningText={{if this.model.emailOnly "Sending..." "Publishing..."}}
|
|
||||||
@task={{this.confirmAndCheckErrorTask}}
|
|
||||||
@class="gh-btn gh-btn-black gh-btn-icon"
|
|
||||||
data-test-button="confirm-publish-and-email"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{else}}
|
|
||||||
<header class="modal-header" data-test-modal="delete-user">
|
|
||||||
<h1>Failed to send email</h1>
|
|
||||||
</header>
|
|
||||||
<button class="close" title="Close" {{on "click" this.closeModal}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>Your post has been published but the email failed to send. Please verify your email settings if the error persists.</p>
|
|
||||||
<p class="mb0">
|
|
||||||
<button type="button" class="gh-btn gh-btn-text regular" {{action (toggle "errorDetailsOpen" this)}} data-test-toggle-error>
|
|
||||||
{{#if this.errorDetailsOpen}}
|
|
||||||
{{svg-jar "arrow-down" class="nudge-top--2 w2 h2 fill-darkgrey mr1"}}
|
|
||||||
{{else}}
|
|
||||||
{{svg-jar "arrow-right" class="nudge-top--1 w2 h2 fill-darkgrey mr1"}}
|
|
||||||
{{/if}}
|
|
||||||
<span>Error details</span>
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{{#liquid-if this.errorDetailsOpen}}
|
|
||||||
<p class="error gh-box gh-box-error mt3 mb3">
|
|
||||||
{{svg-jar "warning"}}
|
|
||||||
{{this.errorMessage}}
|
|
||||||
</p>
|
|
||||||
{{/liquid-if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button {{on "click" this.closeModal}} class="gh-btn" data-test-button="cancel-publish-and-email">
|
|
||||||
<span>Close</span>
|
|
||||||
</button>
|
|
||||||
<GhTaskButton
|
|
||||||
@buttonText="Retry email"
|
|
||||||
@runningText="Sending..."
|
|
||||||
@task={{this.retryEmailTask}}
|
|
||||||
@class="gh-btn gh-btn-red gh-btn-icon"
|
|
||||||
data-test-button="retry-email"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{/unless}}
|
|
@ -1,80 +0,0 @@
|
|||||||
import ModalComponent from 'ghost-admin/components/modal-base';
|
|
||||||
import {action} from '@ember/object';
|
|
||||||
import {inject as service} from '@ember/service';
|
|
||||||
import {task} from 'ember-concurrency';
|
|
||||||
|
|
||||||
export default ModalComponent.extend({
|
|
||||||
membersCountCache: service(),
|
|
||||||
session: service(),
|
|
||||||
store: service(),
|
|
||||||
|
|
||||||
errorMessage: null,
|
|
||||||
memberCount: null,
|
|
||||||
|
|
||||||
// Allowed actions
|
|
||||||
confirm: () => {},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
confirm() {
|
|
||||||
if (this.errorMessage) {
|
|
||||||
return this.retryEmailTask.perform();
|
|
||||||
} else {
|
|
||||||
if (!this.countRecipientsTask.isRunning) {
|
|
||||||
return this.confirmAndCheckErrorTask.perform();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
countRecipients: action(function () {
|
|
||||||
this.countRecipientsTask.perform();
|
|
||||||
}),
|
|
||||||
|
|
||||||
countRecipientsTask: task(function* () {
|
|
||||||
const {sendEmailWhenPublished} = this.model;
|
|
||||||
const filter = `subscribed:true+(${sendEmailWhenPublished})`;
|
|
||||||
const result = sendEmailWhenPublished ? yield this.membersCountCache.countString(filter) : 'no members';
|
|
||||||
this.set('memberCount', result);
|
|
||||||
}),
|
|
||||||
|
|
||||||
confirmAndCheckErrorTask: task(function* () {
|
|
||||||
try {
|
|
||||||
yield this.confirm();
|
|
||||||
this.closeModal();
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
// switch to "failed" state if email fails
|
|
||||||
if (e && e.name === 'EmailFailedError') {
|
|
||||||
this.set('errorMessage', e.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// close modal and continue with normal error handling if it was
|
|
||||||
// a non-email-related error
|
|
||||||
this.closeModal();
|
|
||||||
if (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
retryEmailTask: task(function* () {
|
|
||||||
try {
|
|
||||||
yield this.model.retryEmailSend();
|
|
||||||
this.closeModal();
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
// update "failed" state if email fails again
|
|
||||||
if (e && e.name === 'EmailFailedError') {
|
|
||||||
this.set('errorMessage', e.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: test a non-email failure - maybe this needs to go through
|
|
||||||
// the notifications service
|
|
||||||
if (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
116
ghost/admin/app/components/modals/editor/confirm-publish.hbs
Normal file
116
ghost/admin/app/components/modals/editor/confirm-publish.hbs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<div class="modal-content" data-test-modal="editor/confirm-publish">
|
||||||
|
{{#unless this.errorMessage}}
|
||||||
|
<header class="modal-header" data-test-state="confirm-publish">
|
||||||
|
<h1>Ready to go? Here’s what happens next</h1>
|
||||||
|
</header>
|
||||||
|
<button class="close" title="Close" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
|
||||||
|
|
||||||
|
<div class="modal-body" {{did-insert this.countRecipientsTask.perform}}>
|
||||||
|
{{#if (eq @data.post.displayName 'page')}}
|
||||||
|
<p>
|
||||||
|
Your page will be published {{if @data.isScheduled "at the scheduled time" "immediately"}}. Sound good?
|
||||||
|
</p>
|
||||||
|
{{else if this.isPublishOnly}}
|
||||||
|
<p>
|
||||||
|
Your post will be published {{if @data.isScheduled "at the scheduled time" "immediately"}}
|
||||||
|
and won't be sent as an email. Sound good?
|
||||||
|
</p>
|
||||||
|
{{else}}
|
||||||
|
{{#if this.countRecipientsTask.isRunning}}
|
||||||
|
<div class="flex flex-column items-center">
|
||||||
|
<div class="gh-loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
{{#if this.isEmailOnlyWithNoMembers}}
|
||||||
|
<p>
|
||||||
|
You're trying to {{if @data.isScheduled "schedule" "send"}} a post
|
||||||
|
as an email newsletter with <strong>0 members</strong> selected.
|
||||||
|
Choose a segment of your audience and try again!
|
||||||
|
</p>
|
||||||
|
{{else}}
|
||||||
|
<p>
|
||||||
|
Your post will be delivered to <strong>{{this.memberCountString}}</strong>
|
||||||
|
{{#if @data.emailOnly}}
|
||||||
|
but it will <strong>not</strong>
|
||||||
|
{{else}}
|
||||||
|
and will
|
||||||
|
{{/if}}
|
||||||
|
be published on your site{{#if @data.isScheduled}} at the scheduled time{{/if}}. Sound good?
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
{{#if this.isEmailOnlyWithNoMembers}}
|
||||||
|
<button type="button" class="gh-btn" {{on "click" @close}} data-test-button="cancel-email-with-no-members">
|
||||||
|
<span>Close</span>
|
||||||
|
</button>
|
||||||
|
{{else}}
|
||||||
|
<button {{on "click" @close}} class="gh-btn" data-test-button="cancel-publish-and-email">
|
||||||
|
<span>Cancel</span>
|
||||||
|
</button>
|
||||||
|
{{#if @data.isScheduled}}
|
||||||
|
<GhTaskButton
|
||||||
|
@disabled={{this.countRecipientsTask.isRunning}}
|
||||||
|
@buttonText="Schedule"
|
||||||
|
@runningText="Scheduling..."
|
||||||
|
@task={{this.confirmAndCheckErrorTask}}
|
||||||
|
@class="gh-btn gh-btn-black gh-btn-icon"
|
||||||
|
data-test-button="confirm-schedule"
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
<GhTaskButton
|
||||||
|
@disabled={{this.countRecipientsTask.isRunning}}
|
||||||
|
@buttonText={{this.publishAndSendButtonText}}
|
||||||
|
@runningText={{if @data.emailOnly "Sending..." "Publishing..."}}
|
||||||
|
@task={{this.confirmAndCheckErrorTask}}
|
||||||
|
@class="gh-btn gh-btn-black gh-btn-icon"
|
||||||
|
data-test-button="confirm-publish"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{else}}
|
||||||
|
<header class="modal-header" data-test-state="failed-send">
|
||||||
|
<h1>Failed to send email</h1>
|
||||||
|
</header>
|
||||||
|
<button class="close" title="Close" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Your post has been published but the email failed to send. Please verify your email settings if the error persists.</p>
|
||||||
|
<p class="mb0">
|
||||||
|
<button type="button" class="gh-btn gh-btn-text regular" {{on "click" this.toggleErrorDetails}} data-test-toggle-error>
|
||||||
|
{{#if this.errorDetailsOpen}}
|
||||||
|
{{svg-jar "arrow-down" class="nudge-top--2 w2 h2 fill-darkgrey mr1"}}
|
||||||
|
{{else}}
|
||||||
|
{{svg-jar "arrow-right" class="nudge-top--1 w2 h2 fill-darkgrey mr1"}}
|
||||||
|
{{/if}}
|
||||||
|
<span>Error details</span>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{#liquid-if this.errorDetailsOpen}}
|
||||||
|
<p class="error gh-box gh-box-error mt3 mb3">
|
||||||
|
{{svg-jar "warning"}}
|
||||||
|
{{this.errorMessage}}
|
||||||
|
</p>
|
||||||
|
{{/liquid-if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button {{on "click" @close}} class="gh-btn" data-test-button="cancel-publish-and-email">
|
||||||
|
<span>Close</span>
|
||||||
|
</button>
|
||||||
|
<GhTaskButton
|
||||||
|
@buttonText="Retry email"
|
||||||
|
@runningText="Sending..."
|
||||||
|
@task={{this.retryEmailTask}}
|
||||||
|
@class="gh-btn gh-btn-red gh-btn-icon"
|
||||||
|
data-test-button="retry-email"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/unless}}
|
||||||
|
</div>
|
117
ghost/admin/app/components/modals/editor/confirm-publish.js
Normal file
117
ghost/admin/app/components/modals/editor/confirm-publish.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
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 ModalsEditorConfirmPublishComponent extends Component {
|
||||||
|
@service membersCountCache;
|
||||||
|
@service session;
|
||||||
|
@service store;
|
||||||
|
|
||||||
|
@tracked errorMessage = null;
|
||||||
|
@tracked errorDetailsOpen = false;
|
||||||
|
@tracked memberCount = null;
|
||||||
|
@tracked memberCountString = null;
|
||||||
|
|
||||||
|
get isPublishOnly() {
|
||||||
|
return this.args.data.sendEmailWhenPublished === 'none'
|
||||||
|
|| this.args.data.post.displayName === 'page'
|
||||||
|
|| this.args.data.post.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isEmailOnly() {
|
||||||
|
return this.args.data.emailOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isEmailOnlyWithNoMembers() {
|
||||||
|
return this.isEmailOnly && this.memberCount === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get publishAndSendButtonText() {
|
||||||
|
if (this.isEmailOnly) {
|
||||||
|
return 'Send';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isPublishOnly || this.memberCount === 0) {
|
||||||
|
return 'Publish';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Publish and Send';
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
confirm() {
|
||||||
|
if (this.errorMessage) {
|
||||||
|
return this.retryEmailTask.perform();
|
||||||
|
} else {
|
||||||
|
if (!this.countRecipientsTask.isRunning) {
|
||||||
|
return this.confirmAndCheckErrorTask.perform();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleErrorDetails() {
|
||||||
|
this.errorDetailsOpen = !this.errorDetailsOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
@task
|
||||||
|
*countRecipientsTask() {
|
||||||
|
const {sendEmailWhenPublished} = this.args.data;
|
||||||
|
|
||||||
|
if (sendEmailWhenPublished === 'none') {
|
||||||
|
this.memberCount = 0;
|
||||||
|
this.memberCountString = 'no members';
|
||||||
|
}
|
||||||
|
|
||||||
|
const filter = `subscribed:true+(${sendEmailWhenPublished})`;
|
||||||
|
|
||||||
|
this.memberCount = sendEmailWhenPublished ? yield this.membersCountCache.count(filter) : 0;
|
||||||
|
this.memberCountString = sendEmailWhenPublished ? yield this.membersCountCache.countString(filter) : 'no members';
|
||||||
|
}
|
||||||
|
|
||||||
|
@task
|
||||||
|
*confirmAndCheckErrorTask() {
|
||||||
|
try {
|
||||||
|
yield this.args.data.confirm();
|
||||||
|
this.args.close();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
// switch to "failed" state if email fails
|
||||||
|
if (e && e.name === 'EmailFailedError') {
|
||||||
|
this.errorMessage = e.message;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// close modal and continue with normal error handling if it was
|
||||||
|
// a non-email-related error
|
||||||
|
this.args.close();
|
||||||
|
|
||||||
|
if (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@task
|
||||||
|
*retryEmailTask() {
|
||||||
|
try {
|
||||||
|
yield this.args.data.retryEmailSend();
|
||||||
|
this.args.close();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
// update "failed" state if email fails again
|
||||||
|
if (e && e.name === 'EmailFailedError') {
|
||||||
|
this.errorMessage = e.message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: test a non-email failure - maybe this needs to go through
|
||||||
|
// the notifications service
|
||||||
|
if (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -208,8 +208,10 @@ describe('Acceptance: Editor', function () {
|
|||||||
'draft post publish button text'
|
'draft post publish button text'
|
||||||
).to.equal('Publish');
|
).to.equal('Publish');
|
||||||
|
|
||||||
// Publish the post
|
// Publish the post and re-open publish menu
|
||||||
await click('[data-test-publishmenu-save]');
|
await click('[data-test-publishmenu-save]');
|
||||||
|
await click('[data-test-button="confirm-publish"]');
|
||||||
|
await click('[data-test-publishmenu-trigger]');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
find('[data-test-publishmenu-save]').textContent.trim(),
|
find('[data-test-publishmenu-save]').textContent.trim(),
|
||||||
@ -341,6 +343,8 @@ describe('Acceptance: Editor', function () {
|
|||||||
|
|
||||||
await datepickerSelect('[data-test-publishmenu-draft] [data-test-date-time-picker-datepicker]', new Date(newFutureTime.format().replace(/\+.*$/, '')));
|
await datepickerSelect('[data-test-publishmenu-draft] [data-test-date-time-picker-datepicker]', new Date(newFutureTime.format().replace(/\+.*$/, '')));
|
||||||
await click('[data-test-publishmenu-save]');
|
await click('[data-test-publishmenu-save]');
|
||||||
|
await click('[data-test-button="confirm-schedule"]');
|
||||||
|
await click('[data-test-publishmenu-trigger]');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
find('[data-test-publishmenu-save]').textContent.trim(),
|
find('[data-test-publishmenu-save]').textContent.trim(),
|
||||||
@ -451,6 +455,7 @@ describe('Acceptance: Editor', function () {
|
|||||||
await blur('[data-test-publishmenu-draft] [data-test-date-time-picker-time-input]');
|
await blur('[data-test-publishmenu-draft] [data-test-date-time-picker-time-input]');
|
||||||
|
|
||||||
await click('[data-test-publishmenu-save]');
|
await click('[data-test-publishmenu-save]');
|
||||||
|
await click('[data-test-button="confirm-schedule"]');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
findAll('.gh-alert').length,
|
findAll('.gh-alert').length,
|
||||||
@ -475,6 +480,7 @@ describe('Acceptance: Editor', function () {
|
|||||||
await fillIn('[data-test-editor-title-input]', Array(260).join('a'));
|
await fillIn('[data-test-editor-title-input]', Array(260).join('a'));
|
||||||
await click('[data-test-publishmenu-trigger]');
|
await click('[data-test-publishmenu-trigger]');
|
||||||
await click('[data-test-publishmenu-save]');
|
await click('[data-test-publishmenu-save]');
|
||||||
|
await click('[data-test-button="confirm-publish"]');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
findAll('.gh-alert').length,
|
findAll('.gh-alert').length,
|
||||||
|
@ -38,7 +38,8 @@ describe('Acceptance: Error Handling', function () {
|
|||||||
await visit('/posts');
|
await visit('/posts');
|
||||||
await click('.posts-list li:nth-of-type(2) a'); // select second post
|
await click('.posts-list li:nth-of-type(2) a'); // select second post
|
||||||
await click('[data-test-publishmenu-trigger]');
|
await click('[data-test-publishmenu-trigger]');
|
||||||
await click('[data-test-publishmenu-save]'); // "Save post"
|
await click('[data-test-publishmenu-save]');
|
||||||
|
await click('[data-test-button="confirm-publish"]'); // "Save post"
|
||||||
|
|
||||||
// has the refresh to update alert
|
// has the refresh to update alert
|
||||||
expect(findAll('.gh-alert').length).to.equal(1);
|
expect(findAll('.gh-alert').length).to.equal(1);
|
||||||
@ -100,6 +101,7 @@ describe('Acceptance: Error Handling', function () {
|
|||||||
await visit('/editor/post/1');
|
await visit('/editor/post/1');
|
||||||
await click('[data-test-publishmenu-trigger]');
|
await click('[data-test-publishmenu-trigger]');
|
||||||
await click('[data-test-publishmenu-save]');
|
await click('[data-test-publishmenu-save]');
|
||||||
|
await click('[data-test-button="confirm-publish"]');
|
||||||
|
|
||||||
expect(findAll('.gh-alert').length).to.equal(1);
|
expect(findAll('.gh-alert').length).to.equal(1);
|
||||||
expect(find('.gh-alert').textContent).to.not.match(/html>/);
|
expect(find('.gh-alert').textContent).to.not.match(/html>/);
|
||||||
|
Loading…
Reference in New Issue
Block a user