Added confirmation modal and use email model in place of action

This commit is contained in:
Kevin Ansfield 2019-11-07 15:37:26 +07:00
parent 6450edb1f6
commit 56204d0129
15 changed files with 188 additions and 81 deletions

View File

@ -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

View File

@ -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;
}
});

View File

@ -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() {

View File

@ -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();
}
});

View File

@ -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();

View 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();
})
});

View File

@ -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
app/models/email.js Normal file
View 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')
});

View File

@ -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');

View File

@ -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;
}

View File

@ -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;
}
});

View File

@ -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

View File

@ -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>

View File

@ -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}}

View File

@ -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>