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 Component from '@ember/component';
import EmailFailedError from 'ghost-admin/errors/email-failed-error';
import {action} from '@ember/object'; import {action} from '@ember/object';
import {computed} from '@ember/object'; import {computed} from '@ember/object';
import {reads} from '@ember/object/computed'; import {reads} from '@ember/object/computed';
import {inject as service} from '@ember/service'; 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({ export default Component.extend({
clock: service(), clock: service(),
@ -184,8 +188,30 @@ export default Component.extend({
_confirmEmailSend: task(function* () { _confirmEmailSend: task(function* () {
this.sendEmailConfirmed = true; this.sendEmailConfirmed = true;
yield this.save.perform(); let post = yield this.save.perform();
this.set('showEmailConfirmationModal', false);
// 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) { openEmailConfirmationModal: action(function (dropdown) {
@ -214,6 +240,8 @@ export default Component.extend({
return; return;
} }
this.sendEmailConfirmed = false;
// 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.
this.set('runningText', this._runningText); this.set('runningText', this._runningText);

View File

@ -2,10 +2,29 @@ import ModalComponent from 'ghost-admin/components/modal-base';
import {task} from 'ember-concurrency'; import {task} from 'ember-concurrency';
export default ModalComponent.extend({ export default ModalComponent.extend({
errorMessage: null,
// Allowed actions // Allowed actions
confirm: () => {}, confirm: () => {},
confirmTask: task(function* () { confirmAndCheckError: task(function* () {
try {
yield this.confirm(); 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... Saving...
{{else if (or this.post.isPublished this.post.pastScheduledTime)}} {{else if (or this.post.isPublished this.post.pastScheduledTime)}}
Published 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"}} and sent to {{pluralize this.post.email.emailCount "member"}}
{{/if}} {{/if}}
{{else if this.post.isScheduled}} {{else if this.post.isScheduled}}

View File

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

View File

@ -1,23 +1,45 @@
{{#unless errorMessage}}
<header class="modal-header" data-test-modal="delete-user"> <header class="modal-header" data-test-modal="delete-user">
<h1>Ready to go? Heres what happens next</h1> <h1>Ready to go? Heres what happens next</h1>
</header> </header>
<a class="close" href="" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a> <button class="close" title="Close" {{on "click" this.closeModal}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
<div class="modal-body"> <div class="modal-body">
<p> <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? 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> </p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button {{action "closeModal"}} class="gh-btn" data-test-button="cancel-publish-and-email"> <button {{on "click" this.closeModal}} class="gh-btn" data-test-button="cancel-publish-and-email">
<span>Cancel</span> <span>Cancel</span>
</button> </button>
<GhTaskButton <GhTaskButton
@buttonText={{if this.model.isScheduled "Schedule" "Publish and send"}} @buttonText={{if this.model.isScheduled "Schedule" "Publish and send"}}
@runningText={{if this.model.isScheduled "Scheduling..." "Publishing..."}} @runningText={{if this.model.isScheduled "Scheduling..." "Publishing..."}}
@task={{this.confirmTask}} @task={{this.confirmAndCheckError}}
@class="gh-btn gh-btn-green gh-btn-icon" @class="gh-btn gh-btn-green gh-btn-icon"
data-test-button="confirm-publish-and-email" data-test-button="confirm-publish-and-email"
/> />
</div> </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}}