diff --git a/core/client/app/components/gh-ed-editor.js b/core/client/app/components/gh-ed-editor.js index e0385f9105..2db284284e 100644 --- a/core/client/app/components/gh-ed-editor.js +++ b/core/client/app/components/gh-ed-editor.js @@ -57,5 +57,11 @@ export default TextArea.extend(EditorAPI, EditorShortcuts, EditorScroll, { enable() { let textarea = this.get('element'); textarea.removeAttribute('readonly'); + }, + + actions: { + toggleCopyHTMLModal(generatedHTML) { + this.attrs.toggleCopyHTMLModal(generatedHTML); + } } }); diff --git a/core/client/app/components/gh-editor.js b/core/client/app/components/gh-editor.js index d3824ac6d7..e56075a49c 100644 --- a/core/client/app/components/gh-editor.js +++ b/core/client/app/components/gh-editor.js @@ -7,6 +7,9 @@ export default Component.extend({ tagName: 'section', classNames: ['gh-view'], + showCopyHTMLModal: false, + copyHTMLModalContent: null, + // updated when gh-ed-editor component scrolls editorScrollInfo: null, // updated when markdown is rendered @@ -58,6 +61,11 @@ export default Component.extend({ actions: { selectTab(tab) { this.set('activeTab', tab); + }, + + toggleCopyHTMLModal(generatedHTML) { + this.set('copyHTMLModalContent', generatedHTML); + this.toggleProperty('showCopyHTMLModal'); } } }); diff --git a/core/client/app/components/gh-fullscreen-modal.js b/core/client/app/components/gh-fullscreen-modal.js new file mode 100644 index 0000000000..16ee24e8af --- /dev/null +++ b/core/client/app/components/gh-fullscreen-modal.js @@ -0,0 +1,76 @@ +import Ember from 'ember'; +import LiquidTether from 'liquid-tether/components/liquid-tether'; + +const {RSVP, isBlank, on, run} = Ember; +const emberA = Ember.A; + +const FullScreenModalComponent = LiquidTether.extend({ + to: 'fullscreen-modal', + target: 'document.body', + targetModifier: 'visible', + targetAttachment: 'top center', + attachment: 'top center', + tetherClass: 'fullscreen-modal', + overlayClass: 'fullscreen-modal-background', + modalPath: 'unknown', + + dropdown: Ember.inject.service(), + + init() { + this._super(...arguments); + this.modalPath = `modals/${this.get('modal')}`; + }, + + setTetherClass: on('init', function () { + let tetherClass = this.get('tetherClass'); + let modifiers = (this.get('modifier') || '').split(' '); + let tetherClasses = emberA([tetherClass]); + + modifiers.forEach((modifier) => { + if (!isBlank(modifier)) { + let className = `${tetherClass}-${modifier}`; + tetherClasses.push(className); + } + }); + + this.set('tetherClass', tetherClasses.join(' ')); + }), + + closeDropdowns: on('didInsertElement', function () { + run.schedule('afterRender', this, function () { + this.get('dropdown').closeDropdowns(); + }); + }), + + actions: { + close() { + if (this.attrs.close) { + return this.attrs.close(); + } + + return new RSVP.Promise((resolve) => { + resolve(); + }); + }, + + confirm() { + if (this.attrs.confirm) { + return this.attrs.confirm(); + } + + return new RSVP.Promise((resolve) => { + resolve(); + }); + }, + + clickOverlay() { + this.send('close'); + } + } +}); + +FullScreenModalComponent.reopenClass({ + positionalParams: ['modal'] +}); + +export default FullScreenModalComponent; diff --git a/core/client/app/components/gh-modal-dialog.js b/core/client/app/components/gh-modal-dialog.js deleted file mode 100644 index 14f4235385..0000000000 --- a/core/client/app/components/gh-modal-dialog.js +++ /dev/null @@ -1,67 +0,0 @@ -import Ember from 'ember'; - -const {Component, computed} = Ember; - -function K() { - return this; -} - -export default Component.extend({ - - confirmaccept: 'confirmAccept', - confirmreject: 'confirmReject', - - klass: computed('type', 'style', function () { - let classNames = []; - - classNames.push(this.get('type') ? `modal-${this.get('type')}` : 'modal'); - - if (this.get('style')) { - this.get('style').split(',').forEach((style) => { - classNames.push(`modal-style-${style}`); - }); - } - - return classNames.join(' '); - }), - - acceptButtonClass: computed('confirm.accept.buttonClass', function () { - return this.get('confirm.accept.buttonClass') ? this.get('confirm.accept.buttonClass') : 'btn btn-green'; - }), - - rejectButtonClass: computed('confirm.reject.buttonClass', function () { - return this.get('confirm.reject.buttonClass') ? this.get('confirm.reject.buttonClass') : 'btn btn-red'; - }), - - didInsertElement() { - this._super(...arguments); - this.$('.js-modal-container, .js-modal-background').addClass('fade-in open'); - this.$('.js-modal').addClass('open'); - }, - - close() { - this.$('.js-modal, .js-modal-background').removeClass('fade-in').addClass('fade-out'); - - // The background should always be the last thing to fade out, so check on that instead of the content - this.$('.js-modal-background').on('animationend webkitAnimationEnd oanimationend MSAnimationEnd', (event) => { - if (event.originalEvent.animationName === 'fade-out') { - this.$('.js-modal, .js-modal-background').removeClass('open'); - } - }); - - this.sendAction(); - }, - - actions: { - closeModal() { - this.close(); - }, - - confirm(type) { - this.sendAction(`confirm${type}`); - this.close(); - }, - - noBubble: K - } -}); diff --git a/core/client/app/components/gh-nav-menu.js b/core/client/app/components/gh-nav-menu.js index def7a79599..e5ce0eac32 100644 --- a/core/client/app/components/gh-nav-menu.js +++ b/core/client/app/components/gh-nav-menu.js @@ -21,8 +21,8 @@ export default Component.extend({ this.sendAction('toggleMaximise'); }, - openModal(modal) { - this.sendAction('openModal', modal); + showMarkdownHelp() { + this.sendAction('showMarkdownHelp'); }, closeMobileMenu() { diff --git a/core/client/app/components/gh-tag-settings-form.js b/core/client/app/components/gh-tag-settings-form.js index 0e16aae2e4..7e9aaef169 100644 --- a/core/client/app/components/gh-tag-settings-form.js +++ b/core/client/app/components/gh-tag-settings-form.js @@ -125,7 +125,7 @@ export default Component.extend({ }, deleteTag() { - this.sendAction('openModal', 'delete-tag', this.get('tag')); + this.attrs.showDeleteTagModal(); } } diff --git a/core/client/app/components/gh-upload-modal.js b/core/client/app/components/gh-upload-modal.js deleted file mode 100644 index 1f39601d8b..0000000000 --- a/core/client/app/components/gh-upload-modal.js +++ /dev/null @@ -1,82 +0,0 @@ -import Ember from 'ember'; -import ModalDialog from 'ghost/components/gh-modal-dialog'; -import upload from 'ghost/assets/lib/uploader'; -import cajaSanitizers from 'ghost/utils/caja-sanitizers'; - -const {inject, isEmpty} = Ember; - -export default ModalDialog.extend({ - layoutName: 'components/gh-modal-dialog', - - config: inject.service(), - - didInsertElement() { - this._super(...arguments); - upload.call(this.$('.js-drop-zone'), {fileStorage: this.get('config.fileStorage')}); - }, - - keyDown() { - this.setErrorState(false); - }, - - setErrorState(state) { - if (state) { - this.$('.js-upload-url').addClass('error'); - } else { - this.$('.js-upload-url').removeClass('error'); - } - }, - - confirm: { - reject: { - buttonClass: 'btn btn-default', - text: 'Cancel', // The reject button text - func() { // The function called on rejection - return true; - } - }, - - accept: { - buttonClass: 'btn btn-blue right', - text: 'Save', // The accept button text: 'Save' - func() { - let imageType = `model.${this.get('imageType')}`; - let value; - - if (this.$('.js-upload-url').val()) { - value = this.$('.js-upload-url').val(); - - if (!isEmpty(value) && !cajaSanitizers.url(value)) { - this.setErrorState(true); - return {message: 'Image URI is not valid'}; - } - } else { - value = this.$('.js-upload-target').attr('src'); - } - - this.set(imageType, value); - return true; - } - } - }, - - actions: { - closeModal() { - this.sendAction(); - }, - - confirm(type) { - let func = this.get(`confirm.${type}.func`); - let result; - - if (typeof func === 'function') { - result = func.apply(this); - } - - if (!result.message) { - this.sendAction(); - this.sendAction(`confirm${type}`); - } - } - } -}); diff --git a/core/client/app/components/gh-user-invited.js b/core/client/app/components/gh-user-invited.js index 192b9344c9..eaca086c53 100644 --- a/core/client/app/components/gh-user-invited.js +++ b/core/client/app/components/gh-user-invited.js @@ -31,8 +31,7 @@ export default Component.extend({ notifications.showAlert('Invitation email was not sent. Please try resending.', {type: 'error', key: 'invite.resend.not-sent'}); } else { user.set('status', result.users[0].status); - notifications.showNotification(notificationText); - notifications.closeAlerts('invite.resend'); + notifications.showNotification(notificationText, {key: 'invite.resend.success'}); } }).catch((error) => { notifications.showAPIError(error, {key: 'invite.resend'}); @@ -51,8 +50,7 @@ export default Component.extend({ if (user.get('invited')) { user.destroyRecord().then(() => { let notificationText = `Invitation revoked. (${email})`; - notifications.showNotification(notificationText); - notifications.closeAlerts('invite.revoke'); + notifications.showNotification(notificationText, {key: 'invite.revoke.success'}); }).catch((error) => { notifications.showAPIError(error, {key: 'invite.revoke'}); }); diff --git a/core/client/app/components/modals/base.js b/core/client/app/components/modals/base.js new file mode 100644 index 0000000000..40951062be --- /dev/null +++ b/core/client/app/components/modals/base.js @@ -0,0 +1,45 @@ +/* global key */ +import Ember from 'ember'; + +const {Component, on, run} = Ember; + +export default Component.extend({ + tagName: 'section', + classNames: 'modal-content', + + _previousKeymasterScope: null, + + setupShortcuts: on('didInsertElement', function () { + run(function () { + document.activeElement.blur(); + }); + this._previousKeymasterScope = key.getScope(); + + key('enter', 'modal', () => { + this.send('confirm'); + }); + + key('escape', 'modal', () => { + this.send('closeModal'); + }); + + key.setScope('modal'); + }), + + removeShortcuts: on('willDestroyElement', function () { + key.unbind('enter', 'modal'); + key.unbind('escape', 'modal'); + + key.setScope(this._previousKeymasterScope); + }), + + actions: { + confirm() { + throw new Error('You must override the "confirm" action in your modal component'); + }, + + closeModal() { + this.attrs.closeModal(); + } + } +}); diff --git a/core/client/app/components/modals/copy-html.js b/core/client/app/components/modals/copy-html.js new file mode 100644 index 0000000000..078b0b12be --- /dev/null +++ b/core/client/app/components/modals/copy-html.js @@ -0,0 +1,9 @@ +import Ember from 'ember'; +import ModalComponent from 'ghost/components/modals/base'; + +const {computed} = Ember; +const {alias} = computed; + +export default ModalComponent.extend({ + generatedHtml: alias('model') +}); diff --git a/core/client/app/components/modals/delete-all.js b/core/client/app/components/modals/delete-all.js new file mode 100644 index 0000000000..51d792ae6c --- /dev/null +++ b/core/client/app/components/modals/delete-all.js @@ -0,0 +1,48 @@ +import Ember from 'ember'; +import ModalComponent from 'ghost/components/modals/base'; +import {request as ajax} from 'ic-ajax'; + +const {inject} = Ember; + +export default ModalComponent.extend({ + + submitting: false, + + ghostPaths: inject.service('ghost-paths'), + notifications: inject.service(), + store: inject.service(), + + _deleteAll() { + return ajax(this.get('ghostPaths.url').api('db'), { + type: 'DELETE' + }); + }, + + _unloadData() { + this.get('store').unloadAll('post'); + this.get('store').unloadAll('tag'); + }, + + _showSuccess() { + this.get('notifications').showAlert('All content deleted from database.', {type: 'success', key: 'all-content.delete.success'}); + }, + + _showFailure(error) { + this.get('notifications').showAPIError(error, {key: 'all-content.delete'}); + }, + + actions: { + confirm() { + this.set('submitting', true); + + this._deleteAll().then(() => { + this._unloadData(); + this._showSuccess(); + }).catch((error) => { + this._showFailure(error); + }).finally(() => { + this.send('closeModal'); + }); + } + } +}); diff --git a/core/client/app/components/modals/delete-post.js b/core/client/app/components/modals/delete-post.js new file mode 100644 index 0000000000..3353b317e2 --- /dev/null +++ b/core/client/app/components/modals/delete-post.js @@ -0,0 +1,51 @@ +import Ember from 'ember'; +import ModalComponent from 'ghost/components/modals/base'; + +const {computed, inject} = Ember; +const {alias} = computed; + +export default ModalComponent.extend({ + + submitting: false, + + post: alias('model'), + + notifications: inject.service(), + routing: inject.service('-routing'), + + _deletePost() { + let post = this.get('post'); + + // definitely want to clear the data store and post of any unsaved, + // client-generated tags + post.updateTags(); + + return post.destroyRecord(); + }, + + _success() { + // clear any previous error messages + this.get('notifications').closeAlerts('post.delete'); + + // redirect to content screen + this.get('routing').transitionTo('posts'); + }, + + _failure() { + this.get('notifications').showAlert('Your post could not be deleted. Please try again.', {type: 'error', key: 'post.delete.failed'}); + }, + + actions: { + confirm() { + this.set('submitting', true); + + this._deletePost().then(() => { + this._success(); + }, () => { + this._failure(); + }).finally(() => { + this.send('closeModal'); + }); + } + } +}); diff --git a/core/client/app/components/modals/delete-tag.js b/core/client/app/components/modals/delete-tag.js new file mode 100644 index 0000000000..9ddd21dbd5 --- /dev/null +++ b/core/client/app/components/modals/delete-tag.js @@ -0,0 +1,26 @@ +import Ember from 'ember'; +import ModalComponent from 'ghost/components/modals/base'; + +const {computed} = Ember; +const {alias} = computed; + +export default ModalComponent.extend({ + + submitting: false, + + tag: alias('model'), + + postInflection: computed('tag.count.posts', function () { + return this.get('tag.count.posts') > 1 ? 'posts' : 'post'; + }), + + actions: { + confirm() { + this.set('submitting', true); + + this.attrs.confirm().finally(() => { + this.send('closeModal'); + }); + } + } +}); diff --git a/core/client/app/components/modals/delete-user.js b/core/client/app/components/modals/delete-user.js new file mode 100644 index 0000000000..7cccf0be55 --- /dev/null +++ b/core/client/app/components/modals/delete-user.js @@ -0,0 +1,18 @@ +import ModalComponent from 'ghost/components/modals/base'; + +export default ModalComponent.extend({ + + submitting: false, + + user: null, + + actions: { + confirm() { + this.set('submitting', true); + + this.attrs.confirm().finally(() => { + this.send('closeModal'); + }); + } + } +}); diff --git a/core/client/app/components/modals/invite-new-user.js b/core/client/app/components/modals/invite-new-user.js new file mode 100644 index 0000000000..f0fc52b93a --- /dev/null +++ b/core/client/app/components/modals/invite-new-user.js @@ -0,0 +1,121 @@ +import Ember from 'ember'; +import ModalComponent from 'ghost/components/modals/base'; +import ValidationEngine from 'ghost/mixins/validation-engine'; + +const {RSVP, inject, run} = Ember; +const emberA = Ember.A; + +export default ModalComponent.extend(ValidationEngine, { + classNames: 'modal-content invite-new-user', + + role: null, + roles: null, + authorRole: null, + submitting: false, + + validationType: 'inviteUser', + + notifications: inject.service(), + store: inject.service(), + + init() { + this._super(...arguments); + + // populate roles and set initial value for the dropdown + run.schedule('afterRender', this, function () { + this.get('store').query('role', {permissions: 'assign'}).then((roles) => { + let authorRole = roles.findBy('name', 'Author'); + + this.set('roles', roles); + this.set('authorRole', authorRole); + + if (!this.get('role')) { + this.set('role', authorRole); + } + }); + }); + }, + + willDestroyElement() { + this._super(...arguments); + // TODO: this should not be needed, ValidationEngine acts as a + // singleton and so it's errors and hasValidated state stick around + this.get('errors').clear(); + this.set('hasValidated', emberA()); + }, + + validate() { + let email = this.get('email'); + + // TODO: either the validator should check the email's existence or + // the API should return an appropriate error when attempting to save + return new RSVP.Promise((resolve, reject) => { + return this._super().then(() => { + this.get('store').findAll('user', {reload: true}).then((result) => { + let invitedUser = result.findBy('email', email); + + if (invitedUser) { + this.get('errors').clear('email'); + if (invitedUser.get('status') === 'invited' || invitedUser.get('status') === 'invited-pending') { + this.get('errors').add('email', 'A user with that email address was already invited.'); + } else { + this.get('errors').add('email', 'A user with that email address already exists.'); + } + + // TODO: this shouldn't be needed, ValidationEngine doesn't mark + // properties as validated when validating an entire object + this.get('hasValidated').addObject('email'); + reject(); + } else { + resolve(); + } + }); + }, () => { + // TODO: this shouldn't be needed, ValidationEngine doesn't mark + // properties as validated when validating an entire object + this.get('hasValidated').addObject('email'); + reject(); + }); + }); + }, + + actions: { + setRole(role) { + this.set('role', role); + }, + + confirm() { + let email = this.get('email'); + let role = this.get('role'); + let notifications = this.get('notifications'); + let newUser; + + this.validate().then(() => { + this.set('submitting', true); + + newUser = this.get('store').createRecord('user', { + email, + role, + status: 'invited' + }); + + newUser.save().then(() => { + let notificationText = `Invitation sent! (${email})`; + + // If sending the invitation email fails, the API will still return a status of 201 + // but the user's status in the response object will be 'invited-pending'. + if (newUser.get('status') === 'invited-pending') { + notifications.showAlert('Invitation email was not sent. Please try resending.', {type: 'error', key: 'invite.send.failed'}); + } else { + notifications.showNotification(notificationText, {key: 'invite.send.success'}); + } + }).catch((errors) => { + newUser.deleteRecord(); + notifications.showErrors(errors, {key: 'invite.send'}); + }).finally(() => { + this.send('closeModal'); + }); + }); + } + } +}); diff --git a/core/client/app/components/modals/leave-editor.js b/core/client/app/components/modals/leave-editor.js new file mode 100644 index 0000000000..498c2a37ea --- /dev/null +++ b/core/client/app/components/modals/leave-editor.js @@ -0,0 +1,11 @@ +import ModalComponent from 'ghost/components/modals/base'; + +export default ModalComponent.extend({ + actions: { + confirm() { + this.attrs.confirm().finally(() => { + this.send('closeModal'); + }); + } + } +}); diff --git a/core/client/app/components/modals/markdown-help.js b/core/client/app/components/modals/markdown-help.js new file mode 100644 index 0000000000..7827870b6b --- /dev/null +++ b/core/client/app/components/modals/markdown-help.js @@ -0,0 +1,4 @@ +import ModalComponent from 'ghost/components/modals/base'; + +export default ModalComponent.extend({ +}); diff --git a/core/client/app/components/modals/re-authenticate.js b/core/client/app/components/modals/re-authenticate.js new file mode 100644 index 0000000000..91c18c91f7 --- /dev/null +++ b/core/client/app/components/modals/re-authenticate.js @@ -0,0 +1,64 @@ +import Ember from 'ember'; +import ModalComponent from 'ghost/components/modals/base'; +import ValidationEngine from 'ghost/mixins/validation-engine'; + +const {$, computed, inject} = Ember; + +export default ModalComponent.extend(ValidationEngine, { + validationType: 'signin', + + submitting: false, + authenticationError: null, + + notifications: inject.service(), + session: inject.service(), + + identification: computed('session.user.email', function () { + return this.get('session.user.email'); + }), + + _authenticate() { + let session = this.get('session'); + let authStrategy = 'authenticator:oauth2'; + let identification = this.get('identification'); + let password = this.get('password'); + + session.set('skipAuthSuccessHandler', true); + + this.toggleProperty('submitting'); + + return session.authenticate(authStrategy, identification, password).finally(() => { + this.toggleProperty('submitting'); + session.set('skipAuthSuccessHandler', undefined); + }); + }, + + actions: { + confirm() { + // Manually trigger events for input fields, ensuring legacy compatibility with + // browsers and password managers that don't send proper events on autofill + $('#login').find('input').trigger('change'); + + this.set('authenticationError', null); + + this.validate({property: 'signin'}).then(() => { + this._authenticate().then(() => { + this.get('notifications').closeAlerts('post.save'); + this.send('closeModal'); + }).catch((error) => { + if (error && error.errors) { + error.errors.forEach((err) => { + err.message = Ember.String.htmlSafe(err.message); + }); + + this.get('errors').add('password', 'Incorrect password'); + this.get('hasValidated').pushObject('password'); + this.set('authenticationError', error.errors[0].message); + } + }); + }, () => { + this.get('hasValidated').pushObject('password'); + }); + } + } +}); diff --git a/core/client/app/components/modals/transfer-owner.js b/core/client/app/components/modals/transfer-owner.js new file mode 100644 index 0000000000..d4e432e8c6 --- /dev/null +++ b/core/client/app/components/modals/transfer-owner.js @@ -0,0 +1,17 @@ +import ModalComponent from 'ghost/components/modals/base'; + +export default ModalComponent.extend({ + + user: null, + submitting: false, + + actions: { + confirm() { + this.set('submitting', true); + + this.attrs.confirm().finally(() => { + this.send('closeModal'); + }); + } + } +}); diff --git a/core/client/app/components/modals/upload-image.js b/core/client/app/components/modals/upload-image.js new file mode 100644 index 0000000000..f14b975344 --- /dev/null +++ b/core/client/app/components/modals/upload-image.js @@ -0,0 +1,86 @@ +import Ember from 'ember'; +import ModalComponent from 'ghost/components/modals/base'; +import upload from 'ghost/assets/lib/uploader'; +import cajaSanitizers from 'ghost/utils/caja-sanitizers'; + +const {computed, inject, isEmpty} = Ember; + +export default ModalComponent.extend({ + + acceptEncoding: 'image/*', + model: null, + submitting: false, + + config: inject.service(), + notifications: inject.service(), + + imageUrl: computed('model.model', 'model.imageProperty', { + get() { + let imageProperty = this.get('model.imageProperty'); + + return this.get(`model.model.${imageProperty}`); + }, + + set(key, value) { + let model = this.get('model.model'); + let imageProperty = this.get('model.imageProperty'); + + return model.set(imageProperty, value); + } + }), + + didInsertElement() { + this._super(...arguments); + upload.call(this.$('.js-drop-zone'), { + fileStorage: this.get('config.fileStorage') + }); + }, + + keyDown() { + this._setErrorState(false); + }, + + _setErrorState(state) { + if (state) { + this.$('.js-upload-url').addClass('error'); + } else { + this.$('.js-upload-url').removeClass('error'); + } + }, + + _setImageProperty() { + let value; + + if (this.$('.js-upload-url').val()) { + value = this.$('.js-upload-url').val(); + + if (!isEmpty(value) && !cajaSanitizers.url(value)) { + this._setErrorState(true); + return {message: 'Image URI is not valid'}; + } + } else { + value = this.$('.js-upload-target').attr('src'); + } + + this.set('imageUrl', value); + return true; + }, + + actions: { + confirm() { + let model = this.get('model.model'); + let notifications = this.get('notifications'); + let result = this._setImageProperty(); + + if (!result.message) { + this.set('submitting', true); + + model.save().catch((err) => { + notifications.showAPIError(err, {key: 'image.upload'}); + }).finally(() => { + this.send('closeModal'); + }); + } + } + } +}); diff --git a/core/client/app/controllers/application.js b/core/client/app/controllers/application.js index 3b5bb2fb82..d155ffa2c4 100644 --- a/core/client/app/controllers/application.js +++ b/core/client/app/controllers/application.js @@ -10,6 +10,7 @@ export default Controller.extend({ topNotificationCount: 0, showMobileMenu: false, showSettingsMenu: false, + showMarkdownHelpModal: false, autoNav: false, autoNavOpen: computed('autoNav', { diff --git a/core/client/app/controllers/editor/edit.js b/core/client/app/controllers/editor/edit.js index 828e21f746..7cb7b57523 100644 --- a/core/client/app/controllers/editor/edit.js +++ b/core/client/app/controllers/editor/edit.js @@ -4,9 +4,11 @@ import EditorControllerMixin from 'ghost/mixins/editor-base-controller'; const {Controller} = Ember; export default Controller.extend(EditorControllerMixin, { + showDeletePostModal: false, + actions: { - openDeleteModal() { - this.send('openModal', 'delete-post', this.get('model')); + toggleDeletePostModal() { + this.toggleProperty('showDeletePostModal'); } } }); diff --git a/core/client/app/controllers/modals/copy-html.js b/core/client/app/controllers/modals/copy-html.js deleted file mode 100644 index 7905d43949..0000000000 --- a/core/client/app/controllers/modals/copy-html.js +++ /dev/null @@ -1,8 +0,0 @@ -import Ember from 'ember'; - -const {Controller, computed} = Ember; -const {alias} = computed; - -export default Controller.extend({ - generatedHTML: alias('model.generatedHTML') -}); diff --git a/core/client/app/controllers/modals/delete-all.js b/core/client/app/controllers/modals/delete-all.js deleted file mode 100644 index 3980a3e25a..0000000000 --- a/core/client/app/controllers/modals/delete-all.js +++ /dev/null @@ -1,38 +0,0 @@ -import Ember from 'ember'; -import {request as ajax} from 'ic-ajax'; - -const {Controller, inject} = Ember; - -export default Controller.extend({ - ghostPaths: inject.service('ghost-paths'), - notifications: inject.service(), - - confirm: { - accept: { - text: 'Delete', - buttonClass: 'btn btn-red' - }, - reject: { - text: 'Cancel', - buttonClass: 'btn btn-default btn-minor' - } - }, - - actions: { - confirmAccept() { - ajax(this.get('ghostPaths.url').api('db'), { - type: 'DELETE' - }).then(() => { - this.get('notifications').showAlert('All content deleted from database.', {type: 'success', key: 'all-content.delete.success'}); - this.store.unloadAll('post'); - this.store.unloadAll('tag'); - }).catch((response) => { - this.get('notifications').showAPIError(response, {key: 'all-content.delete'}); - }); - }, - - confirmReject() { - return false; - } - } -}); diff --git a/core/client/app/controllers/modals/delete-post.js b/core/client/app/controllers/modals/delete-post.js deleted file mode 100644 index 560f1e4ff7..0000000000 --- a/core/client/app/controllers/modals/delete-post.js +++ /dev/null @@ -1,40 +0,0 @@ -import Ember from 'ember'; - -const {Controller, inject} = Ember; - -export default Controller.extend({ - dropdown: inject.service(), - notifications: inject.service(), - - confirm: { - accept: { - text: 'Delete', - buttonClass: 'btn btn-red' - }, - reject: { - text: 'Cancel', - buttonClass: 'btn btn-default btn-minor' - } - }, - - actions: { - confirmAccept() { - let model = this.get('model'); - - // definitely want to clear the data store and post of any unsaved, client-generated tags - model.updateTags(); - - model.destroyRecord().then(() => { - this.get('dropdown').closeDropdowns(); - this.get('notifications').closeAlerts('post.delete'); - this.transitionToRoute('posts.index'); - }, () => { - this.get('notifications').showAlert('Your post could not be deleted. Please try again.', {type: 'error', key: 'post.delete.failed'}); - }); - }, - - confirmReject() { - return false; - } - } -}); diff --git a/core/client/app/controllers/modals/delete-tag.js b/core/client/app/controllers/modals/delete-tag.js deleted file mode 100644 index 08c1dfa5d2..0000000000 --- a/core/client/app/controllers/modals/delete-tag.js +++ /dev/null @@ -1,45 +0,0 @@ -import Ember from 'ember'; - -const {Controller, computed, inject} = Ember; - -export default Controller.extend({ - application: inject.controller(), - notifications: inject.service(), - - postInflection: computed('model.count.posts', function () { - return this.get('model.count.posts') > 1 ? 'posts' : 'post'; - }), - - actions: { - confirmAccept() { - let tag = this.get('model'); - - this.send('closeMenus'); - - tag.destroyRecord().then(() => { - let currentRoute = this.get('application.currentRouteName') || ''; - - if (currentRoute.match(/^settings\.tags/)) { - this.transitionToRoute('settings.tags.index'); - } - }).catch((error) => { - this.get('notifications').showAPIError(error, {key: 'tag.delete'}); - }); - }, - - confirmReject() { - return false; - } - }, - - confirm: { - accept: { - text: 'Delete', - buttonClass: 'btn btn-red' - }, - reject: { - text: 'Cancel', - buttonClass: 'btn btn-default btn-minor' - } - } -}); diff --git a/core/client/app/controllers/modals/delete-user.js b/core/client/app/controllers/modals/delete-user.js deleted file mode 100644 index 8844b37cd9..0000000000 --- a/core/client/app/controllers/modals/delete-user.js +++ /dev/null @@ -1,56 +0,0 @@ -import Ember from 'ember'; - -const {Controller, PromiseProxyMixin, computed, inject} = Ember; -const {alias} = computed; - -export default Controller.extend({ - notifications: inject.service(), - - userPostCount: computed('model.id', function () { - let query = { - filter: `author:${this.get('model.slug')}`, - status: 'all' - }; - - let promise = this.store.query('post', query).then((results) => { - return results.meta.pagination.total; - }); - - return Ember.Object.extend(PromiseProxyMixin, { - count: alias('content'), - - inflection: computed('count', function () { - return this.get('count') > 1 ? 'posts' : 'post'; - }) - }).create({promise}); - }), - - confirm: { - accept: { - text: 'Delete User', - buttonClass: 'btn btn-red' - }, - reject: { - text: 'Cancel', - buttonClass: 'btn btn-default btn-minor' - } - }, - - actions: { - confirmAccept() { - let user = this.get('model'); - - user.destroyRecord().then(() => { - this.get('notifications').closeAlerts('user.delete'); - this.store.unloadAll('post'); - this.transitionToRoute('team'); - }, () => { - this.get('notifications').showAlert('The user could not be deleted. Please try again.', {type: 'error', key: 'user.delete.failed'}); - }); - }, - - confirmReject() { - return false; - } - } -}); diff --git a/core/client/app/controllers/modals/invite-new-user.js b/core/client/app/controllers/modals/invite-new-user.js deleted file mode 100644 index 8a8b8b9592..0000000000 --- a/core/client/app/controllers/modals/invite-new-user.js +++ /dev/null @@ -1,104 +0,0 @@ -import Ember from 'ember'; -import ValidationEngine from 'ghost/mixins/validation-engine'; - -const {Controller, computed, inject, observer} = Ember; - -export default Controller.extend(ValidationEngine, { - notifications: inject.service(), - - validationType: 'signup', - - role: null, - authorRole: null, - - roles: computed(function () { - return this.store.query('role', {permissions: 'assign'}); - }), - - // Used to set the initial value for the dropdown - authorRoleObserver: observer('roles.@each.role', function () { - this.get('roles').then((roles) => { - let authorRole = roles.findBy('name', 'Author'); - - this.set('authorRole', authorRole); - - if (!this.get('role')) { - this.set('role', authorRole); - } - }); - }), - - confirm: { - accept: { - text: 'send invitation now' - }, - reject: { - buttonClass: 'hidden' - } - }, - - confirmReject() { - return false; - }, - - actions: { - setRole(role) { - this.set('role', role); - }, - - confirmAccept() { - let email = this.get('email'); - let role = this.get('role'); - let validationErrors = this.get('errors.messages'); - let newUser; - - // reset the form and close the modal - this.set('email', ''); - this.set('role', this.get('authorRole')); - - this.store.findAll('user', {reload: true}).then((result) => { - let invitedUser = result.findBy('email', email); - - if (invitedUser) { - if (invitedUser.get('status') === 'invited' || invitedUser.get('status') === 'invited-pending') { - this.get('notifications').showAlert('A user with that email address was already invited.', {type: 'warn', key: 'invite.send.already-invited'}); - } else { - this.get('notifications').showAlert('A user with that email address already exists.', {type: 'warn', key: 'invite.send.user-exists'}); - } - } else { - newUser = this.store.createRecord('user', { - email, - role, - status: 'invited' - }); - - newUser.save().then(() => { - let notificationText = `Invitation sent! (${email})`; - - // If sending the invitation email fails, the API will still return a status of 201 - // but the user's status in the response object will be 'invited-pending'. - if (newUser.get('status') === 'invited-pending') { - this.get('notifications').showAlert('Invitation email was not sent. Please try resending.', {type: 'error', key: 'invite.send.failed'}); - } else { - this.get('notifications').closeAlerts('invite.send'); - this.get('notifications').showNotification(notificationText); - } - }).catch((errors) => { - newUser.deleteRecord(); - // TODO: user model includes ValidationEngine mixin so - // save is overridden in order to validate, we probably - // want to use inline-validations here and only show an - // alert if we have an actual error - if (errors) { - this.get('notifications').showErrors(errors, {key: 'invite.send'}); - } else if (validationErrors) { - this.get('notifications').showAlert(validationErrors.toString(), {type: 'error', key: 'invite.send.validation-error'}); - } - }).finally(() => { - this.get('errors').clear(); - }); - } - }); - } - } -}); diff --git a/core/client/app/controllers/modals/leave-editor.js b/core/client/app/controllers/modals/leave-editor.js deleted file mode 100644 index 5d0ff8b840..0000000000 --- a/core/client/app/controllers/modals/leave-editor.js +++ /dev/null @@ -1,64 +0,0 @@ -import Ember from 'ember'; - -const {Controller, computed, inject, isArray} = Ember; -const {alias} = computed; - -export default Controller.extend({ - notifications: inject.service(), - - args: alias('model'), - - confirm: { - accept: { - text: 'Leave', - buttonClass: 'btn btn-red' - }, - reject: { - text: 'Stay', - buttonClass: 'btn btn-default btn-minor' - } - }, - - actions: { - confirmAccept() { - let args = this.get('args'); - let editorController, - model, - transition; - - if (isArray(args)) { - editorController = args[0]; - transition = args[1]; - model = editorController.get('model'); - } - - if (!transition || !editorController) { - this.get('notifications').showNotification('Sorry, there was an error in the application. Please let the Ghost team know what happened.', {type: 'error'}); - - return true; - } - - // definitely want to clear the data store and post of any unsaved, client-generated tags - model.updateTags(); - - if (model.get('isNew')) { - // the user doesn't want to save the new, unsaved post, so delete it. - model.deleteRecord(); - } else { - // roll back changes on model props - model.rollbackAttributes(); - } - - // setting hasDirtyAttributes to false here allows willTransition on the editor route to succeed - editorController.set('hasDirtyAttributes', false); - - // since the transition is now certain to complete, we can unset window.onbeforeunload here - window.onbeforeunload = null; - - transition.retry(); - }, - - confirmReject() { - } - } -}); diff --git a/core/client/app/controllers/modals/signin.js b/core/client/app/controllers/modals/signin.js deleted file mode 100644 index 8a4dd62522..0000000000 --- a/core/client/app/controllers/modals/signin.js +++ /dev/null @@ -1,57 +0,0 @@ -import Ember from 'ember'; -import ValidationEngine from 'ghost/mixins/validation-engine'; - -const {$, Controller, computed, inject} = Ember; - -export default Controller.extend(ValidationEngine, { - validationType: 'signin', - submitting: false, - - application: inject.controller(), - notifications: inject.service(), - session: inject.service(), - - identification: computed('session.user.email', function () { - return this.get('session.user.email'); - }), - - actions: { - authenticate() { - let appController = this.get('application'); - let authStrategy = 'authenticator:oauth2'; - - appController.set('skipAuthSuccessHandler', true); - - this.get('session').authenticate(authStrategy, this.get('identification'), this.get('password')).then(() => { - this.send('closeModal'); - this.set('password', ''); - this.get('notifications').closeAlerts('post.save'); - }).catch(() => { - // if authentication fails a rejected promise will be returned. - // it needs to be caught so it doesn't generate an exception in the console, - // but it's actually "handled" by the sessionAuthenticationFailed action handler. - }).finally(() => { - this.toggleProperty('submitting'); - appController.set('skipAuthSuccessHandler', undefined); - }); - }, - - validateAndAuthenticate() { - this.toggleProperty('submitting'); - - // Manually trigger events for input fields, ensuring legacy compatibility with - // browsers and password managers that don't send proper events on autofill - $('#login').find('input').trigger('change'); - - this.validate({format: false}).then(() => { - this.send('authenticate'); - }).catch((errors) => { - this.get('notifications').showErrors(errors); - }); - }, - - confirmAccept() { - this.send('validateAndAuthenticate'); - } - } -}); diff --git a/core/client/app/controllers/modals/transfer-owner.js b/core/client/app/controllers/modals/transfer-owner.js deleted file mode 100644 index ada0bc1601..0000000000 --- a/core/client/app/controllers/modals/transfer-owner.js +++ /dev/null @@ -1,58 +0,0 @@ -import Ember from 'ember'; -import {request as ajax} from 'ic-ajax'; - -const {Controller, inject, isArray} = Ember; - -export default Controller.extend({ - dropdown: inject.service(), - ghostPaths: inject.service('ghost-paths'), - notifications: inject.service(), - - confirm: { - accept: { - text: 'Yep - I\'m sure', - buttonClass: 'btn btn-red' - }, - reject: { - text: 'Cancel', - buttonClass: 'btn btn-default btn-minor' - } - }, - - actions: { - confirmAccept() { - let user = this.get('model'); - let url = this.get('ghostPaths.url').api('users', 'owner'); - - this.get('dropdown').closeDropdowns(); - - ajax(url, { - type: 'PUT', - data: { - owner: [{ - id: user.get('id') - }] - } - }).then((response) => { - // manually update the roles for the users that just changed roles - // because store.pushPayload is not working with embedded relations - if (response && isArray(response.users)) { - response.users.forEach((userJSON) => { - let user = this.store.peekRecord('user', userJSON.id); - let role = this.store.peekRecord('role', userJSON.roles[0].id); - - user.set('role', role); - }); - } - - this.get('notifications').showAlert(`Ownership successfully transferred to ${user.get('name')}`, {type: 'success', key: 'owner.transfer.success'}); - }).catch((error) => { - this.get('notifications').showAPIError(error, {key: 'owner.transfer'}); - }); - }, - - confirmReject() { - return false; - } - } -}); diff --git a/core/client/app/controllers/modals/upload.js b/core/client/app/controllers/modals/upload.js deleted file mode 100644 index 880a86e8ec..0000000000 --- a/core/client/app/controllers/modals/upload.js +++ /dev/null @@ -1,25 +0,0 @@ -import Ember from 'ember'; - -const {Controller, inject} = Ember; - -export default Controller.extend({ - notifications: inject.service(), - - acceptEncoding: 'image/*', - - actions: { - confirmAccept() { - let notifications = this.get('notifications'); - - this.get('model').save().then((model) => { - return model; - }).catch((err) => { - notifications.showAPIError(err, {key: 'image.upload'}); - }); - }, - - confirmReject() { - return false; - } - } -}); diff --git a/core/client/app/controllers/posts.js b/core/client/app/controllers/posts.js index 852eeb5e69..5518567607 100644 --- a/core/client/app/controllers/posts.js +++ b/core/client/app/controllers/posts.js @@ -68,6 +68,8 @@ function publishedAtCompare(item1, item2) { export default Controller.extend({ + showDeletePostModal: false, + // See PostsRoute's shortcuts postListFocused: equal('keyboardFocus', 'postList'), postContentFocused: equal('keyboardFocus', 'postContent'), @@ -85,6 +87,10 @@ export default Controller.extend({ } this.transitionToRoute('posts.post', post); + }, + + toggleDeletePostModal() { + this.toggleProperty('showDeletePostModal'); } } }); diff --git a/core/client/app/controllers/settings/general.js b/core/client/app/controllers/settings/general.js index 304c76bdc9..ad6f71ccd0 100644 --- a/core/client/app/controllers/settings/general.js +++ b/core/client/app/controllers/settings/general.js @@ -5,6 +5,10 @@ import randomPassword from 'ghost/utils/random-password'; const {Controller, computed, inject, observer} = Ember; export default Controller.extend(SettingsSaveMixin, { + + showUploadLogoModal: false, + showUploadCoverModal: false, + notifications: inject.service(), config: inject.service(), @@ -97,6 +101,14 @@ export default Controller.extend(SettingsSaveMixin, { setTheme(theme) { this.set('model.activeTheme', theme.name); + }, + + toggleUploadCoverModal() { + this.toggleProperty('showUploadCoverModal'); + }, + + toggleUploadLogoModal() { + this.toggleProperty('showUploadLogoModal'); } } }); diff --git a/core/client/app/controllers/settings/labs.js b/core/client/app/controllers/settings/labs.js index cf63ed21ef..b1602bc381 100644 --- a/core/client/app/controllers/settings/labs.js +++ b/core/client/app/controllers/settings/labs.js @@ -7,6 +7,7 @@ export default Controller.extend({ uploadButtonText: 'Import', importErrors: '', submitting: false, + showDeleteAllModal: false, ghostPaths: inject.service('ghost-paths'), notifications: inject.service(), @@ -65,8 +66,7 @@ export default Controller.extend({ // Reload currentUser and set session this.set('session.user', this.store.findRecord('user', currentUserId)); // TODO: keep as notification, add link to view content - notifications.showNotification('Import successful.'); - notifications.closeAlerts('import.upload'); + notifications.showNotification('Import successful.', {key: 'import.upload.success'}); }).catch((response) => { if (response && response.jqXHR && response.jqXHR.responseJSON && response.jqXHR.responseJSON.errors) { this.set('importErrors', response.jqXHR.responseJSON.errors); @@ -109,6 +109,10 @@ export default Controller.extend({ } this.toggleProperty('submitting'); }); + }, + + toggleDeleteAllModal() { + this.toggleProperty('showDeleteAllModal'); } } }); diff --git a/core/client/app/controllers/settings/tags/tag.js b/core/client/app/controllers/settings/tags/tag.js index 1fb1881a51..101917a3a5 100644 --- a/core/client/app/controllers/settings/tags/tag.js +++ b/core/client/app/controllers/settings/tags/tag.js @@ -5,13 +5,16 @@ const {alias} = computed; export default Controller.extend({ + showDeleteTagModal: false, + tag: alias('model'), isMobile: alias('tagsController.isMobile'), + applicationController: inject.controller('application'), tagsController: inject.controller('settings.tags'), notifications: inject.service(), - saveTagProperty(propKey, newValue) { + _saveTagProperty(propKey, newValue) { let tag = this.get('tag'); let currentValue = tag.get(propKey); @@ -36,9 +39,39 @@ export default Controller.extend({ }); }, + _deleteTag() { + let tag = this.get('tag'); + + return tag.destroyRecord().then(() => { + this._deleteTagSuccess(); + }, (error) => { + this._deleteTagFailure(error); + }); + }, + + _deleteTagSuccess() { + let currentRoute = this.get('applicationController.currentRouteName') || ''; + + if (currentRoute.match(/^settings\.tags/)) { + this.transitionToRoute('settings.tags.index'); + } + }, + + _deleteTagFailure(error) { + this.get('notifications').showAPIError(error, {key: 'tag.delete'}); + }, + actions: { setProperty(propKey, value) { - this.saveTagProperty(propKey, value); + this._saveTagProperty(propKey, value); + }, + + toggleDeleteTagModal() { + this.toggleProperty('showDeleteTagModal'); + }, + + deleteTag() { + return this._deleteTag(); } } }); diff --git a/core/client/app/controllers/setup/two.js b/core/client/app/controllers/setup/two.js index 69873d02bb..a9cea253c0 100644 --- a/core/client/app/controllers/setup/two.js +++ b/core/client/app/controllers/setup/two.js @@ -123,12 +123,14 @@ export default Controller.extend(ValidationEngine, { } // Don't call the success handler, otherwise we will be redirected to admin - this.get('application').set('skipAuthSuccessHandler', true); + this.set('session.skipAuthSuccessHandler', true); this.get('session').authenticate('authenticator:oauth2', this.get('email'), this.get('password')).then(() => { this.set('blogCreated', true); return this.afterAuthentication(result); }).catch((error) => { this._handleAuthenticationError(error); + }).finally(() => { + this.set('session.skipAuthSuccessHandler', undefined); }); }).catch((error) => { this._handleSaveError(error); diff --git a/core/client/app/controllers/team/index.js b/core/client/app/controllers/team/index.js index b6ab30e298..b534ff47e8 100644 --- a/core/client/app/controllers/team/index.js +++ b/core/client/app/controllers/team/index.js @@ -5,10 +5,12 @@ const {alias, filter} = computed; export default Controller.extend({ - session: inject.service(), + showInviteUserModal: false, users: alias('model'), + session: inject.service(), + activeUsers: filter('users', function (user) { return /^active|warn-[1-4]|locked$/.test(user.get('status')); }), @@ -17,5 +19,11 @@ export default Controller.extend({ let status = user.get('status'); return status === 'invited' || status === 'invited-pending'; - }) + }), + + actions: { + toggleInviteUserModal() { + this.toggleProperty('showInviteUserModal'); + } + } }); diff --git a/core/client/app/controllers/team/user.js b/core/client/app/controllers/team/user.js index e320056804..6638555c1a 100644 --- a/core/client/app/controllers/team/user.js +++ b/core/client/app/controllers/team/user.js @@ -1,42 +1,45 @@ import Ember from 'ember'; +import {request as ajax} from 'ic-ajax'; import SlugGenerator from 'ghost/models/slug-generator'; import isNumber from 'ghost/utils/isNumber'; import boundOneWay from 'ghost/utils/bound-one-way'; import ValidationEngine from 'ghost/mixins/validation-engine'; -const {Controller, RSVP, computed, inject} = Ember; +const {Controller, RSVP, computed, inject, isArray} = Ember; const {alias, and, not, or, readOnly} = computed; export default Controller.extend(ValidationEngine, { // ValidationEngine settings validationType: 'user', submitting: false, + lastPromise: null, + showDeleteUserModal: false, + showTransferOwnerModal: false, + showUploadCoverModal: false, + showUplaodImageModal: false, + dropdown: inject.service(), ghostPaths: inject.service('ghost-paths'), notifications: inject.service(), session: inject.service(), - lastPromise: null, - - currentUser: alias('session.user'), user: alias('model'), - email: readOnly('user.email'), - slugValue: boundOneWay('user.slug'), + currentUser: alias('session.user'), + + email: readOnly('model.email'), + slugValue: boundOneWay('model.slug'), + + isNotOwnersProfile: not('user.isOwner'), + isAdminUserOnOwnerProfile: and('currentUser.isAdmin', 'user.isOwner'), + canAssignRoles: or('currentUser.isAdmin', 'currentUser.isOwner'), + canMakeOwner: and('currentUser.isOwner', 'isNotOwnProfile', 'user.isAdmin'), + rolesDropdownIsVisible: and('isNotOwnProfile', 'canAssignRoles', 'isNotOwnersProfile'), + userActionsAreVisible: or('deleteUserActionIsVisible', 'canMakeOwner'), isNotOwnProfile: computed('user.id', 'currentUser.id', function () { return this.get('user.id') !== this.get('currentUser.id'); }), - isNotOwnersProfile: not('user.isOwner'), - - isAdminUserOnOwnerProfile: and('currentUser.isAdmin', 'user.isOwner'), - - canAssignRoles: or('currentUser.isAdmin', 'currentUser.isOwner'), - - canMakeOwner: and('currentUser.isOwner', 'isNotOwnProfile', 'user.isAdmin'), - - rolesDropdownIsVisible: and('isNotOwnProfile', 'canAssignRoles', 'isNotOwnersProfile'), - deleteUserActionIsVisible: computed('currentUser', 'canAssignRoles', 'user', function () { if ((this.get('canAssignRoles') && this.get('isNotOwnProfile') && !this.get('user.isOwner')) || (this.get('currentUser.isEditor') && (this.get('isNotOwnProfile') || @@ -45,8 +48,6 @@ export default Controller.extend(ValidationEngine, { } }), - userActionsAreVisible: or('deleteUserActionIsVisible', 'canMakeOwner'), - // duplicated in gh-user-active -- find a better home and consolidate? userDefault: computed('ghostPaths', function () { return this.get('ghostPaths.url').asset('/shared/img/user-image.png'); @@ -85,6 +86,23 @@ export default Controller.extend(ValidationEngine, { return this.store.query('role', {permissions: 'assign'}); }), + _deleteUser() { + if (this.get('deleteUserActionIsVisible')) { + let user = this.get('user'); + return user.destroyRecord(); + } + }, + + _deleteUserSuccess() { + this.get('notifications').closeAlerts('user.delete'); + this.store.unloadAll('post'); + this.transitionToRoute('team'); + }, + + _deleteUserFailure() { + this.get('notifications').showAlert('The user could not be deleted. Please try again.', {type: 'error', key: 'user.delete.failed'}); + }, + actions: { changeRole(newRole) { this.set('model.role', newRole); @@ -137,6 +155,20 @@ export default Controller.extend(ValidationEngine, { this.set('lastPromise', promise); }, + deleteUser() { + return this._deleteUser().then(() => { + this._deleteUserSuccess(); + }, () => { + this._deleteUserFailure(); + }); + }, + + toggleDeleteUserModal() { + if (this.get('deleteUserActionIsVisible')) { + this.toggleProperty('showDeleteUserModal'); + } + }, + password() { let user = this.get('user'); @@ -210,6 +242,51 @@ export default Controller.extend(ValidationEngine, { }); this.set('lastPromise', promise); + }, + + transferOwnership() { + let user = this.get('user'); + let url = this.get('ghostPaths.url').api('users', 'owner'); + + this.get('dropdown').closeDropdowns(); + + return ajax(url, { + type: 'PUT', + data: { + owner: [{ + id: user.get('id') + }] + } + }).then((response) => { + // manually update the roles for the users that just changed roles + // because store.pushPayload is not working with embedded relations + if (response && isArray(response.users)) { + response.users.forEach((userJSON) => { + let user = this.store.peekRecord('user', userJSON.id); + let role = this.store.peekRecord('role', userJSON.roles[0].id); + + user.set('role', role); + }); + } + + this.get('notifications').showAlert(`Ownership successfully transferred to ${user.get('name')}`, {type: 'success', key: 'owner.transfer.success'}); + }).catch((error) => { + this.get('notifications').showAPIError(error, {key: 'owner.transfer'}); + }); + }, + + toggleTransferOwnerModal() { + if (this.get('canMakeOwner')) { + this.toggleProperty('showTransferOwnerModal'); + } + }, + + toggleUploadCoverModal() { + this.toggleProperty('showUploadCoverModal'); + }, + + toggleUploadImageModal() { + this.toggleProperty('showUploadImageModal'); } } }); diff --git a/core/client/app/mixins/ed-editor-shortcuts.js b/core/client/app/mixins/ed-editor-shortcuts.js index ba04dfe55d..e204f2870b 100644 --- a/core/client/app/mixins/ed-editor-shortcuts.js +++ b/core/client/app/mixins/ed-editor-shortcuts.js @@ -113,7 +113,7 @@ let shortcuts = { } // Talk to the editor - editor.sendAction('openModal', 'copy-html', {generatedHTML}); + editor.send('toggleCopyHTMLModal', generatedHTML); }, currentDate(replacement) { diff --git a/core/client/app/mixins/editor-base-controller.js b/core/client/app/mixins/editor-base-controller.js index e95a0acd6e..0ab628a604 100644 --- a/core/client/app/mixins/editor-base-controller.js +++ b/core/client/app/mixins/editor-base-controller.js @@ -20,6 +20,9 @@ export default Mixin.create({ editor: null, submitting: false, + showLeaveEditorModal: false, + showReAuthenticateModal: false, + postSettingsMenuController: inject.controller('post-settings-menu'), notifications: inject.service(), @@ -251,13 +254,12 @@ export default Mixin.create({ actions: { save(options) { - let status; let prevStatus = this.get('model.status'); let isNew = this.get('model.isNew'); let autoSaveId = this._autoSaveId; let timedSaveId = this._timedSaveId; let psmController = this.get('postSettingsMenuController'); - let promise; + let promise, status; options = options || {}; @@ -388,6 +390,44 @@ export default Mixin.create({ updateHeight(height) { this.set('height', height); + }, + + toggleLeaveEditorModal(transition) { + this.set('leaveEditorTransition', transition); + this.toggleProperty('showLeaveEditorModal'); + }, + + leaveEditor() { + let transition = this.get('leaveEditorTransition'); + let model = this.get('model'); + + if (!transition) { + this.get('notifications').showAlert('Sorry, there was an error in the application. Please let the Ghost team know what happened.', {type: 'error'}); + return; + } + + // definitely want to clear the data store and post of any unsaved, client-generated tags + model.updateTags(); + + if (model.get('isNew')) { + // the user doesn't want to save the new, unsaved post, so delete it. + model.deleteRecord(); + } else { + // roll back changes on model props + model.rollbackAttributes(); + } + + // setting hasDirtyAttributes to false here allows willTransition on the editor route to succeed + this.set('hasDirtyAttributes', false); + + // since the transition is now certain to complete, we can unset window.onbeforeunload here + window.onbeforeunload = null; + + return transition.retry(); + }, + + toggleReAuthenticateModal() { + this.toggleProperty('showReAuthenticateModal'); } } }); diff --git a/core/client/app/mixins/editor-base-route.js b/core/client/app/mixins/editor-base-route.js index ef8f33e121..db843fa9e5 100644 --- a/core/client/app/mixins/editor-base-route.js +++ b/core/client/app/mixins/editor-base-route.js @@ -63,7 +63,7 @@ export default Mixin.create(styleBody, ShortcutsRoute, { if (!fromNewToEdit && !deletedWithoutChanges && controllerIsDirty) { transition.abort(); - this.send('openModal', 'leave-editor', [controller, transition]); + controller.send('toggleLeaveEditorModal', transition); return; } diff --git a/core/client/app/mixins/validation-engine.js b/core/client/app/mixins/validation-engine.js index a72a57d994..914e97e78b 100644 --- a/core/client/app/mixins/validation-engine.js +++ b/core/client/app/mixins/validation-engine.js @@ -12,6 +12,7 @@ import ResetValidator from 'ghost/validators/reset'; import UserValidator from 'ghost/validators/user'; import TagSettingsValidator from 'ghost/validators/tag-settings'; import NavItemValidator from 'ghost/validators/nav-item'; +import InviteUserValidator from 'ghost/validators/invite-user'; const {Mixin, RSVP, isArray} = Ember; const {Errors, Model} = DS; @@ -41,7 +42,8 @@ export default Mixin.create({ reset: ResetValidator, user: UserValidator, tag: TagSettingsValidator, - navItem: NavItemValidator + navItem: NavItemValidator, + inviteUser: InviteUserValidator }, // This adds the Errors object to the validation engine, and shouldn't affect diff --git a/core/client/app/routes/application.js b/core/client/app/routes/application.js index bda1d32ba2..d77ef19c1e 100644 --- a/core/client/app/routes/application.js +++ b/core/client/app/routes/application.js @@ -1,5 +1,3 @@ -/* global key */ - import Ember from 'ember'; import AuthConfiguration from 'ember-simple-auth/configuration'; import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin'; @@ -16,7 +14,6 @@ function K() { let shortcuts = {}; shortcuts.esc = {action: 'closeMenus', scope: 'all'}; -shortcuts.enter = {action: 'confirmModal', scope: 'modal'}; shortcuts[`${ctrlOrCmd}+s`] = {action: 'save', scope: 'all'}; export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, { @@ -37,9 +34,7 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, { }, sessionAuthenticated() { - let appController = this.controllerFor('application'); - - if (appController && appController.get('skipAuthSuccessHandler')) { + if (this.get('session.skipAuthSuccessHandler')) { return; } @@ -64,7 +59,6 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, { closeMenus() { this.get('dropdown').closeDropdowns(); - this.send('closeModal'); this.controller.setProperties({ showSettingsMenu: false, showMobileMenu: false @@ -90,48 +84,6 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, { windowProxy.replaceLocation(AuthConfiguration.baseURL); }, - openModal(modalName, model, type) { - this.get('dropdown').closeDropdowns(); - key.setScope('modal'); - modalName = `modals/${modalName}`; - this.set('modalName', modalName); - - // We don't always require a modal to have a controller - // so we're skipping asserting if one exists - if (this.controllerFor(modalName, true)) { - this.controllerFor(modalName).set('model', model); - - if (type) { - this.controllerFor(modalName).set('imageType', type); - this.controllerFor(modalName).set('src', model.get(type)); - } - } - - return this.render(modalName, { - into: 'application', - outlet: 'modal' - }); - }, - - confirmModal() { - let modalName = this.get('modalName'); - - this.send('closeModal'); - - if (this.controllerFor(modalName, true)) { - this.controllerFor(modalName).send('confirmAccept'); - } - }, - - closeModal() { - this.disconnectOutlet({ - outlet: 'modal', - parentView: 'application' - }); - - key.setScope('default'); - }, - loadServerNotifications(isDelayed) { if (this.get('session.isAuthenticated')) { this.get('session.user').then((user) => { @@ -146,6 +98,10 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, { } }, + toggleMarkdownHelpModal() { + this.get('controller').toggleProperty('showMarkdownHelpModal'); + }, + // noop default for unhandled save (used from shortcuts) save: K } diff --git a/core/client/app/routes/editor/edit.js b/core/client/app/routes/editor/edit.js index 6e8e68ab0b..edb2b4971d 100644 --- a/core/client/app/routes/editor/edit.js +++ b/core/client/app/routes/editor/edit.js @@ -59,7 +59,7 @@ export default AuthenticatedRoute.extend(base, NotFoundHandler, { actions: { authorizationFailed() { - this.send('openModal', 'signin'); + this.get('controller').send('toggleReAuthenticateModal'); } } }); diff --git a/core/client/app/routes/posts/post.js b/core/client/app/routes/posts/post.js index 88b8ea95b0..ac69038b57 100644 --- a/core/client/app/routes/posts/post.js +++ b/core/client/app/routes/posts/post.js @@ -68,7 +68,7 @@ export default AuthenticatedRoute.extend(ShortcutsRoute, { }, deletePost() { - this.send('openModal', 'delete-post', this.get('controller.model')); + this.controllerFor('posts').send('toggleDeletePostModal'); } } }); diff --git a/core/client/app/routes/team/user.js b/core/client/app/routes/team/user.js index 41950a4491..a6f464ea69 100644 --- a/core/client/app/routes/team/user.js +++ b/core/client/app/routes/team/user.js @@ -10,7 +10,7 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, NotFoun classNames: ['team-view-user'], model(params) { - return this.store.queryRecord('user', {slug: params.user_slug}); + return this.store.queryRecord('user', {slug: params.user_slug, include: 'count.posts'}); }, serialize(model) { diff --git a/core/client/app/styles/components/modals.css b/core/client/app/styles/components/modals.css index dfe9000bdc..af5369264c 100644 --- a/core/client/app/styles/components/modals.css +++ b/core/client/app/styles/components/modals.css @@ -2,47 +2,25 @@ /* ---------------------------------------------------------- */ -/* Full screen container +/* Fullscreen Modal /* ---------------------------------------------------------- */ -.modal-container { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1040; - display: none; - overflow-x: auto; - overflow-y: scroll; - padding-right: 10px; - padding-left: 10px; - transition: all 0.15s linear 0s; - transform: translateZ(0); +.fullscreen-modal-liquid-target { + overflow-y: auto; + height: 100vh; } -.modal-background { +.fullscreen-modal-background { position: fixed; top: 0; right: 0; bottom: 0; left: 0; - z-index: 1030; - display: none; + z-index: 0; background: rgba(0, 0, 0, 0.6); } - -/* The modal -/* ---------------------------------------------------------- */ - -.modal, -.modal-action { - right: auto; - left: 50%; - z-index: 1050; - margin-right: auto; - margin-left: auto; +.fullscreen-modal { padding-top: 30px; padding-bottom: 30px; max-width: 550px; @@ -51,35 +29,44 @@ } @media (max-width: 900px) { - .modal, - .modal-action { + .fullscreen-modal { padding: 10px; } } -.modal button, -.modal-action button { - min-width: 100px; +/* Modifiers +/* ---------------------------------------------------------- */ + +.fullscreen-modal-wide { + width: 550px; } -.modal .image-uploader, -.modal .pre-image-uploader, -.modal-action .image-uploader, -.modal-action .pre-image-uploader { - margin: 0; +@media (max-width: 900px) { + .fullscreen-modal-wide { + width: 100%; + } } -.modal-action { +.fullscreen-modal-action { padding: 60px 0 30px; } @media (max-width: 900px) { - .modal-action { + .fullscreen-modal-action { padding: 30px 0; } } +/* The modal +/* ---------------------------------------------------------- */ + +.fullscreen-modal .image-uploader, +.fullscreen-modal .pre-image-uploader { + margin: 0; +} + + /* Modal content /* ---------------------------------------------------------- */ @@ -149,6 +136,7 @@ .modal-footer button { margin-left: 8px; + min-width: 100px; text-align: center; } @@ -157,22 +145,9 @@ } -/* Modifiers +/* Content Modifiers /* ---------------------------------------------------------- */ -.modal-style-wide { - width: 550px; -} - -@media (max-width: 900px) { - .modal-style-wide { - width: 100%; - } -} - -.modal-style-centered { - text-align: center; -} /* Login styles */ .modal-body .login-form { @@ -207,29 +182,3 @@ flex: 1; } } - - -/* Open States -/* ---------------------------------------------------------- */ - -.modal-container.open, -.modal-container.open > .modal, -.modal-container.open > .modal-action { - display: block; -} - -.modal-background.open { - display: block; -} - - -/* Animations -/* ---------------------------------------------------------- */ - -.modal-container.fade-out { - animation-duration: 0.08s; -} - -.modal-background.fade-out { - animation-duration: 0.15s; -} diff --git a/core/client/app/styles/layouts/main.css b/core/client/app/styles/layouts/main.css index 7f0508992b..bc4feb78f8 100644 --- a/core/client/app/styles/layouts/main.css +++ b/core/client/app/styles/layouts/main.css @@ -5,8 +5,12 @@ Ember's app container, set height so that .gh-app and .gh-viewport don't need to use 100vh where bottom of screen gets covered by iOS menus http://nicolas-hoizey.com/2015/02/viewport-height-is-taller-than-the-visible-part-of-the-document-in-some-mobile-browsers.html + + TODO: Once we have routable components it should be possible to remove this + by moving the gh-app component functionality into the application component + which would remove the extra div that this targets. */ -body > .ember-view { +body > .ember-view:not(.liquid-target-container) { height: 100%; } diff --git a/core/client/app/templates/application.hbs b/core/client/app/templates/application.hbs index b1aa13582a..87e5f2c2f3 100644 --- a/core/client/app/templates/application.hbs +++ b/core/client/app/templates/application.hbs @@ -5,7 +5,7 @@
{{#unless signedOut}} - {{gh-nav-menu open=autoNavOpen toggleMaximise="toggleAutoNav" openAutoNav="openAutoNav" openModal="openModal" closeMobileMenu="closeMobileMenu"}} + {{gh-nav-menu open=autoNavOpen toggleMaximise="toggleAutoNav" openAutoNav="openAutoNav" showMarkdownHelp="toggleMarkdownHelpModal" closeMobileMenu="closeMobileMenu"}} {{/unless}} {{#gh-main onMouseEnter="closeAutoNav" data-notification-count=topNotificationCount}} @@ -21,3 +21,9 @@ {{outlet "settings-menu"}}
{{!gh-viewport}} {{/gh-app}} + +{{#if showMarkdownHelpModal}} + {{gh-fullscreen-modal "markdown-help" + close=(route-action "toggleMarkdownHelpModal") + modifier="wide"}} +{{/if}} diff --git a/core/client/app/templates/components/gh-editor.hbs b/core/client/app/templates/components/gh-editor.hbs index f4a0b59217..047bf32b36 100644 --- a/core/client/app/templates/components/gh-editor.hbs +++ b/core/client/app/templates/components/gh-editor.hbs @@ -1 +1,8 @@ -{{yield this}} +{{yield this (action 'toggleCopyHTMLModal')}} + +{{#if showCopyHTMLModal}} + {{gh-fullscreen-modal "copy-html" + model=copyHTMLModalContent + close=(action "toggleCopyHTMLModal") + modifier="action"}} +{{/if}} diff --git a/core/client/app/templates/components/gh-fullscreen-modal.hbs b/core/client/app/templates/components/gh-fullscreen-modal.hbs new file mode 100644 index 0000000000..06ecdf5f79 --- /dev/null +++ b/core/client/app/templates/components/gh-fullscreen-modal.hbs @@ -0,0 +1,11 @@ +
+
+ {{#if hasBlock}} + {{yield}} + {{else}} + {{component modalPath + model=model + confirm=(action 'confirm') + closeModal=(action 'close')}} + {{/if}} +
diff --git a/core/client/app/templates/components/gh-nav-menu.hbs b/core/client/app/templates/components/gh-nav-menu.hbs index 6603bed585..3d7a0da53f 100644 --- a/core/client/app/templates/components/gh-nav-menu.hbs +++ b/core/client/app/templates/components/gh-nav-menu.hbs @@ -53,7 +53,7 @@
  • Tweet @TryGhost!
  • How to Use Ghost
  • -
  • Markdown Help
  • +
  • Markdown Help
  • Wishlist
  • diff --git a/core/client/app/templates/components/modals/copy-html.hbs b/core/client/app/templates/components/modals/copy-html.hbs new file mode 100644 index 0000000000..dbbfd78e4f --- /dev/null +++ b/core/client/app/templates/components/modals/copy-html.hbs @@ -0,0 +1,8 @@ + + + + diff --git a/core/client/app/templates/components/modals/delete-all.hbs b/core/client/app/templates/components/modals/delete-all.hbs new file mode 100644 index 0000000000..5a12e068b5 --- /dev/null +++ b/core/client/app/templates/components/modals/delete-all.hbs @@ -0,0 +1,13 @@ + + + + + + diff --git a/core/client/app/templates/components/modals/delete-post.hbs b/core/client/app/templates/components/modals/delete-post.hbs new file mode 100644 index 0000000000..0c9ab67823 --- /dev/null +++ b/core/client/app/templates/components/modals/delete-post.hbs @@ -0,0 +1,17 @@ + + + + + + diff --git a/core/client/app/templates/components/modals/delete-tag.hbs b/core/client/app/templates/components/modals/delete-tag.hbs new file mode 100644 index 0000000000..be144fdd9e --- /dev/null +++ b/core/client/app/templates/components/modals/delete-tag.hbs @@ -0,0 +1,17 @@ + + + + + + diff --git a/core/client/app/templates/components/modals/delete-user.hbs b/core/client/app/templates/components/modals/delete-user.hbs new file mode 100644 index 0000000000..f74faccc12 --- /dev/null +++ b/core/client/app/templates/components/modals/delete-user.hbs @@ -0,0 +1,17 @@ + + + + + + diff --git a/core/client/app/templates/components/modals/invite-new-user.hbs b/core/client/app/templates/components/modals/invite-new-user.hbs new file mode 100644 index 0000000000..8be5e093fd --- /dev/null +++ b/core/client/app/templates/components/modals/invite-new-user.hbs @@ -0,0 +1,41 @@ + + + + + + diff --git a/core/client/app/templates/components/modals/leave-editor.hbs b/core/client/app/templates/components/modals/leave-editor.hbs new file mode 100644 index 0000000000..15527ba94a --- /dev/null +++ b/core/client/app/templates/components/modals/leave-editor.hbs @@ -0,0 +1,18 @@ + + + + + + diff --git a/core/client/app/templates/modals/markdown.hbs b/core/client/app/templates/components/modals/markdown-help.hbs similarity index 91% rename from core/client/app/templates/modals/markdown.hbs rename to core/client/app/templates/components/modals/markdown-help.hbs index 33ce732284..79dcb4ffb1 100644 --- a/core/client/app/templates/modals/markdown.hbs +++ b/core/client/app/templates/components/modals/markdown-help.hbs @@ -1,5 +1,9 @@ -{{#gh-modal-dialog action="closeModal" showClose=true style="wide" - title="Markdown Help"}} + + + + diff --git a/core/client/app/templates/components/modals/re-authenticate.hbs b/core/client/app/templates/components/modals/re-authenticate.hbs new file mode 100644 index 0000000000..d5dcc0d8ef --- /dev/null +++ b/core/client/app/templates/components/modals/re-authenticate.hbs @@ -0,0 +1,16 @@ + + + + diff --git a/core/client/app/templates/components/modals/transfer-owner.hbs b/core/client/app/templates/components/modals/transfer-owner.hbs new file mode 100644 index 0000000000..6932b0251e --- /dev/null +++ b/core/client/app/templates/components/modals/transfer-owner.hbs @@ -0,0 +1,16 @@ + + + + + + diff --git a/core/client/app/templates/components/modals/upload-image.hbs b/core/client/app/templates/components/modals/upload-image.hbs new file mode 100644 index 0000000000..0787992634 --- /dev/null +++ b/core/client/app/templates/components/modals/upload-image.hbs @@ -0,0 +1,11 @@ + + + diff --git a/core/client/app/templates/editor/edit.hbs b/core/client/app/templates/editor/edit.hbs index 197569c54c..d39c9c4be9 100644 --- a/core/client/app/templates/editor/edit.hbs +++ b/core/client/app/templates/editor/edit.hbs @@ -1,4 +1,4 @@ -{{#gh-editor editorScrollInfo=editorScrollInfo as |ghEditor|}} +{{#gh-editor editorScrollInfo=editorScrollInfo as |ghEditor toggleCopyHTMLModal|}}
    {{#gh-view-title classNames="gh-editor-title" openMobileMenu="openMobileMenu"}} {{gh-trim-focus-input type="text" id="entry-title" placeholder="Your Post Title" value=model.titleScratch tabindex="1" focus=shouldFocusTitle}} @@ -14,7 +14,7 @@ isNew=model.isNew save="save" setSaveType="setSaveType" - delete="openDeleteModal" + delete="toggleDeletePostModal" submitting=submitting }} @@ -23,22 +23,30 @@
    - Markdown + Markdown Markdown Preview - +
    - {{gh-ed-editor classNames="markdown-editor js-markdown-editor" tabindex="1" spellcheck="true" value=model.scratch setEditor="setEditor" updateScrollInfo="updateEditorScrollInfo" openModal="openModal" onFocusIn="autoSaveNew" height=height focus=shouldFocusEditor}} + {{gh-ed-editor classNames="markdown-editor js-markdown-editor" + tabindex="1" + spellcheck="true" + value=model.scratch + setEditor="setEditor" + updateScrollInfo="updateEditorScrollInfo" + toggleCopyHTMLModal=toggleCopyHTMLModal + onFocusIn="autoSaveNew" + height=height + focus=shouldFocusEditor}}
    - - Preview + Preview Markdown Preview @@ -53,3 +61,23 @@
    {{/gh-editor}} + +{{#if showDeletePostModal}} + {{gh-fullscreen-modal "delete-post" + model=model + close=(action "toggleDeletePostModal") + modifier="action wide"}} +{{/if}} + +{{#if showLeaveEditorModal}} + {{gh-fullscreen-modal "leave-editor" + confirm=(action "leaveEditor") + close=(action "toggleLeaveEditorModal") + modifier="action wide"}} +{{/if}} + +{{#if showReAuthenticateModal}} + {{gh-fullscreen-modal "re-authenticate" + close=(action "toggleReAuthenticateModal") + modifier="action wide"}} +{{/if}} diff --git a/core/client/app/templates/modals/copy-html.hbs b/core/client/app/templates/modals/copy-html.hbs deleted file mode 100644 index 0778b53b30..0000000000 --- a/core/client/app/templates/modals/copy-html.hbs +++ /dev/null @@ -1,6 +0,0 @@ -{{#gh-modal-dialog action="closeModal" showClose=true type="action" - title="Generated HTML" confirm=confirm class="copy-html"}} - - {{textarea value=generatedHTML rows="6"}} - -{{/gh-modal-dialog}} diff --git a/core/client/app/templates/modals/delete-all.hbs b/core/client/app/templates/modals/delete-all.hbs deleted file mode 100644 index 67c0524740..0000000000 --- a/core/client/app/templates/modals/delete-all.hbs +++ /dev/null @@ -1,6 +0,0 @@ -{{#gh-modal-dialog action="closeModal" type="action" style="wide" - title="Would you really like to delete all content from your blog?" confirm=confirm}} - -

    This is permanent! No backups, no restores, no magic undo button.
    We warned you, ok?

    - -{{/gh-modal-dialog}} diff --git a/core/client/app/templates/modals/delete-post.hbs b/core/client/app/templates/modals/delete-post.hbs deleted file mode 100644 index 13915e921c..0000000000 --- a/core/client/app/templates/modals/delete-post.hbs +++ /dev/null @@ -1,6 +0,0 @@ -{{#gh-modal-dialog action="closeModal" showClose=true type="action" style="wide" - title="Are you sure you want to delete this post?" confirm=confirm}} - -

    You're about to delete "{{model.title}}".
    This is permanent! No backups, no restores, no magic undo button.
    We warned you, ok?

    - -{{/gh-modal-dialog}} diff --git a/core/client/app/templates/modals/delete-tag.hbs b/core/client/app/templates/modals/delete-tag.hbs deleted file mode 100644 index d9c90af43e..0000000000 --- a/core/client/app/templates/modals/delete-tag.hbs +++ /dev/null @@ -1,9 +0,0 @@ -{{#gh-modal-dialog action="closeModal" showClose=true type="action" style="wide" - title="Are you sure you want to delete this tag?" confirm=confirm}} - - {{#if model.count.posts}} - WARNING: This tag is attached to {{model.count.posts}} {{postInflection}}. You're about to delete "{{model.name}}". This is permanent! No backups, no restores, no magic undo button. We warned you, ok? - {{else}} - WARNING: You're about to delete "{{model.name}}". This is permanent! No backups, no restores, no magic undo button. We warned you, ok? - {{/if}} -{{/gh-modal-dialog}} diff --git a/core/client/app/templates/modals/delete-user.hbs b/core/client/app/templates/modals/delete-user.hbs deleted file mode 100644 index ef6b5fc38a..0000000000 --- a/core/client/app/templates/modals/delete-user.hbs +++ /dev/null @@ -1,12 +0,0 @@ -{{#gh-modal-dialog action="closeModal" showClose=true type="action" style="wide" - title="Are you sure you want to delete this user?" confirm=confirm}} - - {{#unless userPostCount.isPending}} - {{#if userPostCount.count}} - WARNING: This user is the author of {{userPostCount.count}} {{userPostCount.inflection}}. All posts and user data will be deleted. There is no way to recover this. - {{else}} - WARNING: All user data will be deleted. There is no way to recover this. - {{/if}} - {{/unless}} - -{{/gh-modal-dialog}} diff --git a/core/client/app/templates/modals/invite-new-user.hbs b/core/client/app/templates/modals/invite-new-user.hbs deleted file mode 100644 index 8c44403d07..0000000000 --- a/core/client/app/templates/modals/invite-new-user.hbs +++ /dev/null @@ -1,26 +0,0 @@ -{{#gh-modal-dialog action="closeModal" showClose=true type="action" - title="Invite a New User" confirm=confirm class="invite-new-user"}} - -
    - {{#gh-form-group errors=errors hasValidated=hasValidated property="email"}} - - {{gh-input enter="confirmAccept" class="email" id="new-user-email" type="email" placeholder="Email Address" name="email" autofocus="autofocus" - autocapitalize="off" autocorrect="off" value=email focusOut=(action "validate" "email")}} - {{gh-error-message errors=errors property="email"}} - {{/gh-form-group}} - -
    - - - {{gh-select-native id="new-user-role" - content=roles - optionValuePath="id" - optionLabelPath="name" - selection=role - action="setRole" - }} - -
    -
    - -{{/gh-modal-dialog}} diff --git a/core/client/app/templates/modals/leave-editor.hbs b/core/client/app/templates/modals/leave-editor.hbs deleted file mode 100644 index e7c620ce99..0000000000 --- a/core/client/app/templates/modals/leave-editor.hbs +++ /dev/null @@ -1,9 +0,0 @@ -{{#gh-modal-dialog action="closeModal" showClose=true type="action" style="wide" - title="Are you sure you want to leave this page?" confirm=confirm}} - -

    Hey there! It looks like you're in the middle of writing something and you haven't saved all of your - content.

    - -

    Save before you go!

    - -{{/gh-modal-dialog}} diff --git a/core/client/app/templates/modals/signin.hbs b/core/client/app/templates/modals/signin.hbs deleted file mode 100644 index ad6938f397..0000000000 --- a/core/client/app/templates/modals/signin.hbs +++ /dev/null @@ -1,11 +0,0 @@ -{{#gh-modal-dialog action="closeModal" showClose=true type="action" style="wide" animation="fade" - title="Please re-authenticate" confirm=confirm}} - -
    -
    - {{input class="gh-input password" type="password" placeholder="Password" name="password" value=password}} -
    - {{#gh-spin-button class="btn btn-blue" type="submit" action="validateAndAuthenticate" submitting=submitting}}Log in{{/gh-spin-button}} -
    - -{{/gh-modal-dialog}} diff --git a/core/client/app/templates/modals/transfer-owner.hbs b/core/client/app/templates/modals/transfer-owner.hbs deleted file mode 100644 index 7d2899b82a..0000000000 --- a/core/client/app/templates/modals/transfer-owner.hbs +++ /dev/null @@ -1,6 +0,0 @@ -{{#gh-modal-dialog action="closeModal" showClose=true type="action" style="wide" - title="Transfer Ownership" confirm=confirm}} - -

    Are you sure you want to transfer the ownership of this blog? You will not be able to undo this action.

    - -{{/gh-modal-dialog}} diff --git a/core/client/app/templates/modals/upload.hbs b/core/client/app/templates/modals/upload.hbs deleted file mode 100644 index ea19fc871f..0000000000 --- a/core/client/app/templates/modals/upload.hbs +++ /dev/null @@ -1,7 +0,0 @@ -{{#gh-upload-modal action="closeModal" close=true type="action" style="wide" model=model imageType=imageType}} -
    - logo - -
    - -{{/gh-upload-modal}} diff --git a/core/client/app/templates/posts.hbs b/core/client/app/templates/posts.hbs index 79651ac411..3ccceeb160 100644 --- a/core/client/app/templates/posts.hbs +++ b/core/client/app/templates/posts.hbs @@ -44,3 +44,10 @@ {{/gh-content-view-container}} + +{{#if showDeletePostModal}} + {{gh-fullscreen-modal "delete-post" + model=currentPost + close=(action "toggleDeletePostModal") + modifier="action wide"}} +{{/if}} diff --git a/core/client/app/templates/settings/general.hbs b/core/client/app/templates/settings/general.hbs index 99bc4cf853..1fb0cf1e2f 100644 --- a/core/client/app/templates/settings/general.hbs +++ b/core/client/app/templates/settings/general.hbs @@ -31,21 +31,35 @@
    {{#if model.logo}} - + {{else}} - + {{/if}}

    Display a sexy logo for your publication

    + + {{#if showUploadLogoModal}} + {{gh-fullscreen-modal "upload-image" + model=(hash model=model imageProperty="logo") + close=(action "toggleUploadLogoModal") + modifier="action wide"}} + {{/if}}
    {{#if model.cover}} - cover photo + cover photo {{else}} - + {{/if}}

    Display a cover image on your site

    + + {{#if showUploadCoverModal}} + {{gh-fullscreen-modal "upload-image" + model=(hash model=model imageProperty="cover") + close=(action "toggleUploadCoverModal") + modifier="action wide"}} + {{/if}}
    @@ -93,11 +107,11 @@ {{#if model.isPrivate}} - {{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="password"}} - {{gh-input name="general[password]" type="text" value=model.password focusOut=(action "validate" "password")}} - {{gh-error-message errors=model.errors property="password"}} -

    This password will be needed to access your blog. All search engine optimization and social features are now disabled. This password is stored in plaintext.

    - {{/gh-form-group}} + {{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="password"}} + {{gh-input name="general[password]" type="text" value=model.password focusOut=(action "validate" "password")}} + {{gh-error-message errors=model.errors property="password"}} +

    This password will be needed to access your blog. All search engine optimization and social features are now disabled. This password is stored in plaintext.

    + {{/gh-form-group}} {{/if}}
    diff --git a/core/client/app/templates/settings/labs.hbs b/core/client/app/templates/settings/labs.hbs index c952924a26..9286fc8316 100644 --- a/core/client/app/templates/settings/labs.hbs +++ b/core/client/app/templates/settings/labs.hbs @@ -28,7 +28,7 @@
    - +

    Delete all posts and tags from the database.

    @@ -57,3 +57,9 @@ + +{{#if showDeleteAllModal}} + {{gh-fullscreen-modal "delete-all" + close=(action "toggleDeleteAllModal") + modifier="action wide"}} +{{/if}} diff --git a/core/client/app/templates/settings/tags/settings-menu.hbs b/core/client/app/templates/settings/tags/settings-menu.hbs deleted file mode 100644 index 79db7ec772..0000000000 --- a/core/client/app/templates/settings/tags/settings-menu.hbs +++ /dev/null @@ -1,88 +0,0 @@ -
    - {{#gh-tabs-manager selected="showSubview" class="settings-menu-container"}} -
    -
    -

    Tag Settings

    - -
    -
    - {{gh-uploader uploaded="setCoverImage" canceled="clearCoverImage" description="Add tag image" image=activeTag.image initUploader="setUploaderReference" tagName="section"}} -
    - {{#gh-form-group errors=activeTag.errors hasValidated=activeTag.hasValidated property="name"}} - - {{gh-input id="tag-name" name="name" type="text" value=activeTagNameScratch focus-out="saveActiveTagName"}} - {{gh-error-message errors=activeTag.errors property="name"}} - {{/gh-form-group}} - - {{#gh-form-group errors=activeTag.errors hasValidated=activeTag.hasValidated property="slug"}} - - {{gh-input id="tag-url" name="url" type="text" value=activeTagSlugScratch focus-out="saveActiveTagSlug"}} - {{gh-url-preview prefix="tag" slug=activeTagSlugScratch tagName="p" classNames="description"}} - {{gh-error-message errors=activeTag.errors property="slug"}} - {{/gh-form-group}} - - {{#gh-form-group errors=activeTag.errors hasValidated=activeTag.hasValidated property="description"}} - - {{gh-textarea id="tag-description" name="description" value=activeTagDescriptionScratch focus-out="saveActiveTagDescription"}} -

    Maximum: 200 characters. You’ve used {{gh-count-down-characters activeTagDescriptionScratch 200}}

    - {{/gh-form-group}} - - - - {{#unless activeTag.isNew}} - - {{/unless}} -
    -
    -
    {{! .settings-menu-pane }} - -
    - {{#gh-tab-pane}} - {{#if isViewingSubview}} -
    - -

    Meta Data

    -
    {{!flexbox space-between}}
    -
    - -
    -
    - {{#gh-form-group errors=activeTag.errors hasValidated=activeTag.hasValidated property="meta_title"}} - - {{gh-input id="meta-title" name="meta_title" type="text" value=activeTagMetaTitleScratch focus-out="saveActiveTagMetaTitle"}} - {{gh-error-message errors=activeTag.errors property="meta_title"}} -

    Recommended: 70 characters. You’ve used {{gh-count-down-characters activeTagMetaTitleScratch 70}}

    - {{/gh-form-group}} - - {{#gh-form-group errors=activeTag.errors hasValidated=activeTag.hasValidated property="meta_description"}} - - {{gh-textarea id="meta-description" name="meta_description" value=activeTagMetaDescriptionScratch focus-out="saveActiveTagMetaDescription"}} - {{gh-error-message errors=activeTag.errors property="meta_description"}} -

    Recommended: 156 characters. You’ve used {{gh-count-down-characters activeTagMetaDescriptionScratch 156}}

    - {{/gh-form-group}} - -
    - -
    -
    {{seoTitle}}
    - -
    {{seoDescription}}
    -
    -
    -
    -
    {{! .settings-menu-content }} - {{/if}} - {{/gh-tab-pane}} -
    {{! .settings-menu-pane }} - {{/gh-tabs-manager}} -
    diff --git a/core/client/app/templates/settings/tags/tag.hbs b/core/client/app/templates/settings/tags/tag.hbs index 78655f3a5c..67331d6d97 100644 --- a/core/client/app/templates/settings/tags/tag.hbs +++ b/core/client/app/templates/settings/tags/tag.hbs @@ -1 +1,12 @@ -{{gh-tag-settings-form tag=tag setProperty=(action "setProperty") openModal="openModal"}} +{{gh-tag-settings-form tag=tag + setProperty=(action "setProperty") + showDeleteTagModal=(action "toggleDeleteTagModal") + isMobile=isMobile}} + +{{#if showDeleteTagModal}} + {{gh-fullscreen-modal "delete-tag" + model=tag + confirm=(action "deleteTag") + close=(action "toggleDeleteTagModal") + modifier="action wide"}} +{{/if}} diff --git a/core/client/app/templates/team/index.hbs b/core/client/app/templates/team/index.hbs index bd517d4e59..cd8e3f697a 100644 --- a/core/client/app/templates/team/index.hbs +++ b/core/client/app/templates/team/index.hbs @@ -4,8 +4,14 @@ {{!-- Do not show Invite user button to authors --}} {{#unless session.user.isAuthor}}
    - +
    + + {{#if showInviteUserModal}} + {{gh-fullscreen-modal "invite-new-user" + close=(action "toggleInviteUserModal") + modifier="action"}} + {{/if}} {{/unless}}
    @@ -26,15 +32,15 @@ ic
    {{user.email}}
    - {{#if user.pending}} - - Invitation not sent - please try again - - {{else}} - - Invitation sent: {{component.createdAt}} - - {{/if}} + {{#if user.pending}} + + Invitation not sent - please try again + + {{else}} + + Invitation sent: {{component.createdAt}} + + {{/if}}