Added polling when confirming email to show immediate error

no issue

- when confirming email send, after initial save in, poll every second for a maximum of 10 seconds and check the status of the email
  - if it's `'success'` close the modal immediately
  - if it's `'failure'` switch the confirm modal to an error state
  - if the save fails for some other reason (validation, server error) close the modal immediately and let the normal editor error handling do it's thing
- fixed confirm modal not appearing when retrying a save after a post validation failed
- show email status in post status area
    - `"and sending to x members"` when email is pending or submitting
    - `"and sent to x members"` once email is fully submitted
This commit is contained in:
Kevin Ansfield 2019-11-20 23:23:23 +00:00
parent 31b5b9319d
commit 30b23f2a7c
6 changed files with 134 additions and 55 deletions

View File

@ -1,9 +1,13 @@
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 {reads} from '@ember/object/computed';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {task, timeout} from 'ember-concurrency';
const CONFIRM_EMAIL_POLL_LENGTH = 1000;
const CONFIRM_EMAIL_MAX_POLL_LENGTH = 10 * 1000;
export default Component.extend({
clock: service(),
@ -184,8 +188,30 @@ export default Component.extend({
_confirmEmailSend: task(function* () {
this.sendEmailConfirmed = true;
yield this.save.perform();
this.set('showEmailConfirmationModal', false);
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;
}),
openEmailConfirmationModal: action(function (dropdown) {
@ -214,6 +240,8 @@ export default Component.extend({
return;
}
this.sendEmailConfirmed = false;
// runningText needs to be declared before the other states change during the
// save action.
this.set('runningText', this._runningText);

View File

@ -2,10 +2,29 @@ import ModalComponent from 'ghost-admin/components/modal-base';
import {task} from 'ember-concurrency';
export default ModalComponent.extend({
errorMessage: null,
// Allowed actions
confirm: () => {},
confirmTask: task(function* () {
yield this.confirm();
confirmAndCheckError: 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;
}
}
})
});

View File

@ -0,0 +1,6 @@
export default class EmailFailedError extends Error {
constructor(message) {
super(message);
this.name = 'EmailFailedError';
}
}

View File

@ -2,7 +2,9 @@
Saving...
{{else if (or this.post.isPublished this.post.pastScheduledTime)}}
Published
{{#if this.post.email}}
{{#if (or (eq this.post.email.status "submitting") (eq this.post.email.status "submitting"))}}
and sending to {{pluralize this.post.email.emailCount "member"}}
{{else if (eq this.post.email.status "submitted")}}
and sent to {{pluralize this.post.email.emailCount "member"}}
{{/if}}
{{else if this.post.isScheduled}}

View File

@ -1,32 +1,32 @@
{{#basic-dropdown verticalPosition="below" onOpen=(action "open") onClose=(action "close") as |dd|}}
{{#dd.trigger class="gh-btn gh-btn-outline gh-publishmenu-trigger"}}
<span data-test-publishmenu-trigger>{{triggerText}} {{svg-jar "arrow-down"}}</span>
<span data-test-publishmenu-trigger>{{this.triggerText}} {{svg-jar "arrow-down"}}</span>
{{/dd.trigger}}
{{#dd.content class="gh-publishmenu-dropdown"}}
{{#if (eq displayState "published")}}
{{gh-publishmenu-published
post=post
saveType=saveType
setSaveType=(action "setSaveType")
backgroundTask=this.backgroundTask}}
{{gh-publishmenu-published
post=this.post
saveType=this.saveType
setSaveType=(action "setSaveType")
backgroundTask=this.backgroundTask}}
{{else if (eq displayState "scheduled")}}
{{gh-publishmenu-scheduled
post=post
saveType=saveType
isClosing=isClosing
memberCount=this.memberCount
setSaveType=(action "setSaveType")}}
{{gh-publishmenu-scheduled
post=this.post
saveType=this.saveType
isClosing=this.isClosing
memberCount=this.memberCount
setSaveType=(action "setSaveType")}}
{{else}}
{{gh-publishmenu-draft
post=post
saveType=saveType
setSaveType=(action "setSaveType")
backgroundTask=this.backgroundTask
memberCount=this.memberCount
sendEmailWhenPublished=this.sendEmailWhenPublished}}
{{gh-publishmenu-draft
post=this.post
saveType=this.saveType
setSaveType=(action "setSaveType")
backgroundTask=this.backgroundTask
memberCount=this.memberCount
sendEmailWhenPublished=this.sendEmailWhenPublished}}
{{/if}}
{{!--
@ -34,21 +34,23 @@
or cancel the task when the post status updates and switches components
--}}
<footer class="gh-publishmenu-footer">
<button class="gh-btn gh-btn-outline gh-btn-link" {{action dd.actions.close}} data-test-publishmenu-cancel>
<button class="gh-btn gh-btn-outline gh-btn-link" {{on "click" (action dd.actions.close)}} data-test-publishmenu-cancel>
<span>Cancel</span>
</button>
{{gh-task-button buttonText
task=save
taskArgs=(hash dropdown=dd)
successText=successText
runningText=runningText
class="gh-btn gh-btn-blue gh-publishmenu-button gh-btn-icon"
data-test-publishmenu-save=true}}
<GhTaskButton
@buttonText={{this.buttonText}}
@task={{this.save}}
@taskArgs={{hash dropdown=dd}}
@successText={{this.successText}}
@runningText={{this.runningText}}
@class="gh-btn gh-btn-blue gh-publishmenu-button gh-btn-icon"
data-test-publishmenu-save=true
/>
</footer>
{{/dd.content}}
{{/basic-dropdown}}
{{#if showEmailConfirmationModal}}
{{#if this.showEmailConfirmationModal}}
<GhFullscreenModal
@modal="confirm-email-send"
@model={{hash

View File

@ -1,23 +1,45 @@
<header class="modal-header" data-test-modal="delete-user">
<h1>Ready to go? Heres what happens next</h1>
</header>
<a class="close" href="" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a>
{{#unless errorMessage}}
<header class="modal-header" data-test-modal="delete-user">
<h1>Ready to go? Heres 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">
<p>
Your post will be delivered to <strong>{{if this.model.paidOnly "all paid members" (pluralize this.model.memberCount "member")}}</strong> and will be published on your site{{#if this.model.isScheduled}} at the scheduled time{{/if}}. Sounds good?
</p>
</div>
<div class="modal-body">
<p>
Your post will be delivered to
<strong>{{if this.model.paidOnly "all paid members" (pluralize this.model.memberCount "member")}}</strong>
and will be published on your site{{#if this.model.isScheduled}} at the scheduled time{{/if}}. Sounds good?
</p>
</div>
<div class="modal-footer">
<button {{action "closeModal"}} class="gh-btn" data-test-button="cancel-publish-and-email">
<span>Cancel</span>
</button>
<GhTaskButton
@buttonText={{if this.model.isScheduled "Schedule" "Publish and send"}}
@runningText={{if this.model.isScheduled "Scheduling..." "Publishing..."}}
@task={{this.confirmTask}}
@class="gh-btn gh-btn-green gh-btn-icon"
data-test-button="confirm-publish-and-email"
/>
</div>
<div class="modal-footer">
<button {{on "click" this.closeModal}} class="gh-btn" data-test-button="cancel-publish-and-email">
<span>Cancel</span>
</button>
<GhTaskButton
@buttonText={{if this.model.isScheduled "Schedule" "Publish and send"}}
@runningText={{if this.model.isScheduled "Scheduling..." "Publishing..."}}
@task={{this.confirmAndCheckError}}
@class="gh-btn gh-btn-green gh-btn-icon"
data-test-button="confirm-publish-and-email"
/>
</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.</p>
<p class="error">Error: {{this.errorMessage}}</p>
<p>Check your <LinkTo @route="settings.labs">mailgun settings</LinkTo> if it persists.</p>
</div>
<div class="modal-footer">
<button {{on "click" this.closeModal}} class="gh-btn" data-test-button="cancel-publish-and-email">
<span>Close</span>
</button>
</div>
{{/unless}}