Skip to content
This repository has been archived by the owner on Nov 28, 2022. It is now read-only.

Commit

Permalink
Added polling when confirming email to show immediate error
Browse files Browse the repository at this point in the history
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
  • Loading branch information
kevinansfield committed Nov 20, 2019
1 parent dbe40ab commit 76c357a
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 55 deletions.
34 changes: 31 additions & 3 deletions app/components/gh-publishmenu.js
@@ -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(),
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
23 changes: 21 additions & 2 deletions app/components/modal-confirm-email-send.js
Expand Up @@ -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;
}
}
})
});
6 changes: 6 additions & 0 deletions app/errors/email-failed-error.js
@@ -0,0 +1,6 @@
export default class EmailFailedError extends Error {
constructor(message) {
super(message);
this.name = 'EmailFailedError';
}
}
4 changes: 3 additions & 1 deletion app/templates/components/gh-editor-post-status.hbs
Expand Up @@ -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}}
Expand Down
58 changes: 30 additions & 28 deletions app/templates/components/gh-publishmenu.hbs
@@ -1,54 +1,56 @@
{{#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}}

{{!--
save button needs to be outside of menu components so it doesn't lose state
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
Expand Down
64 changes: 43 additions & 21 deletions app/templates/components/modal-confirm-email-send.hbs
@@ -1,23 +1,45 @@
<header class="modal-header" data-test-modal="delete-user">
<h1>Ready to go? Here’s 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? 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">
<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}}

0 comments on commit 76c357a

Please sign in to comment.