mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-24 23:48:13 -05:00
Added confirmation modal and use email model in place of action
This commit is contained in:
parent
71ded80e8c
commit
0c0da3813e
15 changed files with 188 additions and 81 deletions
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 <GhFullscreenModal> 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();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
11
ghost/admin/app/components/modal-confirm-email-send.js
Normal file
11
ghost/admin/app/components/modal-confirm-email-send.js
Normal file
|
@ -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();
|
||||
})
|
||||
});
|
|
@ -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');
|
||||
|
|
22
ghost/admin/app/models/email.js
Normal file
22
ghost/admin/app/models/email.js
Normal file
|
@ -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')
|
||||
});
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -24,7 +24,8 @@
|
|||
<div class="gh-publishmenu-radio-desc">Set automatic future publish date</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#if (and this.feature.labs.members (eq this.post.displayName "post") (not mailgunError) (not this.deliveredAction))}}
|
||||
|
||||
{{#if this.canSendEmail}}
|
||||
<div class="gh-publishmenu-radio">
|
||||
{{#if this.backgroundLoader.isRunning}}
|
||||
<div class="gh-loading-spinner" style="zoom: 50%"></div>
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
</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"
|
||||
|
@ -47,3 +48,13 @@
|
|||
</footer>
|
||||
{{/dd.content}}
|
||||
{{/basic-dropdown}}
|
||||
|
||||
{{#if showEmailConfirmationModal}}
|
||||
<GhFullscreenModal
|
||||
@modal="confirm-email-send"
|
||||
@model={{hash memberCount=this.memberCount isScheduled=(eq this.saveType "schedule")}}
|
||||
@confirm={{this.confirmEmailSend}}
|
||||
@close={{this.closeEmailConfirmationModal}}
|
||||
@modifier="action wide"
|
||||
/>
|
||||
{{/if}}
|
|
@ -0,0 +1,29 @@
|
|||
<header class="modal-header" data-test-modal="delete-user">
|
||||
<h1>Are you sure you want to send email?</h1>
|
||||
</header>
|
||||
<a class="close" href="" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body">
|
||||
<p><strong>PLEASE NOTE:</strong> You are about to email this post to <strong>{{pluralize this.model.memberCount "member"}}</strong>.</p>
|
||||
<ul>
|
||||
{{#if this.model.isScheduled}}
|
||||
<li>Email will be sent when the post is published at the scheduled time</li>
|
||||
{{else}}
|
||||
<li>Email will be sent immediately</li>
|
||||
{{/if}}
|
||||
<li>It will <em>not</em> be possible to email this post again in the future</li>
|
||||
</ul>
|
||||
</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>
|
Loading…
Add table
Reference in a new issue