0
Fork 0
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:
Hannah Wolfe 2016-01-12 21:11:38 +00:00
commit c30f1d5ed7
89 changed files with 1269 additions and 1078 deletions

View file

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

View file

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

View 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;

View file

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

View file

@ -21,8 +21,8 @@ export default Component.extend({
this.sendAction('toggleMaximise');
},
openModal(modal) {
this.sendAction('openModal', modal);
showMarkdownHelp() {
this.sendAction('showMarkdownHelp');
},
closeMobileMenu() {

View file

@ -125,7 +125,7 @@ export default Component.extend({
},
deleteTag() {
this.sendAction('openModal', 'delete-tag', this.get('tag'));
this.attrs.showDeleteTagModal();
}
}

View file

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

View file

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

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

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

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

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

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

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

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

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

View file

@ -0,0 +1,4 @@
import ModalComponent from 'ghost/components/modals/base';
export default ModalComponent.extend({
});

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

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

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

View file

@ -10,6 +10,7 @@ export default Controller.extend({
topNotificationCount: 0,
showMobileMenu: false,
showSettingsMenu: false,
showMarkdownHelpModal: false,
autoNav: false,
autoNavOpen: computed('autoNav', {

View file

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

View file

@ -1,8 +0,0 @@
import Ember from 'ember';
const {Controller, computed} = Ember;
const {alias} = computed;
export default Controller.extend({
generatedHTML: alias('model.generatedHTML')
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -113,7 +113,7 @@ let shortcuts = {
}
// Talk to the editor
editor.sendAction('openModal', 'copy-html', {generatedHTML});
editor.send('toggleCopyHTMLModal', generatedHTML);
},
currentDate(replacement) {

View file

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

View file

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

View file

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

View file

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

View file

@ -59,7 +59,7 @@ export default AuthenticatedRoute.extend(base, NotFoundHandler, {
actions: {
authorizationFailed() {
this.send('openModal', 'signin');
this.get('controller').send('toggleReAuthenticateModal');
}
}
});

View file

@ -68,7 +68,7 @@ export default AuthenticatedRoute.extend(ShortcutsRoute, {
},
deletePost() {
this.send('openModal', 'delete-post', this.get('controller.model'));
this.controllerFor('posts').send('toggleDeletePostModal');
}
}
});

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +1,8 @@
{{yield this}}
{{yield this (action 'toggleCopyHTMLModal')}}
{{#if showCopyHTMLModal}}
{{gh-fullscreen-modal "copy-html"
model=copyHTMLModalContent
close=(action "toggleCopyHTMLModal")
modifier="action"}}
{{/if}}

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

View file

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

View file

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

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

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

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

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

View file

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

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -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": {

View file

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

View file

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

View file

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

View file

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