From 0c0da3813e7a45b6434a9762306a15b5ec1648a2 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Thu, 7 Nov 2019 15:37:26 +0700 Subject: [PATCH] Added confirmation modal and use email model in place of action --- .../app/components/gh-editor-post-status.js | 10 --- .../app/components/gh-post-settings-menu.js | 27 +++---- .../app/components/gh-publishmenu-draft.js | 34 +++++---- ghost/admin/app/components/gh-publishmenu.js | 75 ++++++++++++++----- ghost/admin/app/components/gh-task-button.js | 3 +- .../components/modal-confirm-email-send.js | 11 +++ ghost/admin/app/controllers/editor.js | 14 +--- ghost/admin/app/models/email.js | 22 ++++++ ghost/admin/app/models/post.js | 11 +-- ghost/admin/app/serializers/page.js | 2 + ghost/admin/app/serializers/post.js | 6 +- .../components/gh-editor-post-status.hbs | 11 ++- .../components/gh-publishmenu-draft.hbs | 3 +- .../templates/components/gh-publishmenu.hbs | 11 +++ .../components/modal-confirm-email-send.hbs | 29 +++++++ 15 files changed, 188 insertions(+), 81 deletions(-) create mode 100644 ghost/admin/app/components/modal-confirm-email-send.js create mode 100644 ghost/admin/app/models/email.js create mode 100644 ghost/admin/app/templates/components/modal-confirm-email-send.hbs diff --git a/ghost/admin/app/components/gh-editor-post-status.js b/ghost/admin/app/components/gh-editor-post-status.js index db68a3d2e7..835435fedb 100644 --- a/ghost/admin/app/components/gh-editor-post-status.js +++ b/ghost/admin/app/components/gh-editor-post-status.js @@ -12,16 +12,6 @@ export default Component.extend({ _isSaving: false, - isNew: reads('post.isNew'), - isScheduled: reads('post.isScheduled'), - - isPublished: computed('post.{isPublished,pastScheduledTime}', function () { - let isPublished = this.get('post.isPublished'); - let pastScheduledTime = this.get('post.pastScheduledTime'); - - return isPublished || pastScheduledTime; - }), - // isSaving will only be true briefly whilst the post is saving, // we want to ensure that the "Saving..." message is shown for at least // a few seconds so that it's noticeable diff --git a/ghost/admin/app/components/gh-post-settings-menu.js b/ghost/admin/app/components/gh-post-settings-menu.js index 283ae251f7..97298d0087 100644 --- a/ghost/admin/app/components/gh-post-settings-menu.js +++ b/ghost/admin/app/components/gh-post-settings-menu.js @@ -103,6 +103,10 @@ export default Component.extend(SettingsMenuMixin, { } }), + mailgunError: computed('settings.memberSubscriptionSettings', function () { + return !this._isMailgunConfigured(); + }), + didReceiveAttrs() { this._super(...arguments); @@ -550,19 +554,6 @@ export default Component.extend(SettingsMenuMixin, { this.set('_showThrobbers', true); }).restartable(), - isMailgunConfigured: function () { - let subSettingsValue = this.get('settings.membersSubscriptionSettings'); - let subscriptionSettings = subSettingsValue ? JSON.parse(subSettingsValue) : {}; - if (Object.keys(subscriptionSettings).includes('mailgunApiKey')) { - return (subscriptionSettings.mailgunApiKey && subscriptionSettings.mailgunDomain); - } - return true; - }, - - mailgunError: computed('settings.memberSubscriptionSettings', function () { - return !this.isMailgunConfigured(); - }), - sendTestEmail: task(function* () { try { const resourceId = this.post.id; @@ -595,5 +586,15 @@ export default Component.extend(SettingsMenuMixin, { if (error) { this.notifications.showAPIError(error); } + }, + + // TODO: put this on settings model + _isMailgunConfigured: function () { + let subSettingsValue = this.get('settings.membersSubscriptionSettings'); + let subscriptionSettings = subSettingsValue ? JSON.parse(subSettingsValue) : {}; + if (Object.keys(subscriptionSettings).includes('mailgunApiKey')) { + return (subscriptionSettings.mailgunApiKey && subscriptionSettings.mailgunDomain); + } + return true; } }); diff --git a/ghost/admin/app/components/gh-publishmenu-draft.js b/ghost/admin/app/components/gh-publishmenu-draft.js index 6f85215b21..7da63f487d 100644 --- a/ghost/admin/app/components/gh-publishmenu-draft.js +++ b/ghost/admin/app/components/gh-publishmenu-draft.js @@ -1,6 +1,7 @@ import Component from '@ember/component'; import moment from 'moment'; import {computed} from '@ember/object'; +import {equal} from '@ember/object/computed'; import {isEmpty} from '@ember/utils'; import {inject as service} from '@ember/service'; @@ -16,24 +17,15 @@ export default Component.extend({ 'data-test-publishmenu-draft': true, - mailgunError: computed('settings.memberSubscriptionSettings', function() { - return !this.isMailgunConfigured(); - }), + disableEmailOption: equal('memberCount', 0), - isMailgunConfigured: function() { - let subSettingsValue = this.get('settings.membersSubscriptionSettings'); - let subscriptionSettings = subSettingsValue ? JSON.parse(subSettingsValue) : {}; - if (Object.keys(subscriptionSettings).includes('mailgunApiKey')) { - return subscriptionSettings.mailgunApiKey && subscriptionSettings.mailgunDomain; - } - return false; - }, + canSendEmail: computed('feature.labs.members', 'post.{displayName,email}', function () { + let membersEnabled = this.feature.get('labs.members'); + let mailgunIsConfigured = this._isMailgunConfigured(); + let isPost = this.post.displayName === 'post'; + let hasSentEmail = !!this.post.email; - disableEmailOption: computed('memberCount', 'settings.membersSubscriptionSettings', function () { - if (!this.feature.members) { - return true; - } - return !this.isMailgunConfigured() || this.membersCount === 0; + return membersEnabled && mailgunIsConfigured && isPost && !hasSentEmail; }), didInsertElement() { @@ -89,6 +81,16 @@ export default Component.extend({ } }, + // TODO: put this on settings model + _isMailgunConfigured: function () { + let subSettingsValue = this.get('settings.membersSubscriptionSettings'); + let subscriptionSettings = subSettingsValue ? JSON.parse(subSettingsValue) : {}; + if (Object.keys(subscriptionSettings).includes('mailgunApiKey')) { + return subscriptionSettings.mailgunApiKey && subscriptionSettings.mailgunDomain; + } + return false; + }, + // API only accepts dates at least 2 mins in the future, default the // scheduled date 5 mins in the future to avoid immediate validation errors _getMinDate() { diff --git a/ghost/admin/app/components/gh-publishmenu.js b/ghost/admin/app/components/gh-publishmenu.js index 7ab06b8388..72f29f75fb 100644 --- a/ghost/admin/app/components/gh-publishmenu.js +++ b/ghost/admin/app/components/gh-publishmenu.js @@ -1,6 +1,6 @@ -import $ from 'jquery'; import Component from '@ember/component'; import boundOneWay from 'ghost-admin/utils/bound-one-way'; +import {action} from '@ember/object'; import {computed} from '@ember/object'; import {reads} from '@ember/object/computed'; import {inject as service} from '@ember/service'; @@ -23,6 +23,8 @@ export default Component.extend({ isClosing: null, + onClose() {}, + forcePublishedMenu: reads('post.pastScheduledTime'), sendEmailWhenPublishedScratch: boundOneWay('post.sendEmailWhenPublished'), @@ -155,30 +157,63 @@ export default Component.extend({ }, close(dropdown, e) { - let post = this.post; + // 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'); - // don't close the menu if the datepicker popup is clicked - if (e && $(e.target).closest('.ember-power-datepicker-content').length) { - return false; + if (onDatepicker || onModal) { + return false; + } } - // cleanup - this.set('sendEmailWhenPublishedScratch', this.post.sendEmailWhenPublishedScratch); - this._resetPublishedAtBlogTZ(); - post.set('statusScratch', null); - post.validate(); - - if (this.onClose) { - this.onClose(); + if (!this._skipDropdownCloseCleanup) { + this._cleanup(); } + this._skipDropdownCloseCleanup = false; + this.onClose(); this.set('isClosing', true); return true; } }, - save: task(function* () { + // action is required because only uses actions + confirmEmailSend: action(function () { + return this._confirmEmailSend.perform(); + }), + + _confirmEmailSend: task(function* () { + this.sendEmailConfirmed = true; + yield this.save.perform(); + this.set('showEmailConfirmationModal', false); + }), + + 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} = {}) { + if ( + this.post.status === 'draft' && + !this.post.email && // email sent previously + this.sendEmailWhenPublishedScratch && + !this.sendEmailConfirmed // set once confirmed so normal save happens + ) { + this.openEmailConfirmationModal(dropdown); + return; + } + // runningText needs to be declared before the other states change during the // save action. this.set('runningText', this._runningText); @@ -208,9 +243,15 @@ export default Component.extend({ this._publishedAtBlogTZ = this.get('post.publishedAtBlogTZ'); }, - // 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 - _resetPublishedAtBlogTZ() { + _cleanup() { + this.set('showConfirmEmailModal', false); + this.set('sendEmailWhenPublishedScratch', this.post.sendEmailWhenPublishedScratch); + + // 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(); } }); diff --git a/ghost/admin/app/components/gh-task-button.js b/ghost/admin/app/components/gh-task-button.js index 3ffee489e1..b9f609706a 100644 --- a/ghost/admin/app/components/gh-task-button.js +++ b/ghost/admin/app/components/gh-task-button.js @@ -27,6 +27,7 @@ const GhTaskButton = Component.extend({ attributeBindings: ['disabled', 'form', 'type', 'tabindex'], task: null, + taskParams: null, disabled: false, defaultClick: false, buttonText: 'Save', @@ -129,7 +130,7 @@ const GhTaskButton = Component.extend({ } this.action(); - task.perform(); + task.perform(this.taskArgs); this._restartAnimation.perform(); diff --git a/ghost/admin/app/components/modal-confirm-email-send.js b/ghost/admin/app/components/modal-confirm-email-send.js new file mode 100644 index 0000000000..928bc35cb2 --- /dev/null +++ b/ghost/admin/app/components/modal-confirm-email-send.js @@ -0,0 +1,11 @@ +import ModalComponent from 'ghost-admin/components/modal-base'; +import {task} from 'ember-concurrency'; + +export default ModalComponent.extend({ + // Allowed actions + confirm: () => {}, + + confirmTask: task(function* () { + yield this.confirm(); + }) +}); diff --git a/ghost/admin/app/controllers/editor.js b/ghost/admin/app/controllers/editor.js index cc37224dc7..33e0c6d350 100644 --- a/ghost/admin/app/controllers/editor.js +++ b/ghost/admin/app/controllers/editor.js @@ -134,10 +134,6 @@ export default Controller.extend({ } }), - deliveredAction: computed('actionsList', function () { - return this.actionsList && this.actionsList.findBy('event', 'delivered'); - }), - _autosaveRunning: computed('_autosave.isRunning', '_timedSave.isRunning', function () { let autosave = this.get('_autosave.isRunning'); let timedsave = this.get('_timedSave.isRunning'); @@ -544,12 +540,6 @@ export default Controller.extend({ // load supplementel data such as the actions list in the background backgroundLoader: task(function* () { if (this.feature.members) { - let actions = yield this.store.query('action', { - filter: `resource_type:post+resource_id:${this.post.id}+event:delivered`, - limit: 'all' - }); - this.set('actionsList', actions); - let membersResponse = yield this.store.query('member', {limit: 1}); this.set('memberCount', get(membersResponse, 'meta.pagination.total')); } @@ -769,6 +759,10 @@ export default Controller.extend({ if (status === 'published') { type = this.get('post.page') ? 'Page' : 'Post'; path = this.get('post.url'); + + if (prevStatus === 'draft' && this.post.email) { + message = `Published and sent to ${this.post.email.emailCount} members!`; + } } else { type = 'Preview'; path = this.get('post.previewUrl'); diff --git a/ghost/admin/app/models/email.js b/ghost/admin/app/models/email.js new file mode 100644 index 0000000000..1bce380c71 --- /dev/null +++ b/ghost/admin/app/models/email.js @@ -0,0 +1,22 @@ +import Model from 'ember-data/model'; +import attr from 'ember-data/attr'; +import {belongsTo} from 'ember-data/relationships'; + +export default Model.extend({ + emailCount: attr('number'), + error: attr('string'), + html: attr('string'), + plaintext: attr('string'), + stats: attr('json-string'), + status: attr('string'), + subject: attr('string'), + submittedAtUTC: attr('moment-utc'), + uuid: attr('string'), + + createdAtUTC: attr('string'), + createdBy: attr('string'), + updatedAtUTC: attr('string'), + updatedBy: attr('string'), + + post: belongsTo('post') +}); diff --git a/ghost/admin/app/models/post.js b/ghost/admin/app/models/post.js index 37aae6e413..24e288a2e5 100644 --- a/ghost/admin/app/models/post.js +++ b/ghost/admin/app/models/post.js @@ -109,16 +109,11 @@ export default Model.extend(Comparable, ValidationEngine, { uuid: attr('string'), sendEmailWhenPublished: attr('boolean', {defaultValue: false}), - authors: hasMany('user', { - embedded: 'always', - async: false - }), + authors: hasMany('user', {embedded: 'always', async: false}), createdBy: belongsTo('user', {async: true}), + email: belongsTo('email', {async: false}), publishedBy: belongsTo('user', {async: true}), - tags: hasMany('tag', { - embedded: 'always', - async: false - }), + tags: hasMany('tag', {embedded: 'always', async: false}), primaryAuthor: computed('authors.[]', function () { return this.get('authors.firstObject'); diff --git a/ghost/admin/app/serializers/page.js b/ghost/admin/app/serializers/page.js index 0afcb469c0..10e48681de 100644 --- a/ghost/admin/app/serializers/page.js +++ b/ghost/admin/app/serializers/page.js @@ -7,6 +7,8 @@ export default PostSerializer.extend({ // Properties that exist on the model but we don't want sent in the payload delete json.email_subject; delete json.send_email_when_published; + delete json.email_id; + delete json.email; return json; } diff --git a/ghost/admin/app/serializers/post.js b/ghost/admin/app/serializers/post.js index 4b06dee186..876cb98340 100644 --- a/ghost/admin/app/serializers/post.js +++ b/ghost/admin/app/serializers/post.js @@ -10,7 +10,8 @@ export default ApplicationSerializer.extend(EmbeddedRecordsMixin, { tags: {embedded: 'always'}, publishedAtUTC: {key: 'published_at'}, createdAtUTC: {key: 'created_at'}, - updatedAtUTC: {key: 'updated_at'} + updatedAtUTC: {key: 'updated_at'}, + email: {embedded: 'always'} }, normalizeSingleResponse(store, primaryModelClass, payload) { @@ -45,6 +46,9 @@ export default ApplicationSerializer.extend(EmbeddedRecordsMixin, { delete json.visibility; } + delete json.email_id; + delete json.email; + return json; } }); diff --git a/ghost/admin/app/templates/components/gh-editor-post-status.hbs b/ghost/admin/app/templates/components/gh-editor-post-status.hbs index 004a0aa42b..71e85f2021 100644 --- a/ghost/admin/app/templates/components/gh-editor-post-status.hbs +++ b/ghost/admin/app/templates/components/gh-editor-post-status.hbs @@ -1,10 +1,13 @@ {{#if _isSaving}} Saving... -{{else if isPublished}} - Published -{{else if isScheduled}} +{{else if (or this.post.isPublished this.post.pastScheduledTime)}} + Published on {{gh-format-post-time post.publishedAtUTC draft=false}} + {{#if this.post.email}} + and sent to {{this.post.email.emailCount}} members + {{/if}} +{{else if this.post.isScheduled}} Scheduled -{{else if isNew}} +{{else if this.post.isNew}} New {{else}} Draft diff --git a/ghost/admin/app/templates/components/gh-publishmenu-draft.hbs b/ghost/admin/app/templates/components/gh-publishmenu-draft.hbs index 49f7d2b6ca..85c1bf513d 100644 --- a/ghost/admin/app/templates/components/gh-publishmenu-draft.hbs +++ b/ghost/admin/app/templates/components/gh-publishmenu-draft.hbs @@ -24,7 +24,8 @@
Set automatic future publish date
- {{#if (and this.feature.labs.members (eq this.post.displayName "post") (not mailgunError) (not this.deliveredAction))}} + + {{#if this.canSendEmail}}
{{#if this.backgroundLoader.isRunning}}
diff --git a/ghost/admin/app/templates/components/gh-publishmenu.hbs b/ghost/admin/app/templates/components/gh-publishmenu.hbs index c742333078..37a15506a6 100644 --- a/ghost/admin/app/templates/components/gh-publishmenu.hbs +++ b/ghost/admin/app/templates/components/gh-publishmenu.hbs @@ -40,6 +40,7 @@ {{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" @@ -47,3 +48,13 @@ {{/dd.content}} {{/basic-dropdown}} + +{{#if showEmailConfirmationModal}} + +{{/if}} \ No newline at end of file diff --git a/ghost/admin/app/templates/components/modal-confirm-email-send.hbs b/ghost/admin/app/templates/components/modal-confirm-email-send.hbs new file mode 100644 index 0000000000..00b860754f --- /dev/null +++ b/ghost/admin/app/templates/components/modal-confirm-email-send.hbs @@ -0,0 +1,29 @@ + +{{svg-jar "close"}} + + + +