mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Merge pull request #6088 from kevinansfield/liquid-tether-modals
Modals refactor
This commit is contained in:
commit
c30f1d5ed7
89 changed files with 1269 additions and 1078 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
76
core/client/app/components/gh-fullscreen-modal.js
Normal file
76
core/client/app/components/gh-fullscreen-modal.js
Normal file
|
@ -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;
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -21,8 +21,8 @@ export default Component.extend({
|
|||
this.sendAction('toggleMaximise');
|
||||
},
|
||||
|
||||
openModal(modal) {
|
||||
this.sendAction('openModal', modal);
|
||||
showMarkdownHelp() {
|
||||
this.sendAction('showMarkdownHelp');
|
||||
},
|
||||
|
||||
closeMobileMenu() {
|
||||
|
|
|
@ -125,7 +125,7 @@ export default Component.extend({
|
|||
},
|
||||
|
||||
deleteTag() {
|
||||
this.sendAction('openModal', 'delete-tag', this.get('tag'));
|
||||
this.attrs.showDeleteTagModal();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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'});
|
||||
});
|
||||
|
|
45
core/client/app/components/modals/base.js
Normal file
45
core/client/app/components/modals/base.js
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
9
core/client/app/components/modals/copy-html.js
Normal file
9
core/client/app/components/modals/copy-html.js
Normal file
|
@ -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')
|
||||
});
|
48
core/client/app/components/modals/delete-all.js
Normal file
48
core/client/app/components/modals/delete-all.js
Normal file
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
51
core/client/app/components/modals/delete-post.js
Normal file
51
core/client/app/components/modals/delete-post.js
Normal file
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
26
core/client/app/components/modals/delete-tag.js
Normal file
26
core/client/app/components/modals/delete-tag.js
Normal file
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
18
core/client/app/components/modals/delete-user.js
Normal file
18
core/client/app/components/modals/delete-user.js
Normal file
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
121
core/client/app/components/modals/invite-new-user.js
Normal file
121
core/client/app/components/modals/invite-new-user.js
Normal file
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
11
core/client/app/components/modals/leave-editor.js
Normal file
11
core/client/app/components/modals/leave-editor.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import ModalComponent from 'ghost/components/modals/base';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
actions: {
|
||||
confirm() {
|
||||
this.attrs.confirm().finally(() => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
4
core/client/app/components/modals/markdown-help.js
Normal file
4
core/client/app/components/modals/markdown-help.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import ModalComponent from 'ghost/components/modals/base';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
});
|
64
core/client/app/components/modals/re-authenticate.js
Normal file
64
core/client/app/components/modals/re-authenticate.js
Normal file
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
17
core/client/app/components/modals/transfer-owner.js
Normal file
17
core/client/app/components/modals/transfer-owner.js
Normal file
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
86
core/client/app/components/modals/upload-image.js
Normal file
86
core/client/app/components/modals/upload-image.js
Normal file
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -10,6 +10,7 @@ export default Controller.extend({
|
|||
topNotificationCount: 0,
|
||||
showMobileMenu: false,
|
||||
showSettingsMenu: false,
|
||||
showMarkdownHelpModal: false,
|
||||
|
||||
autoNav: false,
|
||||
autoNavOpen: computed('autoNav', {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {Controller, computed} = Ember;
|
||||
const {alias} = computed;
|
||||
|
||||
export default Controller.extend({
|
||||
generatedHTML: alias('model.generatedHTML')
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -113,7 +113,7 @@ let shortcuts = {
|
|||
}
|
||||
|
||||
// Talk to the editor
|
||||
editor.sendAction('openModal', 'copy-html', {generatedHTML});
|
||||
editor.send('toggleCopyHTMLModal', generatedHTML);
|
||||
},
|
||||
|
||||
currentDate(replacement) {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ export default AuthenticatedRoute.extend(base, NotFoundHandler, {
|
|||
|
||||
actions: {
|
||||
authorizationFailed() {
|
||||
this.send('openModal', 'signin');
|
||||
this.get('controller').send('toggleReAuthenticateModal');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -68,7 +68,7 @@ export default AuthenticatedRoute.extend(ShortcutsRoute, {
|
|||
},
|
||||
|
||||
deletePost() {
|
||||
this.send('openModal', 'delete-post', this.get('controller.model'));
|
||||
this.controllerFor('posts').send('toggleDeletePostModal');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<div class="gh-viewport {{if autoNav 'gh-autonav'}} {{if showSettingsMenu 'settings-menu-expanded'}} {{if showMobileMenu 'mobile-menu-expanded'}}">
|
||||
{{#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"}}
|
||||
</div>{{!gh-viewport}}
|
||||
{{/gh-app}}
|
||||
|
||||
{{#if showMarkdownHelpModal}}
|
||||
{{gh-fullscreen-modal "markdown-help"
|
||||
close=(route-action "toggleMarkdownHelpModal")
|
||||
modifier="wide"}}
|
||||
{{/if}}
|
||||
|
|
|
@ -1 +1,8 @@
|
|||
{{yield this}}
|
||||
{{yield this (action 'toggleCopyHTMLModal')}}
|
||||
|
||||
{{#if showCopyHTMLModal}}
|
||||
{{gh-fullscreen-modal "copy-html"
|
||||
model=copyHTMLModalContent
|
||||
close=(action "toggleCopyHTMLModal")
|
||||
modifier="action"}}
|
||||
{{/if}}
|
||||
|
|
11
core/client/app/templates/components/gh-fullscreen-modal.hbs
Normal file
11
core/client/app/templates/components/gh-fullscreen-modal.hbs
Normal file
|
@ -0,0 +1,11 @@
|
|||
<div class="liquid-tether-overlay {{overlayClass}} {{if on-overlay-click 'clickable'}}" {{action 'clickOverlay'}}></div>
|
||||
<div class="liquid-tether {{tetherClass}}">
|
||||
{{#if hasBlock}}
|
||||
{{yield}}
|
||||
{{else}}
|
||||
{{component modalPath
|
||||
model=model
|
||||
confirm=(action 'confirm')
|
||||
closeModal=(action 'close')}}
|
||||
{{/if}}
|
||||
</div>
|
|
@ -53,7 +53,7 @@
|
|||
<li role="presentation"><a class="dropdown-item help-menu-tweet" role="menuitem" tabindex="-1" href="https://twitter.com/intent/tweet?text=%40TryGhost+Hi%21+Can+you+help+me+with+&related=TryGhost" target="_blank" onclick="window.open(this.href, 'twitter-share', 'width=550,height=235');return false;"><i class="icon-twitter"></i> Tweet @TryGhost!</a></li>
|
||||
<li class="divider"></li>
|
||||
<li role="presentation"><a class="dropdown-item help-menu-how-to" role="menuitem" tabindex="-1" href="http://support.ghost.org/how-to-use-ghost/" target="_blank"><i class="icon-book"></i> How to Use Ghost</a></li>
|
||||
<li role="presentation"><a class="dropdown-item help-menu-markdown" role="menuitem" tabindex="-1" href="" {{action "openModal" "markdown"}}><i class="icon-markdown"></i> Markdown Help</a></li>
|
||||
<li role="presentation"><a class="dropdown-item help-menu-markdown" role="menuitem" tabindex="-1" href="" {{action "showMarkdownHelp"}}><i class="icon-markdown"></i> Markdown Help</a></li>
|
||||
<li class="divider"></li>
|
||||
<li role="presentation"><a class="dropdown-item help-menu-wishlist" role="menuitem" tabindex="-1" href="http://ideas.ghost.org/" target="_blank"><i class="icon-idea"></i> Wishlist</a></li>
|
||||
</ul>
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<header class="modal-header">
|
||||
<h1>Generated HTML</h1>
|
||||
</header>
|
||||
<a class="close icon-x" href="" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body">
|
||||
{{textarea value=generatedHtml rows="6"}}
|
||||
</div>
|
13
core/client/app/templates/components/modals/delete-all.hbs
Normal file
13
core/client/app/templates/components/modals/delete-all.hbs
Normal file
|
@ -0,0 +1,13 @@
|
|||
<header class="modal-header">
|
||||
<h1>Would you really like to delete all content from your blog?</h1>
|
||||
</header>
|
||||
<a class="close icon-x" href="" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>This is permanent! No backups, no restores, no magic undo button. <br /> We warned you, ok?</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button {{action "closeModal"}} class="btn btn-default btn-minor">Cancel</button>
|
||||
{{#gh-spin-button action="confirm" class="btn btn-red" submitting=submitting}}Delete{{/gh-spin-button}}
|
||||
</div>
|
17
core/client/app/templates/components/modals/delete-post.hbs
Normal file
17
core/client/app/templates/components/modals/delete-post.hbs
Normal file
|
@ -0,0 +1,17 @@
|
|||
<header class="modal-header">
|
||||
<h1>Are you sure you want to delete this post?</h1>
|
||||
</header>
|
||||
<a class="close icon-x" href="" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
You're about to delete "<strong>{{post.title}}</strong>".<br />
|
||||
This is permanent! No backups, no restores, no magic undo button.<br />
|
||||
We warned you, ok?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button {{action "closeModal"}} class="btn btn-default btn-minor">Cancel</button>
|
||||
{{#gh-spin-button action="confirm" class="btn btn-red" submitting=submitting}}Delete{{/gh-spin-button}}
|
||||
</div>
|
17
core/client/app/templates/components/modals/delete-tag.hbs
Normal file
17
core/client/app/templates/components/modals/delete-tag.hbs
Normal file
|
@ -0,0 +1,17 @@
|
|||
<header class="modal-header">
|
||||
<h1>Are you sure you want to delete this tag?</h1>
|
||||
</header>
|
||||
<a class="close icon-x" href="" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body">
|
||||
<strong>WARNING:</strong>
|
||||
{{#if tag.post_count}}
|
||||
<span class="red">This tag is attached to {{tag.count.posts}} {{postInflection}}.</span>
|
||||
{{/if}}
|
||||
You're about to delete "<strong>{{tag.name}}</strong>". This is permanent! No backups, no restores, no magic undo button. We warned you, ok?
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button {{action "closeModal"}} class="btn btn-default btn-minor">Cancel</button>
|
||||
{{#gh-spin-button action="confirm" class="btn btn-red" submitting=submitting}}Delete{{/gh-spin-button}}
|
||||
</div>
|
17
core/client/app/templates/components/modals/delete-user.hbs
Normal file
17
core/client/app/templates/components/modals/delete-user.hbs
Normal file
|
@ -0,0 +1,17 @@
|
|||
<header class="modal-header">
|
||||
<h1>Are you sure you want to delete this user?</h1>
|
||||
</header>
|
||||
<a class="close icon-x" href="" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body">
|
||||
{{#if user.count.posts}}
|
||||
<strong>WARNING:</strong> <span class="red">This user is the author of {{pluralize user.count.posts 'post'}}.</span> All posts and user data will be deleted. There is no way to recover this.
|
||||
{{else}}
|
||||
<strong>WARNING:</strong> All user data will be deleted. There is no way to recover this.
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button {{action "closeModal"}} class="btn btn-default btn-minor">Cancel</button>
|
||||
{{#gh-spin-button action="confirm" class="btn btn-red" submitting=submitting}}Delete{{/gh-spin-button}}
|
||||
</div>
|
|
@ -0,0 +1,41 @@
|
|||
<header class="modal-header">
|
||||
<h1>Invite a New User</h1>
|
||||
</header>
|
||||
<a class="close icon-x" href="" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
{{#gh-form-group errors=errors hasValidated=hasValidated property="email"}}
|
||||
<label for="new-user-email">Email Address</label>
|
||||
{{gh-input enter="sendInvite"
|
||||
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}}
|
||||
|
||||
<div class="form-group for-select">
|
||||
<label for="new-user-role">Role</label>
|
||||
<span class="gh-select" tabindex="0">
|
||||
{{gh-select-native id="new-user-role"
|
||||
content=roles
|
||||
optionValuePath="id"
|
||||
optionLabelPath="name"
|
||||
selection=role
|
||||
action="setRole"
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
{{#gh-spin-button action="confirm" class="btn btn-green" submitting=submitting}}Send invitation now{{/gh-spin-button}}
|
||||
</div>
|
18
core/client/app/templates/components/modals/leave-editor.hbs
Normal file
18
core/client/app/templates/components/modals/leave-editor.hbs
Normal file
|
@ -0,0 +1,18 @@
|
|||
<header class="modal-header">
|
||||
<h1>Are you sure you want to leave this page?</h1>
|
||||
</header>
|
||||
<a class="close icon-x" href="" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Hey there! It looks like you're in the middle of writing something and
|
||||
you haven't saved all of your content.
|
||||
</p>
|
||||
|
||||
<p>Save before you go!</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button {{action "closeModal"}} class="btn btn-default btn-minor">Stay</button>
|
||||
<button {{action "confirm"}} class="btn btn-red">Leave</button>
|
||||
</div>
|
|
@ -1,5 +1,9 @@
|
|||
{{#gh-modal-dialog action="closeModal" showClose=true style="wide"
|
||||
title="Markdown Help"}}
|
||||
<header class="modal-header">
|
||||
<h1>Markdown Help</h1>
|
||||
</header>
|
||||
<a class="close icon-x" href="" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body">
|
||||
<section class="markdown-help-container">
|
||||
<table class="modal-markdown-help-table">
|
||||
<thead>
|
||||
|
@ -74,4 +78,4 @@
|
|||
</table>
|
||||
For further Markdown syntax reference: <a href="http://support.ghost.org/markdown-guide/" target="_blank">Markdown Documentation</a>
|
||||
</section>
|
||||
{{/gh-modal-dialog}}
|
||||
</div>
|
|
@ -0,0 +1,16 @@
|
|||
<header class="modal-header">
|
||||
<h1>Please re-authenticate</h1>
|
||||
</header>
|
||||
<a class="close icon-x" href="" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body {{if authenticationError 'error'}}">
|
||||
<form id="login" class="login-form" method="post" novalidate="novalidate" {{action "confirm" on="submit"}}>
|
||||
{{#gh-validation-status-container class="password-wrap" errors=errors property="password" hasValidated=hasValidated}}
|
||||
{{input class="gh-input password" type="password" placeholder="Password" name="password" value=password}}
|
||||
{{/gh-validation-status-container}}
|
||||
{{#gh-spin-button class="btn btn-blue" type="submit" submitting=submitting}}Log in{{/gh-spin-button}}
|
||||
</form>
|
||||
{{#if authenticationError}}
|
||||
<p class="response">{{authenticationError}}</p>
|
||||
{{/if}}
|
||||
</div>
|
|
@ -0,0 +1,16 @@
|
|||
<header class="modal-header">
|
||||
<h1>Transfer Ownership</h1>
|
||||
</header>
|
||||
<a class="close icon-x" href="" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Are you sure you want to transfer the ownership of this blog?
|
||||
You will not be able to undo this action.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button {{action "closeModal"}} class="btn btn-default btn-minor">Cancel</button>
|
||||
{{#gh-spin-button action="confirm" class="btn btn-red" submitting=submitting}}Yep - I'm sure{{/gh-spin-button}}
|
||||
</div>
|
11
core/client/app/templates/components/modals/upload-image.hbs
Normal file
11
core/client/app/templates/components/modals/upload-image.hbs
Normal file
|
@ -0,0 +1,11 @@
|
|||
<div class="modal-body">
|
||||
<section class="js-drop-zone">
|
||||
<img class="js-upload-target" src="{{imageUrl}}" alt="logo">
|
||||
<input data-url="upload" class="js-fileupload main" type="file" name="uploadimage" accept="{{acceptEncoding}}">
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button {{action "closeModal"}} class="btn btn-default btn-minor">Cancel</button>
|
||||
{{#gh-spin-button action="confirm" class="btn btn-blue right js-button-accept" submitting=submitting}}Save{{/gh-spin-button}}
|
||||
</div>
|
|
@ -1,4 +1,4 @@
|
|||
{{#gh-editor editorScrollInfo=editorScrollInfo as |ghEditor|}}
|
||||
{{#gh-editor editorScrollInfo=editorScrollInfo as |ghEditor toggleCopyHTMLModal|}}
|
||||
<header class="view-header">
|
||||
{{#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
|
||||
}}
|
||||
</section>
|
||||
|
@ -23,22 +23,30 @@
|
|||
<section class="view-container view-editor">
|
||||
<section class="entry-markdown js-entry-markdown {{if ghEditor.markdownActive 'active'}}">
|
||||
<header class="floatingheader">
|
||||
<span class="desktop-tabs"><a class="markdown-help-label" href="" title="Markdown Help" {{action "openModal" "markdown"}}>Markdown</a></span>
|
||||
<span class="desktop-tabs"><a class="markdown-help-label" href="" title="Markdown Help" {{action "toggleMarkdownHelpModal"}}>Markdown</a></span>
|
||||
<span class="mobile-tabs">
|
||||
<a href="#" {{action 'selectTab' 'markdown' target=ghEditor}} class="{{if ghEditor.markdownActive 'active'}}">Markdown</a>
|
||||
<a href="#" {{action 'selectTab' 'preview' target=ghEditor}} class="{{if ghEditor.previewActive 'active'}}">Preview</a>
|
||||
</span>
|
||||
<a class="markdown-help-icon" href="" title="Markdown Help" {{action "openModal" "markdown"}}><i class="icon-markdown"></i></a>
|
||||
<a class="markdown-help-icon" href="" title="Markdown Help" {{action "toggleMarkdownHelpModal"}}><i class="icon-markdown"></i></a>
|
||||
</header>
|
||||
<section id="entry-markdown-content" class="entry-markdown-content">
|
||||
{{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}}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="entry-preview js-entry-preview {{if ghEditor.previewActive 'active'}}">
|
||||
<header class="floatingheader">
|
||||
<span class="desktop-tabs"><a target="_blank" href="{{model.previewUrl}}">
|
||||
Preview</a></span>
|
||||
<span class="desktop-tabs"><a target="_blank" href="{{model.previewUrl}}">Preview</a></span>
|
||||
<span class="mobile-tabs">
|
||||
<a href="#" {{action 'selectTab' 'markdown' target=ghEditor}} class="{{if ghEditor.markdownActive 'active'}}">Markdown</a>
|
||||
<a href="#" {{action 'selectTab' 'preview' target=ghEditor}} class="{{if ghEditor.previewActive 'active'}}">Preview</a>
|
||||
|
@ -53,3 +61,23 @@
|
|||
</section>
|
||||
</section>
|
||||
{{/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}}
|
||||
|
|
|
@ -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}}
|
|
@ -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}}
|
||||
|
||||
<p>This is permanent! No backups, no restores, no magic undo button. <br /> We warned you, ok?</p>
|
||||
|
||||
{{/gh-modal-dialog}}
|
|
@ -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}}
|
||||
|
||||
<p>You're about to delete "<strong>{{model.title}}</strong>".<br />This is permanent! No backups, no restores, no magic undo button. <br /> We warned you, ok?</p>
|
||||
|
||||
{{/gh-modal-dialog}}
|
|
@ -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}}
|
||||
<strong>WARNING:</strong> <span class="red">This tag is attached to {{model.count.posts}} {{postInflection}}.</span> You're about to delete "<strong>{{model.name}}</strong>". This is permanent! No backups, no restores, no magic undo button. We warned you, ok?
|
||||
{{else}}
|
||||
<strong>WARNING:</strong> You're about to delete "<strong>{{model.name}}</strong>". This is permanent! No backups, no restores, no magic undo button. We warned you, ok?
|
||||
{{/if}}
|
||||
{{/gh-modal-dialog}}
|
|
@ -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}}
|
||||
<strong>WARNING:</strong> <span class="red">This user is the author of {{userPostCount.count}} {{userPostCount.inflection}}.</span> All posts and user data will be deleted. There is no way to recover this.
|
||||
{{else}}
|
||||
<strong>WARNING:</strong> All user data will be deleted. There is no way to recover this.
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
|
||||
{{/gh-modal-dialog}}
|
|
@ -1,26 +0,0 @@
|
|||
{{#gh-modal-dialog action="closeModal" showClose=true type="action"
|
||||
title="Invite a New User" confirm=confirm class="invite-new-user"}}
|
||||
|
||||
<fieldset>
|
||||
{{#gh-form-group errors=errors hasValidated=hasValidated property="email"}}
|
||||
<label for="new-user-email">Email Address</label>
|
||||
{{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}}
|
||||
|
||||
<div class="form-group for-select">
|
||||
<label for="new-user-role">Role</label>
|
||||
<span class="gh-select" tabindex="0">
|
||||
{{gh-select-native id="new-user-role"
|
||||
content=roles
|
||||
optionValuePath="id"
|
||||
optionLabelPath="name"
|
||||
selection=role
|
||||
action="setRole"
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{{/gh-modal-dialog}}
|
|
@ -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}}
|
||||
|
||||
<p>Hey there! It looks like you're in the middle of writing something and you haven't saved all of your
|
||||
content.</p>
|
||||
|
||||
<p>Save before you go!</p>
|
||||
|
||||
{{/gh-modal-dialog}}
|
|
@ -1,11 +0,0 @@
|
|||
{{#gh-modal-dialog action="closeModal" showClose=true type="action" style="wide" animation="fade"
|
||||
title="Please re-authenticate" confirm=confirm}}
|
||||
|
||||
<form id="login" class="login-form" method="post" novalidate="novalidate" {{action "validateAndAuthenticate" on="submit"}}>
|
||||
<div class="password-wrap">
|
||||
{{input class="gh-input password" type="password" placeholder="Password" name="password" value=password}}
|
||||
</div>
|
||||
{{#gh-spin-button class="btn btn-blue" type="submit" action="validateAndAuthenticate" submitting=submitting}}Log in{{/gh-spin-button}}
|
||||
</form>
|
||||
|
||||
{{/gh-modal-dialog}}
|
|
@ -1,6 +0,0 @@
|
|||
{{#gh-modal-dialog action="closeModal" showClose=true type="action" style="wide"
|
||||
title="Transfer Ownership" confirm=confirm}}
|
||||
|
||||
<p>Are you sure you want to transfer the ownership of this blog? You will not be able to undo this action.</p>
|
||||
|
||||
{{/gh-modal-dialog}}
|
|
@ -1,7 +0,0 @@
|
|||
{{#gh-upload-modal action="closeModal" close=true type="action" style="wide" model=model imageType=imageType}}
|
||||
<section class="js-drop-zone">
|
||||
<img class="js-upload-target" src="{{src}}" alt="logo">
|
||||
<input data-url="upload" class="js-fileupload main" type="file" name="uploadimage" accept="{{acceptEncoding}}" >
|
||||
</section>
|
||||
|
||||
{{/gh-upload-modal}}
|
|
@ -44,3 +44,10 @@
|
|||
</section>
|
||||
</div>
|
||||
{{/gh-content-view-container}}
|
||||
|
||||
{{#if showDeletePostModal}}
|
||||
{{gh-fullscreen-modal "delete-post"
|
||||
model=currentPost
|
||||
close=(action "toggleDeletePostModal")
|
||||
modifier="action wide"}}
|
||||
{{/if}}
|
||||
|
|
|
@ -31,21 +31,35 @@
|
|||
<div class="form-group">
|
||||
<label>Blog Logo</label>
|
||||
{{#if model.logo}}
|
||||
<img class="blog-logo" src="{{model.logo}}" alt="logo" role="button" {{action "openModal" "upload" this "logo"}}>
|
||||
<img class="blog-logo" src="{{model.logo}}" alt="logo" role="button" {{action "toggleUploadLogoModal"}}>
|
||||
{{else}}
|
||||
<button type="button" class="btn btn-green js-modal-logo" {{action "openModal" "upload" this "logo"}}>Upload Image</button>
|
||||
<button type="button" class="btn btn-green js-modal-logo" {{action "toggleUploadLogoModal"}}>Upload Image</button>
|
||||
{{/if}}
|
||||
<p>Display a sexy logo for your publication</p>
|
||||
|
||||
{{#if showUploadLogoModal}}
|
||||
{{gh-fullscreen-modal "upload-image"
|
||||
model=(hash model=model imageProperty="logo")
|
||||
close=(action "toggleUploadLogoModal")
|
||||
modifier="action wide"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Blog Cover</label>
|
||||
{{#if model.cover}}
|
||||
<img class="blog-cover" src="{{model.cover}}" alt="cover photo" role="button" {{action "openModal" "upload" this "cover"}}>
|
||||
<img class="blog-cover" src="{{model.cover}}" alt="cover photo" role="button" {{action "toggleUploadCoverModal"}}>
|
||||
{{else}}
|
||||
<button type="button" class="btn btn-green js-modal-cover" {{action "openModal" "upload" this "cover"}}>Upload Image</button>
|
||||
<button type="button" class="btn btn-green js-modal-cover" {{action "toggleUploadCoverModal"}}>Upload Image</button>
|
||||
{{/if}}
|
||||
<p>Display a cover image on your site</p>
|
||||
|
||||
{{#if showUploadCoverModal}}
|
||||
{{gh-fullscreen-modal "upload-image"
|
||||
model=(hash model=model imageProperty="cover")
|
||||
close=(action "toggleUploadCoverModal")
|
||||
modifier="action wide"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
|
@ -93,11 +107,11 @@
|
|||
</div>
|
||||
|
||||
{{#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"}}
|
||||
<p>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.</p>
|
||||
{{/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"}}
|
||||
<p>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.</p>
|
||||
{{/gh-form-group}}
|
||||
{{/if}}
|
||||
</fieldset>
|
||||
</form>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label>Delete all Content</label>
|
||||
<button type="button" class="btn btn-red js-delete" {{action "openModal" "deleteAll"}}>Delete</button>
|
||||
<button type="button" class="btn btn-red js-delete" {{action "toggleDeleteAllModal"}}>Delete</button>
|
||||
<p>Delete all posts and tags from the database.</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
@ -57,3 +57,9 @@
|
|||
</form>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{{#if showDeleteAllModal}}
|
||||
{{gh-fullscreen-modal "delete-all"
|
||||
close=(action "toggleDeleteAllModal")
|
||||
modifier="action wide"}}
|
||||
{{/if}}
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
<div>
|
||||
{{#gh-tabs-manager selected="showSubview" class="settings-menu-container"}}
|
||||
<div class="{{if isViewingSubview 'settings-menu-pane-out-left' 'settings-menu-pane-in'}} settings-menu settings-menu-pane tag-settings-pane">
|
||||
<div class="settings-menu-header">
|
||||
<h4>Tag Settings</h4>
|
||||
<button class="close icon-x settings-menu-header-action" {{action "closeMenus"}}>
|
||||
<span class="hidden">Close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="settings-menu-content">
|
||||
{{gh-uploader uploaded="setCoverImage" canceled="clearCoverImage" description="Add tag image" image=activeTag.image initUploader="setUploaderReference" tagName="section"}}
|
||||
<form>
|
||||
{{#gh-form-group errors=activeTag.errors hasValidated=activeTag.hasValidated property="name"}}
|
||||
<label for="tag-name">Name</label>
|
||||
{{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"}}
|
||||
<label for="tag-url">URL</label>
|
||||
{{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"}}
|
||||
<label for="tag-description">Description</label>
|
||||
{{gh-textarea id="tag-description" name="description" value=activeTagDescriptionScratch focus-out="saveActiveTagDescription"}}
|
||||
<p>Maximum: <b>200</b> characters. You’ve used {{gh-count-down-characters activeTagDescriptionScratch 200}}</p>
|
||||
{{/gh-form-group}}
|
||||
|
||||
<ul class="nav-list nav-list-block">
|
||||
{{#gh-tab tagName="li" classNames="nav-list-item"}}
|
||||
<button type="button" class="meta-data-button">
|
||||
<b>Meta Data</b>
|
||||
<span>Extra content for SEO and social media.</span>
|
||||
</button>
|
||||
<i class="icon-arrow-right"></i>
|
||||
{{/gh-tab}}
|
||||
</ul>
|
||||
|
||||
{{#unless activeTag.isNew}}
|
||||
<button type="button" class="btn btn-link btn-sm tag-delete-button" {{action "openModal" "delete-tag" activeTag}}><i class="icon-trash"></i> Delete Tag</button>
|
||||
{{/unless}}
|
||||
</form>
|
||||
</div>
|
||||
</div>{{! .settings-menu-pane }}
|
||||
|
||||
<div class="{{if isViewingSubview 'settings-menu-pane-in' 'settings-menu-pane-out-right'}} settings-menu settings-menu-pane tag-meta-settings-pane">
|
||||
{{#gh-tab-pane}}
|
||||
{{#if isViewingSubview}}
|
||||
<div class="settings-menu-header subview">
|
||||
<button {{action "closeSubview"}} class="back icon-arrow-left settings-menu-header-action"><span class="hidden">Back</span></button>
|
||||
<h4>Meta Data</h4>
|
||||
<div style="width:23px;">{{!flexbox space-between}}</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-menu-content">
|
||||
<form>
|
||||
{{#gh-form-group errors=activeTag.errors hasValidated=activeTag.hasValidated property="meta_title"}}
|
||||
<label for="meta-title">Meta Title</label>
|
||||
{{gh-input id="meta-title" name="meta_title" type="text" value=activeTagMetaTitleScratch focus-out="saveActiveTagMetaTitle"}}
|
||||
{{gh-error-message errors=activeTag.errors property="meta_title"}}
|
||||
<p>Recommended: <b>70</b> characters. You’ve used {{gh-count-down-characters activeTagMetaTitleScratch 70}}</p>
|
||||
{{/gh-form-group}}
|
||||
|
||||
{{#gh-form-group errors=activeTag.errors hasValidated=activeTag.hasValidated property="meta_description"}}
|
||||
<label for="meta-description">Meta Description</label>
|
||||
{{gh-textarea id="meta-description" name="meta_description" value=activeTagMetaDescriptionScratch focus-out="saveActiveTagMetaDescription"}}
|
||||
{{gh-error-message errors=activeTag.errors property="meta_description"}}
|
||||
<p>Recommended: <b>156</b> characters. You’ve used {{gh-count-down-characters activeTagMetaDescriptionScratch 156}}</p>
|
||||
{{/gh-form-group}}
|
||||
|
||||
<div class="form-group">
|
||||
<label>Search Engine Result Preview</label>
|
||||
<div class="seo-preview">
|
||||
<div class="seo-preview-title">{{seoTitle}}</div>
|
||||
<div class="seo-preview-link">{{seoURL}}</div>
|
||||
<div class="seo-preview-description">{{seoDescription}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>{{! .settings-menu-content }}
|
||||
{{/if}}
|
||||
{{/gh-tab-pane}}
|
||||
</div>{{! .settings-menu-pane }}
|
||||
{{/gh-tabs-manager}}
|
||||
</div>
|
|
@ -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}}
|
||||
|
|
|
@ -4,8 +4,14 @@
|
|||
{{!-- Do not show Invite user button to authors --}}
|
||||
{{#unless session.user.isAuthor}}
|
||||
<section class="view-actions">
|
||||
<button class="btn btn-green" {{action "openModal" "invite-new-user"}} >Invite People</button>
|
||||
<button class="btn btn-green" {{action "toggleInviteUserModal"}} >Invite People</button>
|
||||
</section>
|
||||
|
||||
{{#if showInviteUserModal}}
|
||||
{{gh-fullscreen-modal "invite-new-user"
|
||||
close=(action "toggleInviteUserModal")
|
||||
modifier="action"}}
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
</header>
|
||||
|
||||
|
@ -26,15 +32,15 @@
|
|||
<span class="user-list-item-icon icon-mail">ic</span>
|
||||
<div class="user-list-item-body">
|
||||
<span class="name">{{user.email}}</span><br>
|
||||
{{#if user.pending}}
|
||||
<span class="description-error">
|
||||
Invitation not sent - please try again
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="description">
|
||||
Invitation sent: {{component.createdAt}}
|
||||
</span>
|
||||
{{/if}}
|
||||
{{#if user.pending}}
|
||||
<span class="description-error">
|
||||
Invitation not sent - please try again
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="description">
|
||||
Invitation sent: {{component.createdAt}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<aside class="user-list-item-aside">
|
||||
{{#if component.isSending}}
|
||||
|
@ -61,7 +67,7 @@
|
|||
{{!-- For authors only shows users as a list, otherwise show users with links to user page --}}
|
||||
{{#unless session.user.isAuthor}}
|
||||
{{#gh-user-active user=user as |component|}}
|
||||
{{#link-to 'team.user' user class="user-list-item"}}
|
||||
{{#link-to 'team.user' user.slug class="user-list-item"}}
|
||||
{{partial 'user-list-item'}}
|
||||
{{/link-to}}
|
||||
{{/gh-user-active}}
|
||||
|
|
|
@ -14,16 +14,28 @@
|
|||
{{#gh-dropdown name="user-actions-menu" tagName="ul" classNames="user-actions-menu dropdown-menu dropdown-triangle-top-right"}}
|
||||
{{#if canMakeOwner}}
|
||||
<li>
|
||||
<button {{action "openModal" "transfer-owner" this}}>
|
||||
<button {{action "toggleTransferOwnerModal"}}>
|
||||
Make Owner
|
||||
</button>
|
||||
{{#if showTransferOwnerModal}}
|
||||
{{gh-fullscreen-modal "transfer-owner"
|
||||
confirm=(action "transferOwnership")
|
||||
close=(action "toggleTransferOwnerModal")
|
||||
modifier="action wide"}}
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if deleteUserActionIsVisible}}
|
||||
<li>
|
||||
<button {{action "openModal" "delete-user" this}} class="delete">
|
||||
<button {{action "toggleDeleteUserModal"}} class="delete">
|
||||
Delete User
|
||||
</button>
|
||||
{{#if showDeleteUserModal}}
|
||||
{{gh-fullscreen-modal "delete-user"
|
||||
confirm=(action "deleteUser")
|
||||
close=(action "toggleDeleteUserModal")
|
||||
modifier="action wide"}}
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/gh-dropdown}}
|
||||
|
@ -37,7 +49,13 @@
|
|||
<div class="view-container settings-user">
|
||||
|
||||
<figure class="user-cover" style={{coverImageBackground}}>
|
||||
<button class="btn btn-default user-cover-edit js-modal-cover" {{action "openModal" "upload" user "cover"}}>Change Cover</button>
|
||||
<button class="btn btn-default user-cover-edit" {{action "toggleUploadCoverModal"}}>Change Cover</button>
|
||||
{{#if showUploadCoverModal}}
|
||||
{{gh-fullscreen-modal "upload-image"
|
||||
model=(hash model=user imageProperty="cover")
|
||||
close=(action "toggleUploadCoverModal")
|
||||
modifier="action wide"}}
|
||||
{{/if}}
|
||||
</figure>
|
||||
|
||||
<form class="user-profile" novalidate="novalidate" autocomplete="off">
|
||||
|
@ -50,7 +68,13 @@
|
|||
|
||||
<figure class="user-image">
|
||||
<div id="user-image" class="img" style={{userImageBackground}}><span class="hidden">{{user.name}}"s Picture</span></div>
|
||||
<button type="button" {{action "openModal" "upload" user "image"}} class="edit-user-image js-modal-image">Edit Picture</button>
|
||||
<button type="button" {{action "toggleUploadImageModal"}} class="edit-user-image">Edit Picture</button>
|
||||
{{#if showUploadImageModal}}
|
||||
{{gh-fullscreen-modal "upload-image"
|
||||
model=(hash model=user imageProperty="image")
|
||||
close=(action "toggleUploadImageModal")
|
||||
modifier="action wide"}}
|
||||
{{/if}}
|
||||
</figure>
|
||||
|
||||
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="name" class="first-form-group"}}
|
||||
|
|
11
core/client/app/transitions.js
Normal file
11
core/client/app/transitions.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { target } from 'liquid-tether';
|
||||
|
||||
export default function () {
|
||||
this.transition(
|
||||
target('fullscreen-modal'),
|
||||
this.toValue(({isVisible}) => isVisible),
|
||||
// this.use('tether', [modal options], [background options])
|
||||
this.use('tether', ['fade', {duration: 150}], ['fade', {duration: 150}]),
|
||||
this.reverse('tether', ['fade', {duration: 80}], ['fade', {duration: 150}])
|
||||
);
|
||||
}
|
17
core/client/app/validators/invite-user.js
Normal file
17
core/client/app/validators/invite-user.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import BaseValidator from './base';
|
||||
|
||||
export default BaseValidator.create({
|
||||
properties: ['email'],
|
||||
|
||||
email(model) {
|
||||
let email = model.get('email');
|
||||
|
||||
if (validator.empty(email)) {
|
||||
model.get('errors').add('email', 'Please enter an email.');
|
||||
this.invalidate();
|
||||
} else if (!validator.isEmail(email)) {
|
||||
model.get('errors').add('email', 'Invalid Email.');
|
||||
this.invalidate();
|
||||
}
|
||||
}
|
||||
});
|
|
@ -41,14 +41,18 @@
|
|||
"ember-data-filter": "1.13.0",
|
||||
"ember-disable-proxy-controllers": "1.0.1",
|
||||
"ember-export-application-global": "1.0.5",
|
||||
"ember-hash-helper-polyfill": "0.1.0",
|
||||
"ember-myth": "0.1.1",
|
||||
"ember-resolver": "2.0.3",
|
||||
"ember-route-action-helper": "0.2.0",
|
||||
"ember-simple-auth": "1.0.0",
|
||||
"ember-sinon": "0.3.0",
|
||||
"ember-suave": "1.2.3",
|
||||
"ember-watson": "0.7.0",
|
||||
"fs-extra": "0.16.3",
|
||||
"glob": "^4.0.5",
|
||||
"liquid-fire": "0.22",
|
||||
"liquid-tether": "0.1.9",
|
||||
"walk-sync": "^0.1.3"
|
||||
},
|
||||
"ember-addon": {
|
||||
|
|
|
@ -115,7 +115,7 @@ describe('Acceptance: Authentication', function () {
|
|||
|
||||
andThen(() => {
|
||||
// we should see a re-auth modal
|
||||
expect(find('.modal-container #login').length, 'modal exists').to.equal(1);
|
||||
expect(find('.fullscreen-modal #login').length, 'modal exists').to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -220,7 +220,7 @@ describe('Acceptance: Settings - Tags', function () {
|
|||
|
||||
// delete tag
|
||||
click('.tag-delete-button');
|
||||
click('.modal-container .btn-red');
|
||||
click('.fullscreen-modal .btn-red');
|
||||
|
||||
andThen(() => {
|
||||
// it redirects to the first tag
|
||||
|
|
|
@ -112,7 +112,7 @@ describe('Acceptance: Team', function () {
|
|||
});
|
||||
|
||||
describe('invite new user', function () {
|
||||
let emailInputField = '.modal-body .form-group input[name="email"]';
|
||||
let emailInputField = '.fullscreen-modal input[name="email"]';
|
||||
|
||||
// @TODO: Evaluate after the modal PR goes in
|
||||
it('modal loads correctly', function () {
|
||||
|
@ -162,8 +162,6 @@ describe('Acceptance: Team', function () {
|
|||
visit('/team');
|
||||
|
||||
andThen(() => {
|
||||
expect(currentURL(), 'currentURL').to.equal('/team');
|
||||
|
||||
expect(find('.user-list.invited-users .user-list-item').length, 'number of invited users').to.equal(0);
|
||||
});
|
||||
|
||||
|
@ -177,7 +175,7 @@ describe('Acceptance: Team', function () {
|
|||
});
|
||||
|
||||
fillIn(emailInputField, 'test@example.com');
|
||||
click('.modal-footer .js-button-accept');
|
||||
click('.fullscreen-modal .btn-green');
|
||||
|
||||
andThen(() => {
|
||||
expect(find('.user-list.invited-users .user-list-item').length, 'number of invited users').to.equal(1);
|
||||
|
@ -197,33 +195,42 @@ describe('Acceptance: Team', function () {
|
|||
|
||||
visit('/team');
|
||||
|
||||
// check our users lists are what we expect
|
||||
andThen(() => {
|
||||
expect(currentURL(), 'currentURL').to.equal('/team');
|
||||
|
||||
expect(find('.user-list.invited-users .user-list-item').length, 'number of invited users').to.equal(1);
|
||||
expect(find('.user-list.invited-users .user-list-item').length, 'number of invited users')
|
||||
.to.equal(1);
|
||||
// number of active users is 2 because of the logged-in user
|
||||
expect(find('.user-list.active-users .user-list-item').length, 'number of active users').to.equal(2);
|
||||
expect(find('.user-list.active-users .user-list-item').length, 'number of active users')
|
||||
.to.equal(2);
|
||||
});
|
||||
|
||||
// click the "invite new user" button to open the modal
|
||||
click('.view-actions .btn-green');
|
||||
|
||||
// fill in and submit the invite user modal with an existing user
|
||||
fillIn(emailInputField, 'test1@example.com');
|
||||
click('.modal-footer .js-button-accept');
|
||||
click('.fullscreen-modal .btn-green');
|
||||
|
||||
andThen(() => {
|
||||
expect(find('.gh-alerts .gh-alert').length, 'number of alerts').to.equal(1);
|
||||
expect(find('.gh-alerts .gh-alert:first').hasClass('gh-alert-yellow'), 'alert is yellow').to.be.true;
|
||||
expect(find('.gh-alerts .gh-alert:first .gh-alert-content').text(), 'first alert\'s text').to.contain('A user with that email address already exists.');
|
||||
// check the inline-validation
|
||||
expect(find('.fullscreen-modal .error .response').text().trim(), 'inviting existing user error')
|
||||
.to.equal('A user with that email address already exists.');
|
||||
});
|
||||
|
||||
click('.gh-alerts .gh-alert:first .gh-alert-close');
|
||||
click('.view-actions .btn-green');
|
||||
// fill in and submit the invite user modal with an invited user
|
||||
fillIn(emailInputField, 'test2@example.com');
|
||||
click('.modal-footer .js-button-accept');
|
||||
click('.fullscreen-modal .btn-green');
|
||||
|
||||
andThen(() => {
|
||||
expect(find('.gh-alerts .gh-alert').length, 'number of alerts').to.equal(1);
|
||||
expect(find('.gh-alerts .gh-alert:first').hasClass('gh-alert-yellow'), 'alert is yellow').to.be.true;
|
||||
expect(find('.gh-alerts .gh-alert:first .gh-alert-content').text(), 'first alert\'s text').to.contain('A user with that email address was already invited.');
|
||||
// check the inline-validation
|
||||
expect(find('.fullscreen-modal .error .response').text().trim(), 'inviting invited user error')
|
||||
.to.equal('A user with that email address was already invited.');
|
||||
|
||||
// ensure that there's been no change in our user lists
|
||||
expect(find('.user-list.invited-users .user-list-item').length, 'number of invited users after failed invites')
|
||||
.to.equal(1);
|
||||
expect(find('.user-list.active-users .user-list-item').length, 'number of active users after failed invites')
|
||||
.to.equal(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -48,14 +48,14 @@ describeComponent(
|
|||
|
||||
it('renders', function () {
|
||||
this.render(hbs`
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
|
||||
`);
|
||||
expect(this.$()).to.have.length(1);
|
||||
});
|
||||
|
||||
it('has the correct title', function () {
|
||||
this.render(hbs`
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
|
||||
`);
|
||||
expect(this.$('.tag-settings-pane h4').text(), 'existing tag title').to.equal('Tag Settings');
|
||||
|
||||
|
@ -65,7 +65,7 @@ describeComponent(
|
|||
|
||||
it('renders main settings', function () {
|
||||
this.render(hbs`
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
|
||||
`);
|
||||
|
||||
expect(this.$('.image-uploader').length, 'displays image uploader').to.equal(1);
|
||||
|
@ -78,7 +78,7 @@ describeComponent(
|
|||
|
||||
it('can switch between main/meta settings', function () {
|
||||
this.render(hbs`
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
|
||||
`);
|
||||
|
||||
expect(this.$('.tag-settings-pane').hasClass('settings-menu-pane-in'), 'main settings are displayed by default').to.be.true;
|
||||
|
@ -105,7 +105,7 @@ describeComponent(
|
|||
});
|
||||
|
||||
this.render(hbs`
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
|
||||
`);
|
||||
|
||||
run(() => {
|
||||
|
@ -133,7 +133,7 @@ describeComponent(
|
|||
});
|
||||
|
||||
this.render(hbs`
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
|
||||
`);
|
||||
|
||||
expectedProperty = 'name';
|
||||
|
@ -187,7 +187,7 @@ describeComponent(
|
|||
hasValidated.push('meta_description');
|
||||
|
||||
this.render(hbs`
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
|
||||
`);
|
||||
|
||||
let nameFormGroup = this.$('input[name="name"]').closest('.form-group');
|
||||
|
@ -212,7 +212,7 @@ describeComponent(
|
|||
|
||||
it('displays char count for text fields', function () {
|
||||
this.render(hbs`
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
|
||||
`);
|
||||
|
||||
let descriptionFormGroup = this.$('textarea[name="description"]').closest('.form-group');
|
||||
|
@ -224,7 +224,7 @@ describeComponent(
|
|||
|
||||
it('renders SEO title preview', function () {
|
||||
this.render(hbs`
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
|
||||
`);
|
||||
expect(this.$('.seo-preview-title').text(), 'displays meta title if present').to.equal('Meta Title');
|
||||
|
||||
|
@ -242,7 +242,7 @@ describeComponent(
|
|||
|
||||
it('renders SEO URL preview', function () {
|
||||
this.render(hbs`
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
|
||||
`);
|
||||
expect(this.$('.seo-preview-link').text(), 'adds url and tag prefix').to.equal('http://localhost:2368/tag/test/');
|
||||
|
||||
|
@ -255,7 +255,7 @@ describeComponent(
|
|||
|
||||
it('renders SEO description preview', function () {
|
||||
this.render(hbs`
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
|
||||
`);
|
||||
expect(this.$('.seo-preview-description').text(), 'displays meta description if present').to.equal('Meta description');
|
||||
|
||||
|
@ -273,7 +273,7 @@ describeComponent(
|
|||
|
||||
it('resets if a new tag is received', function () {
|
||||
this.render(hbs`
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
|
||||
`);
|
||||
run(() => {
|
||||
this.$('.meta-data-button').click();
|
||||
|
@ -287,14 +287,14 @@ describeComponent(
|
|||
});
|
||||
|
||||
it('triggers delete tag modal on delete click', function (done) {
|
||||
this.set('actions.openModal', (modalName, model) => {
|
||||
expect(modalName, 'passed modal name').to.equal('delete-tag');
|
||||
expect(model, 'passed model').to.equal(this.get('tag'));
|
||||
// TODO: will time out if this isn't hit, there's probably a better
|
||||
// way of testing this
|
||||
this.set('actions.openModal', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
this.render(hbs`
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
|
||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') showDeleteTagModal=(action 'openModal')}}
|
||||
`);
|
||||
|
||||
run(() => {
|
||||
|
|
Loading…
Add table
Reference in a new issue