0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-17 23:44:39 -05:00

Merge pull request #5458 from kevinansfield/notifications-refactor

Notifications refactor
This commit is contained in:
Hannah Wolfe 2015-07-29 11:40:47 +01:00
commit 7578188598
65 changed files with 958 additions and 546 deletions

View file

@ -2,7 +2,7 @@ import Ember from 'ember';
export default Ember.Component.extend({ export default Ember.Component.extend({
tagName: 'article', tagName: 'article',
classNames: ['gh-alert', 'gh-alert-blue'], classNames: ['gh-alert'],
classNameBindings: ['typeClass'], classNameBindings: ['typeClass'],
notifications: Ember.inject.service(), notifications: Ember.inject.service(),
@ -10,22 +10,18 @@ export default Ember.Component.extend({
typeClass: Ember.computed(function () { typeClass: Ember.computed(function () {
var classes = '', var classes = '',
message = this.get('message'), message = this.get('message'),
type, type = Ember.get(message, 'type'),
dismissible; typeMapping;
// Check to see if we're working with a DS.Model or a plain JS object typeMapping = {
if (typeof message.toJSON === 'function') { success: 'green',
type = message.get('type'); error: 'red',
dismissible = message.get('dismissible'); warn: 'yellow',
} else { info: 'blue'
type = message.type; };
dismissible = message.dismissible;
}
classes += 'notification-' + type; if (typeMapping[type] !== undefined) {
classes += 'gh-alert-' + typeMapping[type];
if (type === 'success' && dismissible !== false) {
classes += ' notification-passive';
} }
return classes; return classes;

View file

@ -1,18 +1,14 @@
import Ember from 'ember'; import Ember from 'ember';
var AlertsComponent = Ember.Component.extend({
export default Ember.Component.extend({
tagName: 'aside', tagName: 'aside',
classNames: 'gh-alerts', classNames: 'gh-alerts',
messages: Ember.computed.filter('notifications', function (notification) { notifications: Ember.inject.service(),
var displayStatus = (typeof notification.toJSON === 'function') ?
notification.get('status') : notification.status;
return displayStatus === 'persistent'; messages: Ember.computed.alias('notifications.alerts'),
}),
messageCountObserver: Ember.observer('messages.[]', function () { messageCountObserver: Ember.observer('messages.[]', function () {
this.sendAction('notify', this.get('messages').length); this.sendAction('notify', this.get('messages').length);
}) })
}); });
export default AlertsComponent;

View file

@ -2,7 +2,7 @@ import Ember from 'ember';
export default Ember.Component.extend({ export default Ember.Component.extend({
tagName: 'article', tagName: 'article',
classNames: ['gh-notification', 'gh-notification-green'], classNames: ['gh-notification', 'gh-notification-passive'],
classNameBindings: ['typeClass'], classNameBindings: ['typeClass'],
message: null, message: null,
@ -12,22 +12,17 @@ export default Ember.Component.extend({
typeClass: Ember.computed(function () { typeClass: Ember.computed(function () {
var classes = '', var classes = '',
message = this.get('message'), message = this.get('message'),
type, type = Ember.get(message, 'type'),
dismissible; typeMapping;
// Check to see if we're working with a DS.Model or a plain JS object typeMapping = {
if (typeof message.toJSON === 'function') { success: 'green',
type = message.get('type'); error: 'red',
dismissible = message.get('dismissible'); warn: 'yellow'
} else { };
type = message.type;
dismissible = message.dismissible;
}
classes += 'notification-' + type; if (typeMapping[type] !== undefined) {
classes += 'gh-notification-' + typeMapping[type];
if (type === 'success' && dismissible !== false) {
classes += ' notification-passive';
} }
return classes; return classes;
@ -38,11 +33,15 @@ export default Ember.Component.extend({
self.$().on('animationend webkitAnimationEnd oanimationend MSAnimationEnd', function (event) { self.$().on('animationend webkitAnimationEnd oanimationend MSAnimationEnd', function (event) {
if (event.originalEvent.animationName === 'fade-out') { if (event.originalEvent.animationName === 'fade-out') {
self.get('notifications').removeObject(self.get('message')); self.get('notifications').closeNotification(self.get('message'));
} }
}); });
}, },
willDestroyElement: function () {
this.$().off('animationend webkitAnimationEnd oanimationend MSAnimationEnd');
},
actions: { actions: {
closeNotification: function () { closeNotification: function () {
this.get('notifications').closeNotification(this.get('message')); this.get('notifications').closeNotification(this.get('message'));

View file

@ -6,10 +6,5 @@ export default Ember.Component.extend({
notifications: Ember.inject.service(), notifications: Ember.inject.service(),
messages: Ember.computed.filter('notifications.content', function (notification) { messages: Ember.computed.alias('notifications.notifications')
var displayStatus = (typeof notification.toJSON === 'function') ?
notification.get('status') : notification.status;
return displayStatus === 'passive';
})
}); });

View file

@ -13,19 +13,18 @@ var TrimFocusInput = Ember.TextField.extend({
return false; return false;
}), }),
didInsertElement: function () { focusField: Ember.on('didInsertElement', function () {
// This fix is required until Mobile Safari has reliable // This fix is required until Mobile Safari has reliable
// autofocus, select() or focus() support // autofocus, select() or focus() support
if (this.get('focus') && !device.ios()) { if (this.get('focus') && !device.ios()) {
this.$().val(this.$().val()).focus(); this.$().val(this.$().val()).focus();
} }
}, }),
focusOut: function () { trimValue: Ember.on('focusOut', function () {
var text = this.$().val(); var text = this.$().val();
this.$().val(text.trim()); this.$().val(text.trim());
} })
}); });
export default TrimFocusInput; export default TrimFocusInput;

View file

@ -24,10 +24,10 @@ export default Ember.Component.extend({
// If sending the invitation email fails, the API will still return a status of 201 // 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'. // but the user's status in the response object will be 'invited-pending'.
if (result.users[0].status === 'invited-pending') { if (result.users[0].status === 'invited-pending') {
notifications.showWarn('Invitation email was not sent. Please try resending.'); notifications.showAlert('Invitation email was not sent. Please try resending.', {type: 'error'});
} else { } else {
user.set('status', result.users[0].status); user.set('status', result.users[0].status);
notifications.showSuccess(notificationText); notifications.showNotification(notificationText);
} }
}).catch(function (error) { }).catch(function (error) {
notifications.showAPIError(error); notifications.showAPIError(error);
@ -46,14 +46,14 @@ export default Ember.Component.extend({
user.destroyRecord().then(function () { user.destroyRecord().then(function () {
var notificationText = 'Invitation revoked. (' + email + ')'; var notificationText = 'Invitation revoked. (' + email + ')';
notifications.showSuccess(notificationText, false); notifications.showNotification(notificationText);
}).catch(function (error) { }).catch(function (error) {
notifications.showAPIError(error); notifications.showAPIError(error);
}); });
} else { } else {
// if the user is no longer marked as "invited", then show a warning and reload the route // if the user is no longer marked as "invited", then show a warning and reload the route
self.sendAction('reload'); self.sendAction('reload');
notifications.showError('This user has already accepted the invitation.', {delayed: 500}); notifications.showAlert('This user has already accepted the invitation.', {type: 'error', delayed: true});
} }
}); });
} }

View file

@ -12,11 +12,11 @@ export default Ember.Controller.extend({
ajax(this.get('ghostPaths.url').api('db'), { ajax(this.get('ghostPaths.url').api('db'), {
type: 'DELETE' type: 'DELETE'
}).then(function () { }).then(function () {
self.get('notifications').showSuccess('All content deleted from database.'); self.get('notifications').showAlert('All content deleted from database.', {type: 'success'});
self.store.unloadAll('post'); self.store.unloadAll('post');
self.store.unloadAll('tag'); self.store.unloadAll('tag');
}).catch(function (response) { }).catch(function (response) {
self.get('notifications').showErrors(response); self.get('notifications').showAPIError(response);
}); });
}, },

View file

@ -15,9 +15,8 @@ export default Ember.Controller.extend({
model.destroyRecord().then(function () { model.destroyRecord().then(function () {
self.get('dropdown').closeDropdowns(); self.get('dropdown').closeDropdowns();
self.transitionToRoute('posts.index'); self.transitionToRoute('posts.index');
self.get('notifications').showSuccess('Your post has been deleted.', {delayed: true});
}, function () { }, function () {
self.get('notifications').showError('Your post could not be deleted. Please try again.'); self.get('notifications').showAlert('Your post could not be deleted. Please try again.', {type: 'error'});
}); });
}, },

View file

@ -10,14 +10,11 @@ export default Ember.Controller.extend({
actions: { actions: {
confirmAccept: function () { confirmAccept: function () {
var tag = this.get('model'), var tag = this.get('model'),
name = tag.get('name'),
self = this; self = this;
this.send('closeMenus'); this.send('closeMenus');
tag.destroyRecord().then(function () { tag.destroyRecord().catch(function (error) {
self.get('notifications').showSuccess('Deleted ' + name);
}).catch(function (error) {
self.get('notifications').showAPIError(error); self.get('notifications').showAPIError(error);
}); });
}, },

View file

@ -31,9 +31,8 @@ export default Ember.Controller.extend({
user.destroyRecord().then(function () { user.destroyRecord().then(function () {
self.store.unloadAll('post'); self.store.unloadAll('post');
self.transitionToRoute('team'); self.transitionToRoute('team');
self.get('notifications').showSuccess('The user has been deleted.', {delayed: true});
}, function () { }, function () {
self.get('notifications').showError('The user could not be deleted. Please try again.'); self.get('notifications').showAlert('The user could not be deleted. Please try again.', {type: 'error'});
}); });
}, },

View file

@ -55,9 +55,9 @@ export default Ember.Controller.extend({
if (invitedUser) { if (invitedUser) {
if (invitedUser.get('status') === 'invited' || invitedUser.get('status') === 'invited-pending') { if (invitedUser.get('status') === 'invited' || invitedUser.get('status') === 'invited-pending') {
self.get('notifications').showWarn('A user with that email address was already invited.'); self.get('notifications').showAlert('A user with that email address was already invited.', {type: 'warn'});
} else { } else {
self.get('notifications').showWarn('A user with that email address already exists.'); self.get('notifications').showAlert('A user with that email address already exists.', {type: 'warn'});
} }
} else { } else {
newUser = self.store.createRecord('user', { newUser = self.store.createRecord('user', {
@ -72,12 +72,16 @@ export default Ember.Controller.extend({
// If sending the invitation email fails, the API will still return a status of 201 // 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'. // but the user's status in the response object will be 'invited-pending'.
if (newUser.get('status') === 'invited-pending') { if (newUser.get('status') === 'invited-pending') {
self.get('notifications').showWarn('Invitation email was not sent. Please try resending.'); self.get('notifications').showAlert('Invitation email was not sent. Please try resending.', {type: 'error'});
} else { } else {
self.get('notifications').showSuccess(notificationText); self.get('notifications').showAlert(notificationText, {type: 'success'});
} }
}).catch(function (errors) { }).catch(function (errors) {
newUser.deleteRecord(); 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
self.get('notifications').showErrors(errors); self.get('notifications').showErrors(errors);
}); });
} }

View file

@ -19,7 +19,7 @@ export default Ember.Controller.extend({
} }
if (!transition || !editorController) { if (!transition || !editorController) {
this.get('notifications').showError('Sorry, there was an error in the application. Please let the Ghost team know what happened.'); this.get('notifications').showNotification('Sorry, there was an error in the application. Please let the Ghost team know what happened.', {type: 'error'});
return true; return true;
} }

View file

@ -22,7 +22,6 @@ export default Ember.Controller.extend(ValidationEngine, {
this.get('session').authenticate(authStrategy, data).then(function () { this.get('session').authenticate(authStrategy, data).then(function () {
self.send('closeModal'); self.send('closeModal');
self.get('notifications').showSuccess('Login successful.');
self.set('password', ''); self.set('password', '');
}).catch(function () { }).catch(function () {
// if authentication fails a rejected promise will be returned. // if authentication fails a rejected promise will be returned.
@ -41,7 +40,7 @@ export default Ember.Controller.extend(ValidationEngine, {
$('#login').find('input').trigger('change'); $('#login').find('input').trigger('change');
this.validate({format: false}).then(function () { this.validate({format: false}).then(function () {
self.get('notifications').closePassive(); self.get('notifications').closeNotifications();
self.send('authenticate'); self.send('authenticate');
}).catch(function (errors) { }).catch(function (errors) {
self.get('notifications').showErrors(errors); self.get('notifications').showErrors(errors);

View file

@ -33,7 +33,7 @@ export default Ember.Controller.extend({
}); });
} }
self.get('notifications').showSuccess('Ownership successfully transferred to ' + user.get('name')); self.get('notifications').showAlert('Ownership successfully transferred to ' + user.get('name'), {type: 'success'});
}).catch(function (error) { }).catch(function (error) {
self.get('notifications').showAPIError(error); self.get('notifications').showAPIError(error);
}); });

View file

@ -10,11 +10,9 @@ export default Ember.Controller.extend({
var notifications = this.get('notifications'); var notifications = this.get('notifications');
this.get('model').save().then(function (model) { this.get('model').save().then(function (model) {
notifications.showSuccess('Saved');
return model; return model;
}).catch(function (err) { }).catch(function (err) {
notifications.showErrors(err); notifications.showAPIError(err);
}); });
}, },

View file

@ -193,10 +193,6 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
this.get('notifications').showErrors(errors); this.get('notifications').showErrors(errors);
}, },
showSuccess: function (message) {
this.get('notifications').showSuccess(message);
},
actions: { actions: {
togglePage: function () { togglePage: function () {
var self = this; var self = this;

View file

@ -33,8 +33,8 @@ export default Ember.Controller.extend(ValidationEngine, {
var credentials = this.getProperties('newPassword', 'ne2Password', 'token'), var credentials = this.getProperties('newPassword', 'ne2Password', 'token'),
self = this; self = this;
this.toggleProperty('submitting'); this.validate().then(function () {
this.validate({format: false}).then(function () { self.toggleProperty('submitting');
ajax({ ajax({
url: self.get('ghostPaths.url').api('authentication', 'passwordreset'), url: self.get('ghostPaths.url').api('authentication', 'passwordreset'),
type: 'PUT', type: 'PUT',
@ -43,7 +43,7 @@ export default Ember.Controller.extend(ValidationEngine, {
} }
}).then(function (resp) { }).then(function (resp) {
self.toggleProperty('submitting'); self.toggleProperty('submitting');
self.get('notifications').showSuccess(resp.passwordreset[0].message, true); self.get('notifications').showAlert(resp.passwordreset[0].message, {type: 'warn', delayed: true});
self.get('session').authenticate('simple-auth-authenticator:oauth2-password-grant', { self.get('session').authenticate('simple-auth-authenticator:oauth2-password-grant', {
identification: self.get('email'), identification: self.get('email'),
password: credentials.newPassword password: credentials.newPassword
@ -52,9 +52,6 @@ export default Ember.Controller.extend(ValidationEngine, {
self.get('notifications').showAPIError(response); self.get('notifications').showAPIError(response);
self.toggleProperty('submitting'); self.toggleProperty('submitting');
}); });
}).catch(function (error) {
self.toggleProperty('submitting');
self.get('notifications').showErrors(error);
}); });
} }
} }

View file

@ -8,13 +8,9 @@ export default Ember.Controller.extend({
var notifications = this.get('notifications'); var notifications = this.get('notifications');
return this.get('model').save().then(function (model) { return this.get('model').save().then(function (model) {
notifications.closePassive();
notifications.showSuccess('Settings successfully saved.');
return model; return model;
}).catch(function (errors) { }).catch(function (error) {
notifications.closePassive(); notifications.showAPIError(error);
notifications.showErrors(errors);
}); });
} }
} }

View file

@ -63,17 +63,22 @@ export default Ember.Controller.extend({
}), }),
actions: { actions: {
validate: function () {
this.get('model').validate(arguments);
},
save: function () { save: function () {
var notifications = this.get('notifications'), var notifications = this.get('notifications'),
config = this.get('config'); config = this.get('config');
return this.get('model').save().then(function (model) { return this.get('model').save().then(function (model) {
config.set('blogTitle', model.get('title')); config.set('blogTitle', model.get('title'));
notifications.showSuccess('Settings successfully saved.');
return model; return model;
}).catch(function (errors) { }).catch(function (error) {
notifications.showErrors(errors); if (error) {
notifications.showAPIError(error);
}
}); });
}, },

View file

@ -36,7 +36,7 @@ export default Ember.Controller.extend({
this.set('uploadButtonText', 'Importing'); this.set('uploadButtonText', 'Importing');
this.set('importErrors', ''); this.set('importErrors', '');
notifications.closePassive(); notifications.closeNotifications();
formData.append('importfile', file); formData.append('importfile', file);
@ -52,13 +52,14 @@ export default Ember.Controller.extend({
self.store.unloadAll(); self.store.unloadAll();
// Reload currentUser and set session // Reload currentUser and set session
self.set('session.user', self.store.find('user', currentUserId)); self.set('session.user', self.store.find('user', currentUserId));
notifications.showSuccess('Import successful.'); // TODO: keep as notification, add link to view content
notifications.showNotification('Import successful.');
}).catch(function (response) { }).catch(function (response) {
if (response && response.jqXHR && response.jqXHR.responseJSON && response.jqXHR.responseJSON.errors) { if (response && response.jqXHR && response.jqXHR.responseJSON && response.jqXHR.responseJSON.errors) {
self.set('importErrors', response.jqXHR.responseJSON.errors); self.set('importErrors', response.jqXHR.responseJSON.errors);
} }
notifications.showError('Import Failed'); notifications.showAlert('Import Failed', {type: 'error'});
}).finally(function () { }).finally(function () {
self.set('uploadButtonText', 'Import'); self.set('uploadButtonText', 'Import');
}); });
@ -82,7 +83,7 @@ export default Ember.Controller.extend({
ajax(this.get('ghostPaths.url').api('mail', 'test'), { ajax(this.get('ghostPaths.url').api('mail', 'test'), {
type: 'POST' type: 'POST'
}).then(function () { }).then(function () {
notifications.showSuccess('Check your email for the test message.'); notifications.showAlert('Check your email for the test message.', {type: 'info'});
}).catch(function (error) { }).catch(function (error) {
if (typeof error.jqXHR !== 'undefined') { if (typeof error.jqXHR !== 'undefined') {
notifications.showAPIError(error); notifications.showAPIError(error);

View file

@ -108,7 +108,7 @@ export default Ember.Controller.extend({
// Don't save if there's a blank label. // Don't save if there's a blank label.
if (navItems.find(function (item) {return !item.get('isComplete') && !item.get('last');})) { if (navItems.find(function (item) {return !item.get('isComplete') && !item.get('last');})) {
notifications.showErrors([message.htmlSafe()]); notifications.showAlert(message.htmlSafe(), {type: 'error'});
return; return;
} }
@ -148,11 +148,9 @@ export default Ember.Controller.extend({
// we need to have navigationItems recomputed. // we need to have navigationItems recomputed.
this.get('model').notifyPropertyChange('navigation'); this.get('model').notifyPropertyChange('navigation');
notifications.closePassive(); notifications.closeNotifications();
this.get('model').save().then(function () { this.get('model').save().catch(function (err) {
notifications.showSuccess('Navigation items saved.');
}).catch(function (err) {
notifications.showErrors(err); notifications.showErrors(err);
}); });
} }

View file

@ -59,7 +59,7 @@ export default Ember.Controller.extend(PaginationMixin, SettingsMenuMixin, {
activeTag.set(propKey, newValue); activeTag.set(propKey, newValue);
this.get('notifications').closePassive(); this.get('notifications').closeNotifications();
activeTag.save().catch(function (errors) { activeTag.save().catch(function (errors) {
self.showErrors(errors); self.showErrors(errors);

View file

@ -1,7 +1,9 @@
import Ember from 'ember'; import Ember from 'ember';
import DS from 'ember-data';
export default Ember.Controller.extend({ export default Ember.Controller.extend({
notifications: Ember.inject.service(), notifications: Ember.inject.service(),
errors: DS.Errors.create(),
users: '', users: '',
usersArray: Ember.computed('users', function () { usersArray: Ember.computed('users', function () {
var users = this.get('users').split('\n').filter(function (email) { var users = this.get('users').split('\n').filter(function (email) {
@ -62,10 +64,11 @@ export default Ember.Controller.extend({
var self = this, var self = this,
validationErrors = this.get('validateUsers'), validationErrors = this.get('validateUsers'),
users = this.get('usersArray'), users = this.get('usersArray'),
errorMessages,
notifications = this.get('notifications'), notifications = this.get('notifications'),
invitationsString; invitationsString;
this.get('errors').clear();
if (validationErrors === true && users.length > 0) { if (validationErrors === true && users.length > 0) {
this.get('authorRole').then(function (authorRole) { this.get('authorRole').then(function (authorRole) {
Ember.RSVP.Promise.all( Ember.RSVP.Promise.all(
@ -104,30 +107,28 @@ export default Ember.Controller.extend({
if (erroredEmails.length > 0) { if (erroredEmails.length > 0) {
message = 'Failed to send ' + erroredEmails.length + ' invitations: '; message = 'Failed to send ' + erroredEmails.length + ' invitations: ';
message += erroredEmails.join(', '); message += erroredEmails.join(', ');
notifications.showError(message, {delayed: successCount > 0}); notifications.showAlert(message, {type: 'error', delayed: successCount > 0});
} }
if (successCount > 0) { if (successCount > 0) {
// pluralize // pluralize
invitationsString = successCount > 1 ? 'invitations' : 'invitation'; invitationsString = successCount > 1 ? 'invitations' : 'invitation';
notifications.showSuccess(successCount + ' ' + invitationsString + ' sent!', {delayed: true}); notifications.showAlert(successCount + ' ' + invitationsString + ' sent!', {type: 'success', delayed: true});
self.transitionTo('posts.index'); self.transitionTo('posts.index');
} }
}); });
}); });
} else if (users.length === 0) { } else if (users.length === 0) {
notifications.showError('No users to invite.'); this.get('errors').add('users', 'No users to invite.');
} else { } else {
errorMessages = validationErrors.map(function (error) { validationErrors.forEach(function (error) {
// Only one error type here so far, but one day the errors might be more detailed // Only one error type here so far, but one day the errors might be more detailed
switch (error.error) { switch (error.error) {
case 'email': case 'email':
return {message: error.user + ' is not a valid email.'}; self.get('errors').add('users', error.user + ' is not a valid email.');
} }
}); });
notifications.showErrors(errorMessages);
} }
} }
} }

View file

@ -3,13 +3,14 @@ import ValidationEngine from 'ghost/mixins/validation-engine';
import {request as ajax} from 'ic-ajax'; import {request as ajax} from 'ic-ajax';
export default Ember.Controller.extend(ValidationEngine, { export default Ember.Controller.extend(ValidationEngine, {
validationType: 'signin',
submitting: false, submitting: false,
ghostPaths: Ember.inject.service('ghost-paths'), ghostPaths: Ember.inject.service('ghost-paths'),
notifications: Ember.inject.service(), notifications: Ember.inject.service(),
// ValidationEngine settings
validationType: 'signin',
actions: { actions: {
authenticate: function () { authenticate: function () {
var model = this.get('model'), var model = this.get('model'),
@ -30,12 +31,12 @@ export default Ember.Controller.extend(ValidationEngine, {
// browsers and password managers that don't send proper events on autofill // browsers and password managers that don't send proper events on autofill
$('#login').find('input').trigger('change'); $('#login').find('input').trigger('change');
this.validate({format: false}).then(function () { this.validate().then(function () {
self.get('notifications').closePassive(); self.get('notifications').closeNotifications();
self.send('authenticate'); self.send('authenticate');
}).catch(function (errors) { }).catch(function (error) {
if (errors) { if (error) {
self.get('notifications').showErrors(errors); self.get('notifications').showAPIError(error);
} }
}); });
}, },
@ -45,26 +46,24 @@ export default Ember.Controller.extend(ValidationEngine, {
notifications = this.get('notifications'), notifications = this.get('notifications'),
self = this; self = this;
if (!email) { this.validate({property: 'identification'}).then(function () {
return notifications.showError('Enter email address to reset password.'); self.set('submitting', true);
}
self.set('submitting', true); ajax({
url: self.get('ghostPaths.url').api('authentication', 'passwordreset'),
ajax({ type: 'POST',
url: self.get('ghostPaths.url').api('authentication', 'passwordreset'), data: {
type: 'POST', passwordreset: [{
data: { email: email
passwordreset: [{ }]
email: email }
}] }).then(function () {
} self.set('submitting', false);
}).then(function () { notifications.showAlert('Please check your email for instructions.', {type: 'info'});
self.set('submitting', false); }).catch(function (resp) {
notifications.showSuccess('Please check your email for instructions.'); self.set('submitting', false);
}).catch(function (resp) { notifications.showAPIError(resp, {defaultErrorText: 'There was a problem with the reset, please try again.'});
self.set('submitting', false); });
notifications.showAPIError(resp, {defaultErrorText: 'There was a problem with the reset, please try again.'});
}); });
} }
} }

View file

@ -18,10 +18,10 @@ export default Ember.Controller.extend(ValidationEngine, {
data = model.getProperties('name', 'email', 'password', 'token'), data = model.getProperties('name', 'email', 'password', 'token'),
notifications = this.get('notifications'); notifications = this.get('notifications');
notifications.closePassive(); notifications.closeNotifications();
this.toggleProperty('submitting'); this.validate().then(function () {
this.validate({format: false}).then(function () { this.toggleProperty('submitting');
ajax({ ajax({
url: self.get('ghostPaths.url').api('authentication', 'invitation'), url: self.get('ghostPaths.url').api('authentication', 'invitation'),
type: 'POST', type: 'POST',
@ -43,10 +43,9 @@ export default Ember.Controller.extend(ValidationEngine, {
self.toggleProperty('submitting'); self.toggleProperty('submitting');
notifications.showAPIError(resp); notifications.showAPIError(resp);
}); });
}).catch(function (errors) { }).catch(function (error) {
self.toggleProperty('submitting'); if (error) {
if (errors) { notifications.showAPIError(error);
notifications.showErrors(errors);
} }
}); });
} }

View file

@ -2,8 +2,12 @@ import Ember from 'ember';
import SlugGenerator from 'ghost/models/slug-generator'; import SlugGenerator from 'ghost/models/slug-generator';
import isNumber from 'ghost/utils/isNumber'; import isNumber from 'ghost/utils/isNumber';
import boundOneWay from 'ghost/utils/bound-one-way'; import boundOneWay from 'ghost/utils/bound-one-way';
import ValidationEngine from 'ghost/mixins/validation-engine';
export default Ember.Controller.extend(ValidationEngine, {
// ValidationEngine settings
validationType: 'user',
export default Ember.Controller.extend({
ghostPaths: Ember.inject.service('ghost-paths'), ghostPaths: Ember.inject.service('ghost-paths'),
notifications: Ember.inject.service(), notifications: Ember.inject.service(),
@ -105,8 +109,6 @@ export default Ember.Controller.extend({
var currentPath, var currentPath,
newPath; newPath;
self.get('notifications').showSuccess('Settings successfully saved.');
// If the user's slug has changed, change the URL and replace // If the user's slug has changed, change the URL and replace
// the history so refresh and back button still work // the history so refresh and back button still work
if (slugChanged) { if (slugChanged) {
@ -142,13 +144,14 @@ export default Ember.Controller.extend({
ne2Password: '' ne2Password: ''
}); });
self.get('notifications').showSuccess('Password updated.'); self.get('notifications').showAlert('Password updated.', {type: 'success'});
return model; return model;
}).catch(function (errors) { }).catch(function (errors) {
self.get('notifications').showAPIError(errors); self.get('notifications').showAPIError(errors);
}); });
} else { } else {
// TODO: switch to in-line validation
self.get('notifications').showErrors(user.get('passwordValidationErrors')); self.get('notifications').showErrors(user.get('passwordValidationErrors'));
} }
}, },

View file

@ -209,6 +209,7 @@ export default Ember.Mixin.create({
} }
}, },
// TODO: Update for new notification click-action API
showSaveNotification: function (prevStatus, status, delay) { showSaveNotification: function (prevStatus, status, delay) {
var message = this.messageMap.success.post[prevStatus][status], var message = this.messageMap.success.post[prevStatus][status],
path = this.get('model.absoluteUrl'), path = this.get('model.absoluteUrl'),
@ -219,7 +220,7 @@ export default Ember.Mixin.create({
message += `&nbsp;<a href="${path}">View ${type}</a>`; message += `&nbsp;<a href="${path}">View ${type}</a>`;
} }
notifications.showSuccess(message.htmlSafe(), {delayed: delay}); notifications.showNotification(message.htmlSafe(), {delayed: delay});
}, },
showErrorNotification: function (prevStatus, status, errors, delay) { showErrorNotification: function (prevStatus, status, errors, delay) {
@ -229,7 +230,7 @@ export default Ember.Mixin.create({
message += '<br />' + error; message += '<br />' + error;
notifications.showError(message.htmlSafe(), {delayed: delay}); notifications.showAlert(message.htmlSafe(), {type: 'error', delayed: delay});
}, },
actions: { actions: {
@ -263,7 +264,7 @@ export default Ember.Mixin.create({
this.set('timedSaveId', null); this.set('timedSaveId', null);
} }
notifications.closePassive(); notifications.closeNotifications();
// Set the properties that are indirected // Set the properties that are indirected
// set markdown equal to what's in the editor, minus the image markers. // set markdown equal to what's in the editor, minus the image markers.

View file

@ -25,7 +25,7 @@ export default Ember.Mixin.create({
message += '.'; message += '.';
} }
this.get('notifications').showError(message); this.get('notifications').showAlert(message, {type: 'error'});
}, },
actions: { actions: {

View file

@ -15,49 +15,6 @@ import TagSettingsValidator from 'ghost/validators/tag-settings';
// our extensions to the validator library // our extensions to the validator library
ValidatorExtensions.init(); ValidatorExtensions.init();
// This is here because it is used by some things that format errors from api responses
// This function should be removed in the notifications refactor
// format errors to be used in `notifications.showErrors`.
// result is [{message: 'concatenated error messages'}]
function formatErrors(errors, opts) {
var message = 'There was an error';
opts = opts || {};
if (opts.wasSave && opts.validationType) {
message += ' saving this ' + opts.validationType;
}
if (Ember.isArray(errors)) {
// get the validator's error messages from the array.
// normalize array members to map to strings.
message = errors.map(function (error) {
var errorMessage;
if (typeof error === 'string') {
errorMessage = error;
} else {
errorMessage = error.message;
}
return Ember.Handlebars.Utils.escapeExpression(errorMessage);
}).join('<br />').htmlSafe();
} else if (errors instanceof Error) {
message += errors.message || '.';
} else if (typeof errors === 'object') {
// Get messages from server response
message += ': ' + getRequestErrorMessage(errors, true);
} else if (typeof errors === 'string') {
message += ': ' + errors;
} else {
message += '.';
}
// set format for notifications.showErrors
message = [{message: message}];
return message;
}
/** /**
* The class that gets this mixin will receive these properties and functions. * The class that gets this mixin will receive these properties and functions.
* It will be able to validate any properties on itself (or the model it passes to validate()) * It will be able to validate any properties on itself (or the model it passes to validate())
@ -163,15 +120,10 @@ export default Ember.Mixin.create({
return this.validate(options).then(function () { return this.validate(options).then(function () {
return _super.call(self, options); return _super.call(self, options);
}).catch(function (result) { }).catch(function (result) {
// server save failed - validate() would have given back an array // server save failed or validator type doesn't exist
if (!Ember.isArray(result)) { if (result && !Ember.isArray(result)) {
if (options.format !== false) { // return the array of errors from the server
// concatenate all errors into an array with a single object: [{message: 'concatted message'}] result = getRequestErrorMessage(result);
result = formatErrors(result, options);
} else {
// return the array of errors from the server
result = getRequestErrorMessage(result);
}
} }
return Ember.RSVP.reject(result); return Ember.RSVP.reject(result);

View file

@ -1,7 +1,6 @@
import DS from 'ember-data'; import DS from 'ember-data';
var Notification = DS.Model.extend({ var Notification = DS.Model.extend({
dismissible: DS.attr('boolean'), dismissible: DS.attr('boolean'),
location: DS.attr('string'),
status: DS.attr('string'), status: DS.attr('string'),
type: DS.attr('string'), type: DS.attr('string'),
message: DS.attr('string') message: DS.attr('string')

View file

@ -11,7 +11,7 @@ var Router = Ember.Router.extend({
clearNotifications: Ember.on('didTransition', function () { clearNotifications: Ember.on('didTransition', function () {
var notifications = this.get('notifications'); var notifications = this.get('notifications');
notifications.closePassive(); notifications.closeNotifications();
notifications.displayDelayed(); notifications.displayDelayed();
}) })
}); });

View file

@ -66,7 +66,7 @@ export default Ember.Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
this.get('notifications').showErrors(error.errors); this.get('notifications').showErrors(error.errors);
} else { } else {
// connection errors don't return proper status message, only req.body // connection errors don't return proper status message, only req.body
this.get('notifications').showError('There was a problem on the server.'); this.get('notifications').showAlert('There was a problem on the server.', {type: 'error'});
} }
}, },
@ -91,7 +91,7 @@ export default Ember.Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
}, },
sessionInvalidationFailed: function (error) { sessionInvalidationFailed: function (error) {
this.get('notifications').showError(error.message); this.get('notifications').showAlert(error.message, {type: 'error'});
}, },
openModal: function (modalName, model, type) { openModal: function (modalName, model, type) {
@ -152,19 +152,6 @@ export default Ember.Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
} }
}, },
handleErrors: function (errors) {
var notifications = this.get('notifications');
notifications.clear();
errors.forEach(function (errorObj) {
notifications.showError(errorObj.message || errorObj);
if (errorObj.hasOwnProperty('el')) {
errorObj.el.addClass('input-error');
}
});
},
// noop default for unhandled save (used from shortcuts) // noop default for unhandled save (used from shortcuts)
save: Ember.K save: Ember.K
} }

View file

@ -9,7 +9,7 @@ export default Ember.Route.extend(styleBody, {
beforeModel: function () { beforeModel: function () {
if (this.get('session').isAuthenticated) { if (this.get('session').isAuthenticated) {
this.get('notifications').showWarn('You can\'t reset your password while you\'re signed in.', {delayed: true}); this.get('notifications').showAlert('You can\'t reset your password while you\'re signed in.', {type: 'warn', delayed: true});
this.transitionTo(Configuration.routeAfterAuthentication); this.transitionTo(Configuration.routeAfterAuthentication);
} }
}, },

View file

@ -12,7 +12,7 @@ export default Ember.Route.extend(styleBody, {
beforeModel: function () { beforeModel: function () {
if (this.get('session').isAuthenticated) { if (this.get('session').isAuthenticated) {
this.get('notifications').showWarn('You need to sign out to register as a new user.', {delayed: true}); this.get('notifications').showAlert('You need to sign out to register as a new user.', {type: 'warn', delayed: true});
this.transitionTo(Configuration.routeAfterAuthentication); this.transitionTo(Configuration.routeAfterAuthentication);
} }
}, },
@ -26,7 +26,7 @@ export default Ember.Route.extend(styleBody, {
return new Ember.RSVP.Promise(function (resolve) { return new Ember.RSVP.Promise(function (resolve) {
if (!re.test(params.token)) { if (!re.test(params.token)) {
self.get('notifications').showError('Invalid token.', {delayed: true}); self.get('notifications').showAlert('Invalid token.', {type: 'error', delayed: true});
return resolve(self.transitionTo('signin')); return resolve(self.transitionTo('signin'));
} }
@ -47,7 +47,7 @@ export default Ember.Route.extend(styleBody, {
} }
}).then(function (response) { }).then(function (response) {
if (response && response.invitation && response.invitation[0].valid === false) { if (response && response.invitation && response.invitation[0].valid === false) {
self.get('notifications').showError('The invitation does not exist or is no longer valid.', {delayed: true}); self.get('notifications').showAlert('The invitation does not exist or is no longer valid.', {type: 'warn', delayed: true});
return resolve(self.transitionTo('signin')); return resolve(self.transitionTo('signin'));
} }

View file

@ -1,138 +1,94 @@
import Ember from 'ember'; import Ember from 'ember';
import Notification from 'ghost/models/notification';
export default Ember.Service.extend({ export default Ember.Service.extend({
delayedNotifications: Ember.A(), delayedNotifications: Ember.A(),
content: Ember.A(), content: Ember.A(),
timeout: 3000,
pushObject: function (object) { alerts: Ember.computed.filter('content', function (notification) {
// object can be either a DS.Model or a plain JS object, so when working with var status = Ember.get(notification, 'status');
// it, we need to handle both cases. return status === 'alert';
}),
// make sure notifications have all the necessary properties set. notifications: Ember.computed.filter('content', function (notification) {
if (typeof object.toJSON === 'function') { var status = Ember.get(notification, 'status');
// working with a DS.Model return status === 'notification';
}),
if (object.get('location') === '') {
object.set('location', 'bottom');
}
} else {
if (!object.location) {
object.location = 'bottom';
}
}
this._super(object);
},
handleNotification: function (message, delayed) { handleNotification: function (message, delayed) {
if (typeof message.toJSON === 'function') { // If this is an alert message from the server, treat it as html safe
// If this is a persistent message from the server, treat it as html safe if (typeof message.toJSON === 'function' && message.get('status') === 'alert') {
if (message.get('status') === 'persistent') { message.set('message', message.get('message').htmlSafe());
message.set('message', message.get('message').htmlSafe()); }
}
if (!message.get('status')) { if (!Ember.get(message, 'status')) {
message.set('status', 'passive'); Ember.set(message, 'status', 'notification');
}
} else {
if (!message.status) {
message.status = 'passive';
}
} }
if (!delayed) { if (!delayed) {
this.get('content').pushObject(message); this.get('content').pushObject(message);
} else { } else {
this.delayedNotifications.pushObject(message); this.get('delayedNotifications').pushObject(message);
} }
}, },
showError: function (message, options) { showAlert: function (message, options) {
options = options || {}; options = options || {};
if (!options.doNotClosePassive) {
this.closePassive();
}
this.handleNotification({ this.handleNotification({
type: 'error', message: message,
message: message status: 'alert',
type: options.type
}, options.delayed); }, options.delayed);
}, },
showNotification: function (message, options) {
options = options || {};
if (!options.doNotCloseNotifications) {
this.closeNotifications();
}
this.handleNotification({
message: message,
status: 'notification',
type: options.type
}, options.delayed);
},
// TODO: review whether this can be removed once no longer used by validations
showErrors: function (errors, options) { showErrors: function (errors, options) {
options = options || {}; options = options || {};
if (!options.doNotClosePassive) { if (!options.doNotCloseNotifications) {
this.closePassive(); this.closeNotifications();
} }
for (var i = 0; i < errors.length; i += 1) { for (var i = 0; i < errors.length; i += 1) {
this.showError(errors[i].message || errors[i], {doNotClosePassive: true}); this.showNotification(errors[i].message || errors[i], {type: 'error', doNotCloseNotifications: true});
} }
}, },
showAPIError: function (resp, options) { showAPIError: function (resp, options) {
options = options || {}; options = options || {};
options.type = options.type || 'error';
if (!options.doNotClosePassive) { if (!options.doNotCloseNotifications) {
this.closePassive(); this.closeNotifications();
} }
options.defaultErrorText = options.defaultErrorText || 'There was a problem on the server, please try again.'; options.defaultErrorText = options.defaultErrorText || 'There was a problem on the server, please try again.';
if (resp && resp.jqXHR && resp.jqXHR.responseJSON && resp.jqXHR.responseJSON.error) { if (resp && resp.jqXHR && resp.jqXHR.responseJSON && resp.jqXHR.responseJSON.error) {
this.showError(resp.jqXHR.responseJSON.error, options); this.showAlert(resp.jqXHR.responseJSON.error, options);
} else if (resp && resp.jqXHR && resp.jqXHR.responseJSON && resp.jqXHR.responseJSON.errors) { } else if (resp && resp.jqXHR && resp.jqXHR.responseJSON && resp.jqXHR.responseJSON.errors) {
this.showErrors(resp.jqXHR.responseJSON.errors, options); this.showErrors(resp.jqXHR.responseJSON.errors, options);
} else if (resp && resp.jqXHR && resp.jqXHR.responseJSON && resp.jqXHR.responseJSON.message) { } else if (resp && resp.jqXHR && resp.jqXHR.responseJSON && resp.jqXHR.responseJSON.message) {
this.showError(resp.jqXHR.responseJSON.message, options); this.showAlert(resp.jqXHR.responseJSON.message, options);
} else { } else {
this.showError(options.defaultErrorText, {doNotClosePassive: true}); this.showAlert(options.defaultErrorText, {type: options.type, doNotCloseNotifications: true});
} }
}, },
showInfo: function (message, options) {
options = options || {};
if (!options.doNotClosePassive) {
this.closePassive();
}
this.handleNotification({
type: 'info',
message: message
}, options.delayed);
},
showSuccess: function (message, options) {
options = options || {};
if (!options.doNotClosePassive) {
this.closePassive();
}
this.handleNotification({
type: 'success',
message: message
}, options.delayed);
},
showWarn: function (message, options) {
options = options || {};
if (!options.doNotClosePassive) {
this.closePassive();
}
this.handleNotification({
type: 'warn',
message: message
}, options.delayed);
},
displayDelayed: function () { displayDelayed: function () {
var self = this; var self = this;
@ -145,7 +101,7 @@ export default Ember.Service.extend({
closeNotification: function (notification) { closeNotification: function (notification) {
var content = this.get('content'); var content = this.get('content');
if (notification instanceof Notification) { if (typeof notification.toJSON === 'function') {
notification.deleteRecord(); notification.deleteRecord();
notification.save().finally(function () { notification.save().finally(function () {
content.removeObject(notification); content.removeObject(notification);
@ -155,12 +111,8 @@ export default Ember.Service.extend({
} }
}, },
closePassive: function () { closeNotifications: function () {
this.set('content', this.get('content').rejectBy('status', 'passive')); this.set('content', this.get('content').rejectBy('status', 'notification'));
},
closePersistent: function () {
this.set('content', this.get('content').rejectBy('status', 'persistent'));
}, },
closeAll: function () { closeAll: function () {

View file

@ -63,6 +63,16 @@
color: var(--red); color: var(--red);
} }
.gh-notification-passive {
animation: fade-out;
animation-delay: 5s;
animation-iteration-count: 1;
}
.gh-notification-passive:hover {
animation: fade-in;
}
/* Red notification /* Red notification
/* ---------------------------------------------------------- */ /* ---------------------------------------------------------- */

View file

@ -377,6 +377,7 @@
} }
.gh-flow-content .gh-flow-invite { .gh-flow-content .gh-flow-invite {
position: relative;
margin: 0 auto; margin: 0 auto;
max-width: 400px; max-width: 400px;
width: 100%; width: 100%;

View file

@ -2,12 +2,21 @@
<div class="gh-flow-content-wrap"> <div class="gh-flow-content-wrap">
<section class="gh-flow-content fade-in"> <section class="gh-flow-content fade-in">
<form id="reset" class="gh-signin" method="post" novalidate="novalidate" {{action "submit" on="submit"}}> <form id="reset" class="gh-signin" method="post" novalidate="novalidate" {{action "submit" on="submit"}}>
<div class="form-group"> {{#gh-form-group errors=errors property="newPassword"}}
{{input value=newPassword class="gh-input password" type="password" placeholder="Password" name="newpassword" autofocus="autofocus" }} {{input value=newPassword class="gh-input password" type="password" placeholder="Password" name="newpassword" autofocus="autofocus" focusOut=(action "validate" "newPassword")}}
</div> <div class="pw-strength">
<div class="form-group"> <div class="pw-strength-dot"></div>
{{input value=ne2Password class="gh-input password" type="password" placeholder="Confirm Password" name="ne2password" }} <div class="pw-strength-dot"></div>
</div> <div class="pw-strength-dot"></div>
<div class="pw-strength-dot"></div>
<div class="pw-strength-dot <!--pw-strength-activedot-->"></div>
</div>
{{gh-error-message errors=errors property="newPassword"}}
{{/gh-form-group}}
{{#gh-form-group errors=errors property="ne2Password"}}
{{input value=ne2Password class="gh-input password" type="password" placeholder="Confirm Password" name="ne2password" focusOut=(action "validate" "ne2Password")}}
{{gh-error-message errors=errors property="ne2Password"}}
{{/gh-form-group}}
<button class="btn btn-blue btn-block" type="submit" disabled={{submitting}}>Reset Password</button> <button class="btn btn-blue btn-block" type="submit" disabled={{submitting}}>Reset Password</button>
</form> </form>
</section> </section>

View file

@ -10,21 +10,22 @@
<form id="settings-general" novalidate="novalidate"> <form id="settings-general" novalidate="novalidate">
<fieldset> <fieldset>
<div class="form-group"> {{#gh-form-group errors=model.errors property="title"}}
<label for="blog-title">Blog Title</label> <label for="blog-title">Blog Title</label>
{{input id="blog-title" class="gh-input" name="general[title]" type="text" value=model.title}} {{gh-input id="blog-title" class="gh-input" name="general[title]" type="text" value=model.title focusOut=(action "validate" "title")}}
{{gh-error-message errors=model.errors property="title"}}
<p>The name of your blog</p> <p>The name of your blog</p>
</div> {{/gh-form-group}}
<div class="form-group description-container"> {{#gh-form-group class="description-container" errors=model.errors property="description"}}
<label for="blog-description">Blog Description</label> <label for="blog-description">Blog Description</label>
{{textarea id="blog-description" class="gh-input" name="general[description]" value=model.description}} {{gh-textarea id="blog-description" class="gh-input" name="general[description]" value=model.description focusOut=(action "validate" "description")}}
{{gh-error-message errors=model.errors property="description"}}
<p> <p>
Describe what your blog is about Describe what your blog is about
{{gh-count-characters model.description}} {{gh-count-characters model.description}}
</p> </p>
{{/gh-form-group}}
</div>
</fieldset> </fieldset>
<div class="form-group"> <div class="form-group">
@ -52,7 +53,7 @@
<div class="form-group"> <div class="form-group">
<label for="postsPerPage">Posts per page</label> <label for="postsPerPage">Posts per page</label>
{{! `pattern` brings up numeric keypad allowing any number of digits}} {{! `pattern` brings up numeric keypad allowing any number of digits}}
{{input id="postsPerPage" class="gh-input" name="general[postsPerPage]" focus-out="checkPostsPerPage" value=model.postsPerPage min="1" max="1000" type="number" pattern="[0-9]*"}} {{gh-input id="postsPerPage" class="gh-input" name="general[postsPerPage]" focus-out="checkPostsPerPage" value=model.postsPerPage min="1" max="1000" type="number" pattern="[0-9]*"}}
<p>How many posts should be displayed on each page</p> <p>How many posts should be displayed on each page</p>
</div> </div>
@ -92,10 +93,11 @@
</div> </div>
{{#if model.isPrivate}} {{#if model.isPrivate}}
<div class="form-group"> {{#gh-form-group errors=model.errors property="password"}}
{{input name="general[password]" type="text" value=model.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> <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>
</div> {{/gh-form-group}}
{{/if}} {{/if}}
</fieldset> </fieldset>
</form> </form>

View file

@ -8,6 +8,7 @@
<form class="gh-flow-invite"> <form class="gh-flow-invite">
<label>Enter one email address per line, well handle the rest! <i class="icon-mail"></i></label> <label>Enter one email address per line, well handle the rest! <i class="icon-mail"></i></label>
{{textarea class="gh-input" name="users" value=users required="required"}} {{textarea class="gh-input" name="users" value=users required="required"}}
{{gh-error-message errors=errors property="users"}}
</form> </form>
<button {{action 'invite'}} class="btn btn-default btn-lg btn-block {{buttonClass}}"> <button {{action 'invite'}} class="btn btn-default btn-lg btn-block {{buttonClass}}">

View file

@ -2,17 +2,19 @@
<div class="gh-flow-content-wrap"> <div class="gh-flow-content-wrap">
<section class="gh-flow-content"> <section class="gh-flow-content">
<form id="login" class="gh-signin" method="post" novalidate="novalidate" {{action "validateAndAuthenticate" on="submit"}}> <form id="login" class="gh-signin" method="post" novalidate="novalidate" {{action "validateAndAuthenticate" on="submit"}}>
<div class="form-group"> {{#gh-form-group errors=model.errors property="identification"}}
<span class="input-icon icon-mail"> <span class="input-icon icon-mail">
{{gh-trim-focus-input class="gh-input email" type="email" placeholder="Email Address" name="identification" autocapitalize="off" autocorrect="off" tabindex="1" value=model.identification}} {{gh-trim-focus-input class="gh-input email" type="email" placeholder="Email Address" name="identification" autocapitalize="off" autocorrect="off" tabindex="1" value=model.identification focusOut=(action "validate" "identification")}}
</span> </span>
</div> {{gh-error-message errors=model.errors property="identification"}}
<div class="form-group"> {{/gh-form-group}}
{{#gh-form-group errors=model.errors property="password"}}
<span class="input-icon icon-lock forgotten-wrap"> <span class="input-icon icon-lock forgotten-wrap">
{{input class="gh-input password" type="password" placeholder="Password" name="password" tabindex="2" value=model.password}} {{input class="gh-input password" type="password" placeholder="Password" name="password" tabindex="2" value=model.password focusOut=(action "validate" "password")}}
<button type="button" {{action "forgotten"}} class="forgotten-link btn btn-link" tabindex="4" disabled={{submitting}}>Forgot?</button> <button type="button" {{action "forgotten"}} class="forgotten-link btn btn-link" tabindex="4" disabled={{submitting}}>Forgot?</button>
</span> </span>
</div> {{gh-error-message errors=model.errors property="password"}}
{{/gh-form-group}}
<button id="login-button" class="login btn btn-blue btn-block" type="submit" tabindex="3" disabled={{submitting}}>Sign in</button> <button id="login-button" class="login btn btn-blue btn-block" type="submit" tabindex="3" disabled={{submitting}}>Sign in</button>
</form> </form>

View file

@ -14,26 +14,28 @@
<figure class="account-image"> <figure class="account-image">
<div id="account-image" class="img" style="background-image: url(http://www.gravatar.com/avatar/75e958a6674a7d68fe0d575fb235116c?d=404&s=250)"> <div id="account-image" class="img" style="background-image: url(http://www.gravatar.com/avatar/75e958a6674a7d68fe0d575fb235116c?d=404&s=250)">
<!-- fallback to: Ghost/core/shared/img/ghosticon.jpg --> <!-- fallback to: Ghost/core/shared/img/ghosticon.jpg -->
<span class="sr-only">User imge</span> <span class="sr-only">User image</span>
</div> </div>
<a class="edit-account-image" href="#"><i class="icon-photos "><span class="sr-only">Upload an image</span></i></a> <a class="edit-account-image" href="#"><i class="icon-photos "><span class="sr-only">Upload an image</span></i></a>
</figure> </figure>
<div class="form-group"> {{#gh-form-group errors=model.errors property="email"}}
<label for="email-address">Email address</label> <label for="email-address">Email address</label>
<span class="input-icon icon-mail"> <span class="input-icon icon-mail">
{{input class="gh-input" type="email" name="email" autocorrect="off" value=model.email }} {{gh-input type="email" name="email" placeholder="Eg. john@example.com" class="gh-input" autofocus="autofocus" autocorrect="off" value=model.email focusOut=(action "validate" "email")}}
</span> </span>
</div> {{gh-error-message errors=model.errors property="email"}}
<div class="form-group"> {{/gh-form-group}}
{{#gh-form-group errors=model.errors property="name"}}
<label for="full-name">Full name</label> <label for="full-name">Full name</label>
<span class="input-icon icon-user"> <span class="input-icon icon-user">
{{gh-trim-focus-input class="gh-input" type="text" name="name" autofocus="autofocus" autocorrect="off" value=model.name }} {{gh-input type="text" name="name" placeholder="Eg. John H. Watson" class="gh-input" autofocus="autofocus" autocorrect="off" value=model.name focusOut=(action "validate" "name")}}
</span> </span>
</div> {{gh-error-message errors=model.errors property="name"}}
<div class="form-group"> {{/gh-form-group}}
{{#gh-form-group errors=model.errors property="password"}}
<label for="password">Password</label> <label for="password">Password</label>
<span class="input-icon icon-lock"> <span class="input-icon icon-lock">
{{input class="gh-input" type="password" name="password" autofocus="autofocus" autocorrect="off" value=model.password }} {{input class="gh-input" type="password" name="password" autofocus="autofocus" autocorrect="off" value=model.password focusOut=(action "validate" "password")}}
<div class="pw-strength"> <div class="pw-strength">
<div class="pw-strength-dot"></div> <div class="pw-strength-dot"></div>
<div class="pw-strength-dot"></div> <div class="pw-strength-dot"></div>
@ -42,7 +44,8 @@
<div class="pw-strength-dot <!--pw-strength-activedot-->"></div> <div class="pw-strength-dot <!--pw-strength-activedot-->"></div>
</div> </div>
</span> </span>
</div> {{gh-error-message errors=model.errors property="password"}}
{{/gh-form-group}}
</form> </form>
<button type="submit" class="btn btn-green btn-lg btn-block" {{action "signup"}} disabled={{submitting}}>Create Account</button> <button type="submit" class="btn btn-green btn-lg btn-block" {{action "signup"}} disabled={{submitting}}>Create Account</button>

View file

@ -53,32 +53,39 @@
<button type="button" {{action "openModal" "upload" user "image"}} class="edit-user-image js-modal-image">Edit Picture</button> <button type="button" {{action "openModal" "upload" user "image"}} class="edit-user-image js-modal-image">Edit Picture</button>
</figure> </figure>
<div class="form-group first-form-group"> {{#gh-form-group class="first-form-group" errors=user.errors property="name"}}
<label for="user-name">Full Name</label> <label for="user-name">Full Name</label>
{{input value=user.name id="user-name" class="gh-input user-name" placeholder="Full Name" autocorrect="off"}} {{input value=user.name id="user-name" class="gh-input user-name" placeholder="Full Name" autocorrect="off" focusOut=(action "validate" "name")}}
<p>Use your real name so people can recognise you</p> {{#if user.errors.name}}
</div> {{gh-error-message errors=user.errors property="name"}}
{{else}}
<p>Use your real name so people can recognise you</p>
{{/if}}
{{/gh-form-group}}
</fieldset> </fieldset>
<fieldset class="user-details-bottom"> <fieldset class="user-details-bottom">
<div class="form-group"> {{#gh-form-group errors=user.errors property="slug"}}
<label for="user-slug">Slug</label> <label for="user-slug">Slug</label>
{{gh-input class="gh-input user-name" id="user-slug" value=slugValue name="user" focus-out="updateSlug" placeholder="Slug" selectOnClick="true" autocorrect="off"}} {{gh-input class="gh-input user-name" id="user-slug" value=slugValue name="user" focus-out="updateSlug" placeholder="Slug" selectOnClick="true" autocorrect="off"}}
<p>{{gh-blog-url}}/author/{{slugValue}}</p> <p>{{gh-blog-url}}/author/{{slugValue}}</p>
</div> {{gh-error-message errors=user.errors property="slug"}}
{{/gh-form-group}}
<div class="form-group"> {{#gh-form-group errors=user.errors property="email"}}
<label for="user-email">Email</label> <label for="user-email">Email</label>
{{!-- Administrators only see text of Owner's email address but not input --}} {{!-- Administrators only see text of Owner's email address but not input --}}
{{#unless isAdminUserOnOwnerProfile}} {{#unless isAdminUserOnOwnerProfile}}
{{input type="email" value=user.email id="user-email" class="gh-input" placeholder="Email Address" autocapitalize="off" autocorrect="off" autocomplete="off"}} {{input type="email" value=user.email id="user-email" name="email" class="gh-input" placeholder="Email Address" autocapitalize="off" autocorrect="off" autocomplete="off" focusOut=(action "validate" "email")}}
{{gh-error-message errors=user.errors property="email"}}
{{else}} {{else}}
<span>{{user.email}}</span> <span>{{user.email}}</span>
{{/unless}} {{/unless}}
<p>Used for notifications</p> <p>Used for notifications</p>
</div> {{/gh-form-group}}
{{#if rolesDropdownIsVisible}} {{#if rolesDropdownIsVisible}}
<div class="form-group"> <div class="form-group">
<label for="user-role">Role</label> <label for="user-role">Role</label>
@ -94,26 +101,30 @@
<p>What permissions should this user have?</p> <p>What permissions should this user have?</p>
</div> </div>
{{/if}} {{/if}}
<div class="form-group">
{{#gh-form-group errors=user.errors property="location"}}
<label for="user-location">Location</label> <label for="user-location">Location</label>
{{input type="text" value=user.location id="user-location" class="gh-input"}} {{input type="text" value=user.location id="user-location" class="gh-input" focusOut=(action "validate" "location")}}
{{gh-error-message errors=user.errors property="location"}}
<p>Where in the world do you live?</p> <p>Where in the world do you live?</p>
</div> {{/gh-form-group}}
<div class="form-group"> {{#gh-form-group errors=user.errors property="website"}}
<label for="user-website">Website</label> <label for="user-website">Website</label>
{{input type="url" value=user.website id="user-website" class="gh-input" autocapitalize="off" autocorrect="off" autocomplete="off"}} {{input type="url" value=user.website id="user-website" class="gh-input" autocapitalize="off" autocorrect="off" autocomplete="off" focusOut=(action "validate" "website")}}
{{gh-error-message errors=user.errors property="website"}}
<p>Have a website or blog other than this one? Link it!</p> <p>Have a website or blog other than this one? Link it!</p>
</div> {{/gh-form-group}}
<div class="form-group bio-container"> {{#gh-form-group class="bio-container" errors=user.errors property="bio"}}
<label for="user-bio">Bio</label> <label for="user-bio">Bio</label>
{{textarea id="user-bio" class="gh-input" value=user.bio}} {{textarea id="user-bio" class="gh-input" value=user.bio focusOut=(action "validate" "bio")}}
{{gh-error-message errors=user.errors property="bio"}}
<p> <p>
Write about you, in 200 characters or less. Write about you, in 200 characters or less.
{{gh-count-characters user.bio}} {{gh-count-characters user.bio}}
</p> </p>
</div> {{/gh-form-group}}
<hr /> <hr />

View file

@ -5,7 +5,10 @@ var ResetValidator = BaseValidator.create({
var p1 = model.get('newPassword'), var p1 = model.get('newPassword'),
p2 = model.get('ne2Password'); p2 = model.get('ne2Password');
if (!validator.isLength(p1, 8)) { if (validator.empty(p1)) {
model.get('errors').add('newPassword', 'Please enter a password.');
this.invalidate();
} else if (!validator.isLength(p1, 8)) {
model.get('errors').add('newPassword', 'The password is not long enough.'); model.get('errors').add('newPassword', 'The password is not long enough.');
this.invalidate(); this.invalidate();
} else if (!validator.equals(p1, p2)) { } else if (!validator.equals(p1, p2)) {

View file

@ -20,7 +20,7 @@ var SettingValidator = BaseValidator.create({
}, },
password: function (model) { password: function (model) {
var isPrivate = model.get('isPrivate'), var isPrivate = model.get('isPrivate'),
password = this.get('password'); password = model.get('password');
if (isPrivate && password === '') { if (isPrivate && password === '') {
model.get('errors').add('password', 'Password must be supplied'); model.get('errors').add('password', 'Password must be supplied');

View file

@ -7,6 +7,7 @@ var SigninValidator = BaseValidator.create({
if (validator.empty(id)) { if (validator.empty(id)) {
model.get('errors').add('identification', 'Please enter an email'); model.get('errors').add('identification', 'Please enter an email');
this.invalidate();
} else if (!validator.isEmail(id)) { } else if (!validator.isEmail(id)) {
model.get('errors').add('identification', 'Invalid email'); model.get('errors').add('identification', 'Invalid email');
this.invalidate(); this.invalidate();

View file

@ -10,7 +10,10 @@ var UserValidator = BaseValidator.create({
var name = model.get('name'); var name = model.get('name');
if (this.isActive(model)) { if (this.isActive(model)) {
if (!validator.isLength(name, 0, 150)) { if (validator.empty(name)) {
model.get('errors').add('name', 'Please enter a name.');
this.invalidate();
} else if (!validator.isLength(name, 0, 150)) {
model.get('errors').add('name', 'Name is too long'); model.get('errors').add('name', 'Name is too long');
this.invalidate(); this.invalidate();
} }

View file

@ -26,6 +26,7 @@
"password-generator": "git://github.com/bermi/password-generator#49accd7", "password-generator": "git://github.com/bermi/password-generator#49accd7",
"rangyinputs": "1.2.0", "rangyinputs": "1.2.0",
"showdown-ghost": "0.3.6", "showdown-ghost": "0.3.6",
"sinonjs": "1.14.1",
"validator-js": "3.39.0", "validator-js": "3.39.0",
"xregexp": "2.0.0" "xregexp": "2.0.0"
} }

View file

@ -37,6 +37,7 @@
"ember-data": "1.0.0-beta.18", "ember-data": "1.0.0-beta.18",
"ember-export-application-global": "^1.0.2", "ember-export-application-global": "^1.0.2",
"ember-myth": "0.1.0", "ember-myth": "0.1.0",
"ember-sinon": "0.2.1",
"fs-extra": "0.16.3", "fs-extra": "0.16.3",
"glob": "^4.0.5" "glob": "^4.0.5"
}, },

View file

@ -0,0 +1,71 @@
/* jshint expr:true */
import { expect } from 'chai';
import {
describeComponent,
it
}
from 'ember-mocha';
import sinon from 'sinon';
describeComponent(
'gh-alert',
'GhAlertComponent', {
// specify the other units that are required for this test
// needs: ['component:foo', 'helper:bar']
},
function () {
it('renders', function () {
// creates the component instance
var component = this.subject();
expect(component._state).to.equal('preRender');
component.set('message', {message: 'Test message', type: 'success'});
// renders the component on the page
this.render();
expect(component._state).to.equal('inDOM');
expect(this.$().prop('tagName')).to.equal('ARTICLE');
expect(this.$().hasClass('gh-alert')).to.be.true;
expect(this.$().text()).to.match(/Test message/);
});
it('maps success alert type to correct class', function () {
var component = this.subject();
component.set('message', {message: 'Test message', type: 'success'});
expect(this.$().hasClass('gh-alert-green')).to.be.true;
});
it('maps error alert type to correct class', function () {
var component = this.subject();
component.set('message', {message: 'Test message', type: 'error'});
expect(this.$().hasClass('gh-alert-red')).to.be.true;
});
it('maps warn alert type to correct class', function () {
var component = this.subject();
component.set('message', {message: 'Test message', type: 'warn'});
expect(this.$().hasClass('gh-alert-yellow')).to.be.true;
});
it('maps info alert type to correct class', function () {
var component = this.subject();
component.set('message', {message: 'Test message', type: 'info'});
expect(this.$().hasClass('gh-alert-blue')).to.be.true;
});
it('closes notification through notifications service', function () {
var component = this.subject(),
notifications = {},
notification = {message: 'Test close', type: 'success'};
notifications.closeNotification = sinon.spy();
component.set('notifications', notifications);
component.set('message', notification);
this.$().find('button').click();
expect(notifications.closeNotification.calledWith(notification)).to.be.true;
});
}
);

View file

@ -0,0 +1,63 @@
/* jshint expr:true */
import Ember from 'ember';
import { expect } from 'chai';
import {
describeComponent,
it
}
from 'ember-mocha';
import sinon from 'sinon';
describeComponent(
'gh-alerts',
'GhAlertsComponent', {
// specify the other units that are required for this test
needs: ['component:gh-alert']
},
function () {
beforeEach(function () {
// Stub the notifications service
var notifications = Ember.Object.create();
notifications.alerts = Ember.A();
notifications.alerts.pushObject({message: 'First', type: 'error'});
notifications.alerts.pushObject({message: 'Second', type: 'warn'});
this.subject().set('notifications', notifications);
});
it('renders', function () {
// creates the component instance
var component = this.subject();
expect(component._state).to.equal('preRender');
// renders the component on the page
this.render();
expect(component._state).to.equal('inDOM');
expect(this.$().prop('tagName')).to.equal('ASIDE');
expect(this.$().hasClass('gh-alerts')).to.be.true;
expect(this.$().children().length).to.equal(2);
Ember.run(function () {
component.set('notifications.alerts', Ember.A());
});
expect(this.$().children().length).to.equal(0);
});
it('triggers "notify" action when message count changes', function () {
var component = this.subject();
component.sendAction = sinon.spy();
component.get('notifications.alerts')
.pushObject({message: 'New alert', type: 'info'});
expect(component.sendAction.calledWith('notify', 3)).to.be.true;
component.set('notifications.alerts', Ember.A());
expect(component.sendAction.calledWith('notify', 0)).to.be.true;
});
}
);

View file

@ -0,0 +1,82 @@
/* jshint expr:true */
import { expect } from 'chai';
import {
describeComponent,
it
}
from 'ember-mocha';
import sinon from 'sinon';
describeComponent(
'gh-notification',
'GhNotificationComponent', {
// specify the other units that are required for this test
// needs: ['component:foo', 'helper:bar']
},
function () {
it('renders', function () {
// creates the component instance
var component = this.subject();
expect(component._state).to.equal('preRender');
component.set('message', {message: 'Test message', type: 'success'});
// renders the component on the page
this.render();
expect(component._state).to.equal('inDOM');
expect(this.$().prop('tagName')).to.equal('ARTICLE');
expect(this.$().is('.gh-notification, .gh-notification-passive')).to.be.true;
expect(this.$().text()).to.match(/Test message/);
});
it('maps success alert type to correct class', function () {
var component = this.subject();
component.set('message', {message: 'Test message', type: 'success'});
expect(this.$().hasClass('gh-notification-green')).to.be.true;
});
it('maps error alert type to correct class', function () {
var component = this.subject();
component.set('message', {message: 'Test message', type: 'error'});
expect(this.$().hasClass('gh-notification-red')).to.be.true;
});
it('maps warn alert type to correct class', function () {
var component = this.subject();
component.set('message', {message: 'Test message', type: 'warn'});
expect(this.$().hasClass('gh-notification-yellow')).to.be.true;
});
it('closes notification through notifications service', function () {
var component = this.subject(),
notifications = {},
notification = {message: 'Test close', type: 'success'};
notifications.closeNotification = sinon.spy();
component.set('notifications', notifications);
component.set('message', notification);
this.$().find('button').click();
expect(notifications.closeNotification.calledWith(notification)).to.be.true;
});
it('closes notification when animationend event is triggered', function (done) {
var component = this.subject(),
notifications = {},
notification = {message: 'Test close', type: 'success'};
notifications.closeNotification = sinon.spy();
component.set('notifications', notifications);
component.set('message', notification);
// shorten the animation delay to speed up test
this.$().css('animation-delay', '0.1s');
setTimeout(function () {
expect(notifications.closeNotification.calledWith(notification)).to.be.true;
done();
}, 150);
});
}
);

View file

@ -0,0 +1,47 @@
/* jshint expr:true */
import Ember from 'ember';
import { expect } from 'chai';
import {
describeComponent,
it
}
from 'ember-mocha';
describeComponent(
'gh-notifications',
'GhNotificationsComponent', {
// specify the other units that are required for this test
needs: ['component:gh-notification']
},
function () {
beforeEach(function () {
// Stub the notifications service
var notifications = Ember.Object.create();
notifications.notifications = Ember.A();
notifications.notifications.pushObject({message: 'First', type: 'error'});
notifications.notifications.pushObject({message: 'Second', type: 'warn'});
this.subject().set('notifications', notifications);
});
it('renders', function () {
// creates the component instance
var component = this.subject();
expect(component._state).to.equal('preRender');
// renders the component on the page
this.render();
expect(component._state).to.equal('inDOM');
expect(this.$().prop('tagName')).to.equal('ASIDE');
expect(this.$().hasClass('gh-notifications')).to.be.true;
expect(this.$().children().length).to.equal(2);
Ember.run(function () {
component.set('notifications.notifications', Ember.A());
});
expect(this.$().children().length).to.equal(0);
});
}
);

View file

@ -0,0 +1,286 @@
/* jshint expr:true */
import Ember from 'ember';
import sinon from 'sinon';
import { expect } from 'chai';
import {
describeModule,
it
} from 'ember-mocha';
describeModule(
'service:notifications',
'NotificationsService',
{
// Specify the other units that are required for this test.
// needs: ['model:notification']
},
function () {
beforeEach(function () {
this.subject().set('content', Ember.A());
this.subject().set('delayedNotifications', Ember.A());
});
it('filters alerts/notifications', function () {
var notifications = this.subject();
notifications.set('content', [
{message: 'Alert', status: 'alert'},
{message: 'Notification', status: 'notification'}
]);
expect(notifications.get('alerts'))
.to.deep.equal([{message: 'Alert', status: 'alert'}]);
expect(notifications.get('notifications'))
.to.deep.equal([{message: 'Notification', status: 'notification'}]);
});
it('#handleNotification deals with DS.Notification notifications', function () {
var notifications = this.subject(),
notification = Ember.Object.create({message: '<h1>Test</h1>', status: 'alert'});
notification.toJSON = function () {};
notifications.handleNotification(notification);
notification = notifications.get('alerts')[0];
// alerts received from the server should be marked html safe
expect(notification.get('message')).to.have.property('toHTML');
});
it('#handleNotification defaults to notification if no status supplied', function () {
var notifications = this.subject();
notifications.handleNotification({message: 'Test'}, false);
expect(notifications.get('content'))
.to.deep.include({message: 'Test', status: 'notification'});
});
it('#showAlert adds POJO alerts', function () {
var notifications = this.subject();
notifications.showAlert('Test Alert', {type: 'error'});
expect(notifications.get('alerts'))
.to.deep.include({message: 'Test Alert', status: 'alert', type: 'error'});
});
it('#showAlert adds delayed notifications', function () {
var notifications = this.subject();
notifications.showNotification('Test Alert', {type: 'error', delayed: true});
expect(notifications.get('delayedNotifications'))
.to.deep.include({message: 'Test Alert', status: 'notification', type: 'error'});
});
it('#showNotification adds POJO notifications', function () {
var notifications = this.subject();
notifications.showNotification('Test Notification', {type: 'success'});
expect(notifications.get('notifications'))
.to.deep.include({message: 'Test Notification', status: 'notification', type: 'success'});
});
it('#showNotification adds delayed notifications', function () {
var notifications = this.subject();
notifications.showNotification('Test Notification', {delayed: true});
expect(notifications.get('delayedNotifications'))
.to.deep.include({message: 'Test Notification', status: 'notification', type: undefined});
});
it('#showNotification clears existing notifications', function () {
var notifications = this.subject();
notifications.showNotification('First');
notifications.showNotification('Second');
expect(notifications.get('content.length')).to.equal(1);
expect(notifications.get('content'))
.to.deep.equal([{message: 'Second', status: 'notification', type: undefined}]);
});
it('#showNotification keeps existing notifications if doNotCloseNotifications option passed', function () {
var notifications = this.subject();
notifications.showNotification('First');
notifications.showNotification('Second', {doNotCloseNotifications: true});
expect(notifications.get('content.length')).to.equal(2);
});
// TODO: review whether this can be removed once it's no longer used by validations
it('#showErrors adds multiple notifications', function () {
var notifications = this.subject();
notifications.showErrors([
{message: 'First'},
{message: 'Second'}
]);
expect(notifications.get('content')).to.deep.equal([
{message: 'First', status: 'notification', type: 'error'},
{message: 'Second', status: 'notification', type: 'error'}
]);
});
it('#showAPIError adds single json response error', function () {
var notifications = this.subject(),
resp = {jqXHR: {responseJSON: {error: 'Single error'}}};
notifications.showAPIError(resp);
expect(notifications.get('content')).to.deep.equal([
{message: 'Single error', status: 'alert', type: 'error'}
]);
});
// used to display validation errors returned from the server
it('#showAPIError adds multiple json response errors', function () {
var notifications = this.subject(),
resp = {jqXHR: {responseJSON: {errors: ['First error', 'Second error']}}};
notifications.showAPIError(resp);
expect(notifications.get('content')).to.deep.equal([
{message: 'First error', status: 'notification', type: 'error'},
{message: 'Second error', status: 'notification', type: 'error'}
]);
});
it('#showAPIError adds single json response message', function () {
var notifications = this.subject(),
resp = {jqXHR: {responseJSON: {message: 'Single message'}}};
notifications.showAPIError(resp);
expect(notifications.get('content')).to.deep.equal([
{message: 'Single message', status: 'alert', type: 'error'}
]);
});
it('#showAPIError displays default error text if response has no error/message', function () {
var notifications = this.subject(),
resp = {};
notifications.showAPIError(resp);
expect(notifications.get('content')).to.deep.equal([
{message: 'There was a problem on the server, please try again.', status: 'alert', type: 'error'}
]);
notifications.set('content', Ember.A());
notifications.showAPIError(resp, {defaultErrorText: 'Overridden default'});
expect(notifications.get('content')).to.deep.equal([
{message: 'Overridden default', status: 'alert', type: 'error'}
]);
});
it('#displayDelayed moves delayed notifications into content', function () {
var notifications = this.subject();
notifications.showNotification('First', {delayed: true});
notifications.showNotification('Second', {delayed: true});
notifications.showNotification('Third', {delayed: false});
notifications.displayDelayed();
expect(notifications.get('content')).to.deep.equal([
{message: 'Third', status: 'notification', type: undefined},
{message: 'First', status: 'notification', type: undefined},
{message: 'Second', status: 'notification', type: undefined}
]);
});
it('#closeNotification removes POJO notifications', function () {
var notification = {message: 'Close test', status: 'notification'},
notifications = this.subject();
notifications.handleNotification(notification);
expect(notifications.get('notifications'))
.to.include(notification);
notifications.closeNotification(notification);
expect(notifications.get('notifications'))
.to.not.include(notification);
});
it('#closeNotification removes and deletes DS.Notification records', function () {
var notification = Ember.Object.create({message: 'Close test', status: 'alert'}),
notifications = this.subject();
notification.toJSON = function () {};
notification.deleteRecord = function () {};
sinon.spy(notification, 'deleteRecord');
notification.save = function () {
return {
finally: function (callback) { return callback(notification); }
};
};
sinon.spy(notification, 'save');
notifications.handleNotification(notification);
expect(notifications.get('alerts')).to.include(notification);
notifications.closeNotification(notification);
expect(notification.deleteRecord.calledOnce).to.be.true;
expect(notification.save.calledOnce).to.be.true;
// wrap in runloop so filter updates
Ember.run.next(function () {
expect(notifications.get('alerts')).to.not.include(notification);
});
});
it('#closeNotifications only removes notifications', function () {
var notifications = this.subject();
notifications.showAlert('First alert');
notifications.showNotification('First notification');
notifications.showNotification('Second notification', {doNotCloseNotifications: true});
expect(notifications.get('alerts.length')).to.equal(1);
expect(notifications.get('notifications.length')).to.equal(2);
notifications.closeNotifications();
// wrap in runloop so filter updates
Ember.run.next(function () {
expect(notifications.get('alerts.length')).to.equal(1);
expect(notifications.get('notifications.length')).to.equal(1);
});
});
it('#closeAll removes everything without deletion', function () {
var notifications = this.subject(),
notificationModel = Ember.Object.create({message: 'model'});
notificationModel.toJSON = function () {};
notificationModel.deleteRecord = function () {};
sinon.spy(notificationModel, 'deleteRecord');
notificationModel.save = function () {
return {
finally: function (callback) { return callback(notificationModel); }
};
};
sinon.spy(notificationModel, 'save');
notifications.handleNotification(notificationModel);
notifications.handleNotification({message: 'pojo'});
notifications.closeAll();
expect(notifications.get('content')).to.be.empty;
expect(notificationModel.deleteRecord.called).to.be.false;
expect(notificationModel.save.called).to.be.false;
});
}
);

View file

@ -50,7 +50,7 @@ notifications = {
var defaults = { var defaults = {
dismissible: true, dismissible: true,
location: 'bottom', location: 'bottom',
status: 'persistent' status: 'alert'
}, },
addedNotifications = []; addedNotifications = [];
@ -61,7 +61,7 @@ notifications = {
notification = _.assign(defaults, notification, { notification = _.assign(defaults, notification, {
id: notificationCounter id: notificationCounter
// status: 'persistent' // status: 'alert'
}); });
notificationsStore.push(notification); notificationsStore.push(notification);

View file

@ -36,7 +36,7 @@ adminControllers = {
type: 'upgrade', type: 'upgrade',
location: 'settings-about-upgrade', location: 'settings-about-upgrade',
dismissible: false, dismissible: false,
status: 'persistent', status: 'alert',
message: 'Ghost ' + updateVersion + ' is available! Hot Damn. <a href="http://support.ghost.org/how-to-upgrade/" target="_blank">Click here</a> to upgrade.' message: 'Ghost ' + updateVersion + ' is available! Hot Damn. <a href="http://support.ghost.org/how-to-upgrade/" target="_blank">Click here</a> to upgrade.'
}; };

View file

@ -411,9 +411,9 @@ CasperTest.Routines = (function () {
casper.captureScreenshot('setting_up2.png'); casper.captureScreenshot('setting_up2.png');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() { casper.waitForSelectorTextChange('.gh-alert-success', function onSuccess() {
var errorText = casper.evaluate(function () { var errorText = casper.evaluate(function () {
return document.querySelector('.notification-error').innerText; return document.querySelector('.gh-alert').innerText;
}); });
casper.echoConcise('Setup failed. Error text: ' + errorText); casper.echoConcise('Setup failed. Error text: ' + errorText);
}, function onTimeout() { }, function onTimeout() {

View file

@ -26,7 +26,7 @@ CasperTest.begin('Ghost editor functions correctly', 16, function suite(test) {
casper.thenClick('.js-publish-button'); casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success', function onSuccess() { casper.waitForSelector('.gh-notification', function onSuccess() {
test.assert(true, 'Can save with no title.'); test.assert(true, 'Can save with no title.');
test.assertEvalEquals(function () { test.assertEvalEquals(function () {
return document.getElementById('entry-title').value; return document.getElementById('entry-title').value;
@ -58,7 +58,7 @@ CasperTest.begin('Ghost editor functions correctly', 16, function suite(test) {
casper.thenClick('.js-publish-button'); casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success', function onSuccess() { casper.waitForSelector('.gh-notification', function onSuccess() {
test.assertUrlMatch(/ghost\/editor\/\d+\/$/, 'got an id on our URL'); test.assertUrlMatch(/ghost\/editor\/\d+\/$/, 'got an id on our URL');
test.assertEvalEquals(function () { test.assertEvalEquals(function () {
return document.querySelector('#entry-title').value; return document.querySelector('#entry-title').value;
@ -223,7 +223,7 @@ CasperTest.begin('Image Uploads', 23, function suite(test) {
// Save the post with the image // Save the post with the image
casper.thenClick('.js-publish-button'); casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success', function onSuccess() { casper.waitForSelector('.gh-notification', function onSuccess() {
test.assertUrlMatch(/ghost\/editor\/\d+\/$/, 'got an id on our URL'); test.assertUrlMatch(/ghost\/editor\/\d+\/$/, 'got an id on our URL');
}, casper.failOnTimeout(test, 'Post was not successfully created')); }, casper.failOnTimeout(test, 'Post was not successfully created'));
@ -385,7 +385,7 @@ CasperTest.begin('Publish menu - existing post', 23, function suite(test) {
// Create a post in draft status // Create a post in draft status
casper.thenClick('.js-publish-button'); casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success', function checkPostWasCreated() { casper.waitForSelector('.gh-notification', function checkPostWasCreated() {
test.assertUrlMatch(/ghost\/editor\/\d+\/$/, 'got an id on our URL'); test.assertUrlMatch(/ghost\/editor\/\d+\/$/, 'got an id on our URL');
}); });
@ -429,7 +429,7 @@ CasperTest.begin('Publish menu - existing post', 23, function suite(test) {
// Do publish // Do publish
casper.thenClick('.js-publish-button'); casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success', function checkPostWasCreated() { casper.waitForSelector('.gh-notification', function checkPostWasCreated() {
test.assertUrlMatch(/ghost\/editor\/\d+\/$/, 'got an id on our URL'); test.assertUrlMatch(/ghost\/editor\/\d+\/$/, 'got an id on our URL');
}); });
@ -461,7 +461,7 @@ CasperTest.begin('Publish menu - existing post', 23, function suite(test) {
// Do unpublish // Do unpublish
casper.thenClick('.js-publish-button'); casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success', function checkPostWasCreated() { casper.waitForSelector('.gh-notification', function checkPostWasCreated() {
// ... check status, label, class // ... check status, label, class
casper.waitForSelector('.js-publish-splitbutton', function onSuccess() { casper.waitForSelector('.js-publish-splitbutton', function onSuccess() {
test.assertExists('.js-publish-button.btn-blue', 'Publish button should have .btn-blue'); test.assertExists('.js-publish-button.btn-blue', 'Publish button should have .btn-blue');
@ -472,7 +472,7 @@ CasperTest.begin('Publish menu - existing post', 23, function suite(test) {
}); });
}); });
CasperTest.begin('Publish menu - delete post', 7, function testDeleteModal(test) { CasperTest.begin('Publish menu - delete post', 6, function testDeleteModal(test) {
// Create a post that can be deleted // Create a post that can be deleted
CasperTest.Routines.createTestPost.run(false); CasperTest.Routines.createTestPost.run(false);
@ -515,15 +515,8 @@ CasperTest.begin('Publish menu - delete post', 7, function testDeleteModal(test)
// Delete the post // Delete the post
this.click('.modal-content .js-button-accept'); this.click('.modal-content .js-button-accept');
casper.waitForSelector('.notification-success', function onSuccess() { casper.waitWhileVisible('.modal-container', function onSuccess() {
test.assert(true, 'Got success notification from delete post'); test.assert(true, 'clicking delete button should close the delete post modal');
test.assertSelectorHasText(
'.gh-notification-content',
'Your post has been deleted.',
'.gh-notification-content has correct text'
);
}, function onTimeout() {
test.fail('No success notification from delete post');
}); });
}); });
}); });
@ -563,8 +556,7 @@ CasperTest.begin('Publish menu - new post status is correct after failed save',
}); });
}); });
// TODO: Change number of tests back to 6 once the commented-out tests are fixed CasperTest.begin('Publish menu - existing post status is correct after failed save', 6, function suite(test) {
CasperTest.begin('Publish menu - existing post status is correct after failed save', 4, function suite(test) {
casper.thenOpenAndWaitForPageLoad('editor', function testTitleAndUrl() { casper.thenOpenAndWaitForPageLoad('editor', function testTitleAndUrl() {
test.assertTitle('Editor - Test Blog', 'Ghost admin has incorrect title'); test.assertTitle('Editor - Test Blog', 'Ghost admin has incorrect title');
test.assertUrlMatch(/ghost\/editor\/$/, 'Landed on the correct URL'); test.assertUrlMatch(/ghost\/editor\/$/, 'Landed on the correct URL');
@ -579,7 +571,7 @@ CasperTest.begin('Publish menu - existing post status is correct after failed sa
// save // save
casper.thenClick('.js-publish-button'); casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success'); casper.waitForSelector('.gh-notification');
casper.then(function updateTitle() { casper.then(function updateTitle() {
casper.sendKeys('#entry-title', new Array(160).join('y')); casper.sendKeys('#entry-title', new Array(160).join('y'));
@ -607,15 +599,14 @@ CasperTest.begin('Publish menu - existing post status is correct after failed sa
casper.thenClick('.js-publish-button'); casper.thenClick('.js-publish-button');
// ... check status, label, class // ... check status, label, class
// TODO: re-implement these once #5933 is merged casper.waitForSelector('.gh-alert-red', function onSuccess() {
// casper.waitForSelector('.notification-error', function onSuccess() { test.assertExists('.js-publish-button.btn-blue', 'Update button should have .btn-blue');
// test.assertExists('.js-publish-button.btn-blue', 'Update button should have .btn-blue'); // wait for button to settle
// // wait for button to settle casper.wait(500);
// casper.wait(500); test.assertSelectorHasText('.js-publish-button', 'Save Draft', '.js-publish-button says Save Draft');
// test.assertSelectorHasText('.js-publish-button', 'Save Draft', '.js-publish-button says Save Draft'); }, function onTimeout() {
// }, function onTimeout() { test.assert(false, 'Saving post with invalid title should trigger an error');
// test.assert(false, 'Saving post with invalid title should trigger an error'); });
// });
}); });
// test the markdown help modal // test the markdown help modal
@ -660,7 +651,7 @@ CasperTest.begin('Title input is set correctly after using the Post-Settings-Men
// save draft // save draft
casper.thenClick('.js-publish-button'); casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success'); casper.waitForSelector('.gh-notification');
// change the title // change the title
casper.then(function updateTitle() { casper.then(function updateTitle() {
@ -707,7 +698,7 @@ CasperTest.begin('Editor content is set correctly after using the Post-Settings-
// save draft // save draft
casper.thenClick('.js-publish-button'); casper.thenClick('.js-publish-button');
casper.waitForSelector('.notification-success'); casper.waitForSelector('.gh-notification');
// change the content // change the content
casper.then(function updateContent() { casper.then(function updateContent() {

View file

@ -3,7 +3,7 @@
/*globals CasperTest, casper, __utils__ */ /*globals CasperTest, casper, __utils__ */
CasperTest.begin('Post settings menu', 10, function suite(test) { CasperTest.begin('Post settings menu', 8, function suite(test) {
casper.thenOpenAndWaitForPageLoad('editor', function testTitleAndUrl() { casper.thenOpenAndWaitForPageLoad('editor', function testTitleAndUrl() {
test.assertTitle('Editor - Test Blog', 'Ghost admin has incorrect title'); test.assertTitle('Editor - Test Blog', 'Ghost admin has incorrect title');
test.assertUrlMatch(/ghost\/editor\/$/, 'Landed on the correct URL'); test.assertUrlMatch(/ghost\/editor\/$/, 'Landed on the correct URL');
@ -24,16 +24,6 @@ CasperTest.begin('Post settings menu', 10, function suite(test) {
casper.thenClick('.js-publish-button'); casper.thenClick('.js-publish-button');
}); });
casper.waitForSelector('.notification-success', function waitForSuccess() {
test.assert(true, 'got success notification');
test.assertSelectorHasText('.notification-success', 'Saved.', '.notification-success has correct text');
casper.click('.gh-notification-close');
}, function onTimeout() {
test.assert(false, 'No success notification');
});
casper.waitWhileSelector('.notification-success');
casper.thenClick('.post-settings'); casper.thenClick('.post-settings');
casper.waitForOpaque('.settings-menu', function onSuccess() { casper.waitForOpaque('.settings-menu', function onSuccess() {

View file

@ -21,7 +21,7 @@ CasperTest.begin('Settings screen is correct', 5, function suite(test) {
}); });
// ## General settings tests // ## General settings tests
CasperTest.begin('General settings pane is correct', 7, function suite(test) { CasperTest.begin('General settings pane is correct', 4, function suite(test) {
casper.thenOpenAndWaitForPageLoad('settings.general', function testTitleAndUrl() { casper.thenOpenAndWaitForPageLoad('settings.general', function testTitleAndUrl() {
test.assertUrlMatch(/ghost\/settings\/general\/$/, 'Landed on the correct URL'); test.assertUrlMatch(/ghost\/settings\/general\/$/, 'Landed on the correct URL');
}); });
@ -29,9 +29,6 @@ CasperTest.begin('General settings pane is correct', 7, function suite(test) {
function assertImageUploaderModalThenClose() { function assertImageUploaderModalThenClose() {
test.assertSelectorHasText('.description', 'Add image', '.description has the correct text'); test.assertSelectorHasText('.description', 'Add image', '.description has the correct text');
casper.click('.modal-container .js-button-accept'); casper.click('.modal-container .js-button-accept');
casper.waitForSelector('.notification-success', function onSuccess() {
test.assert(true, 'Got success notification');
}, casper.failOnTimeout(test, 'No success notification'));
} }
// Ensure image upload modals display correctly // Ensure image upload modals display correctly
@ -65,27 +62,21 @@ CasperTest.begin('General settings pane is correct', 7, function suite(test) {
// Ensure can save // Ensure can save
casper.waitForSelector('header .btn-blue').then(function () { casper.waitForSelector('header .btn-blue').then(function () {
casper.thenClick('header .btn-blue').waitFor(function successNotification() { casper.thenClick('header .btn-blue');
return this.evaluate(function () { casper.waitForResource('settings/', function onSuccess() {
return document.querySelectorAll('.gh-notification').length > 0; test.assert(true, 'Settings were saved');
});
}, function doneWaiting() { }, function doneWaiting() {
test.pass('Waited for notification'); test.fail('Settings were not saved');
}, casper.failOnTimeout(test, 'Saving the general pane did not result in a notification')); });
}); });
casper.then(function checkSettingsWereSaved() { casper.then(function stopListeningForRequests() {
casper.removeListener('resource.requested', handleSettingsRequest); casper.removeListener('resource.requested', handleSettingsRequest);
}); });
casper.waitForSelector('.notification-success', function onSuccess() {
test.assert(true, 'Got success notification');
}, casper.failOnTimeout(test, 'No success notification :('));
}); });
// ## General settings validations tests // ## General settings validations tests
// // TODO: Change number of tests back to 6 once the commented-out tests are fixed CasperTest.begin('General settings validation is correct', 7, function suite(test) {
CasperTest.begin('General settings validation is correct', 4, function suite(test) {
casper.thenOpenAndWaitForPageLoad('settings.general', function testTitleAndUrl() { casper.thenOpenAndWaitForPageLoad('settings.general', function testTitleAndUrl() {
test.assertTitle('Settings - General - Test Blog', 'Ghost admin has incorrect title'); test.assertTitle('Settings - General - Test Blog', 'Ghost admin has incorrect title');
test.assertUrlMatch(/ghost\/settings\/general\/$/, 'Landed on the correct URL'); test.assertUrlMatch(/ghost\/settings\/general\/$/, 'Landed on the correct URL');
@ -96,24 +87,18 @@ CasperTest.begin('General settings validation is correct', 4, function suite(tes
'general[title]': new Array(152).join('a') 'general[title]': new Array(152).join('a')
}); });
// TODO: re-implement once #5933 is merged casper.waitForText('Title is too long', function onSuccess() {
// casper.waitForSelectorTextChange('.notification-error', function onSuccess() { test.assert(true, 'Blog title length error was shown');
// test.assertSelectorHasText('.notification-error', 'too long', '.notification-error has correct text'); }, casper.failOnTimeout(test, 'Blog title length error did not appear'));
// }, casper.failOnTimeout(test, 'Blog title length error did not appear'), 2000);
casper.thenClick('.gh-notification-close');
// Ensure general blog description field length validation // Ensure general blog description field length validation
casper.fillAndSave('form#settings-general', { casper.fillAndSave('form#settings-general', {
'general[description]': new Array(202).join('a') 'general[description]': new Array(202).join('a')
}); });
// TODO: re-implement once #5933 is merged casper.waitForText('Description is too long', function onSuccess() {
// casper.waitForSelectorTextChange('.notification-error', function onSuccess() { test.assert(true, 'Blog description length error was shown');
// test.assertSelectorHasText('.notification-error', 'too long', '.notification-error has correct text'); }, casper.failOnTimeout(test, 'Blog description length error did not appear'));
// }, casper.failOnTimeout(test, 'Blog description length error did not appear'));
casper.thenClick('.gh-notification-close');
// TODO move these to ember tests, note: async issues - field will be often be null without a casper.wait // TODO move these to ember tests, note: async issues - field will be often be null without a casper.wait
// Check postsPerPage autocorrect // Check postsPerPage autocorrect
@ -136,4 +121,14 @@ CasperTest.begin('General settings validation is correct', 4, function suite(tes
casper.then(function checkSlugInputValue() { casper.then(function checkSlugInputValue() {
test.assertField('general[postsPerPage]', '5', 'posts per page is set correctly'); test.assertField('general[postsPerPage]', '5', 'posts per page is set correctly');
}); });
// Ensure private blog password validation
casper.fillAndSave('form#settings-general', {
'general[isPrivate]': '1',
'general[password]': ''
});
casper.waitForText('Password must be supplied', function onSuccess() {
test.assert(true, 'Password required error was shown');
}, casper.failOnTimeout(test, 'Password required error did not appear'));
}); });

View file

@ -52,7 +52,7 @@ CasperTest.begin('Login limit is in place', 4, function suite(test) {
casper.waitForText('remaining', function onSuccess() { casper.waitForText('remaining', function onSuccess() {
test.assert(true, 'The login limit is in place.'); test.assert(true, 'The login limit is in place.');
test.assertSelectorDoesntHaveText('.notification-error', '[object Object]'); test.assertSelectorDoesntHaveText('.gh-alert', '[object Object]');
}, function onTimeout() { }, function onTimeout() {
test.assert(false, 'We did not trip the login limit.'); test.assert(false, 'We did not trip the login limit.');
}); });
@ -110,8 +110,7 @@ CasperTest.begin('Authenticated user is redirected', 6, function suite(test) {
}); });
}, true); }, true);
// TODO: Change number of tests back to 4 once the commented-out tests are fixed CasperTest.begin('Ensure email field form validation', 4, function suite(test) {
CasperTest.begin('Ensure email field form validation', 2, function suite(test) {
CasperTest.Routines.signout.run(test); CasperTest.Routines.signout.run(test);
casper.thenOpenAndWaitForPageLoad('signin', function testTitleAndUrl() { casper.thenOpenAndWaitForPageLoad('signin', function testTitleAndUrl() {
@ -129,21 +128,17 @@ CasperTest.begin('Ensure email field form validation', 2, function suite(test) {
test.fail('Login form didn\'t fade in.'); test.fail('Login form didn\'t fade in.');
}); });
// casper.waitForSelectorTextChange('.notification-error', function onSuccess() { casper.waitForText('Invalid email', function onSuccess() {
// test.assertSelectorHasText('.notification-error', 'Invalid Email', '.notification-error text is correct'); test.assert(true, 'Invalid email error was shown');
// }, function onTimeout() { }, casper.failOnTimeout(test, 'Invalid email error was not shown'));
// test.fail('Email validation error did not appear');
// }, 2000); casper.then(function testMissingEmail() {
// this.fillAndSave('form.gh-signin', {
// casper.then(function testMissingEmail() { identification: ''
// this.fillAndSave('form.gh-signin', { });
// identification: '' });
// });
// }); casper.waitForText('Please enter an email', function onSuccess() {
// test.assert(true, 'Missing email error was shown');
// casper.waitForSelectorTextChange('.notification-error', function onSuccess() { }, casper.failOnTimeout(test, 'Missing email error was not shown'));
// test.assertSelectorHasText('.notification-error', 'Please enter an email', '.notification-error text is correct');
// }, function onTimeout() {
// test.fail('Missing Email validation error did not appear');
// }, 2000);
}, true); }, true);

View file

@ -71,7 +71,7 @@ CasperTest.begin('Users screen is correct', 9, function suite(test) {
}); });
// ### User settings tests // ### User settings tests
CasperTest.begin('Can save settings', 7, function suite(test) { CasperTest.begin('Can save settings', 5, function suite(test) {
casper.thenOpenAndWaitForPageLoad('team.user', function testTitleAndUrl() { casper.thenOpenAndWaitForPageLoad('team.user', function testTitleAndUrl() {
test.assertTitle('Team - User - Test Blog', 'Ghost Admin title is correct'); test.assertTitle('Team - User - Test Blog', 'Ghost Admin title is correct');
test.assertUrlMatch(/ghost\/team\/test\/$/, 'team.user has correct URL'); test.assertUrlMatch(/ghost\/team\/test\/$/, 'team.user has correct URL');
@ -96,22 +96,16 @@ CasperTest.begin('Can save settings', 7, function suite(test) {
}); });
casper.thenClick('.btn-blue'); casper.thenClick('.btn-blue');
casper.waitFor(function successNotification() { casper.waitForResource(/\/users\/\d\/\?include=roles/, function onSuccess() {
return this.evaluate(function () { test.assert(true, 'Saving the user pane triggered a save request');
return document.querySelectorAll('.gh-notification').length > 0;
});
}, function doneWaiting() { }, function doneWaiting() {
test.pass('Waited for notification'); test.fail('Saving the user pane did not trigger a save request');
}, casper.failOnTimeout(test, 'Saving the user pane did not result in a notification')); });
casper.then(function checkUserWasSaved() { casper.then(function checkUserWasSaved() {
casper.removeListener('resource.requested', handleUserRequest); casper.removeListener('resource.requested', handleUserRequest);
}); });
casper.waitForSelector('.notification-success', function onSuccess() {
test.assert(true, 'Got success notification');
}, casper.failOnTimeout(test, 'No success notification :('));
casper.thenClick('.gh-nav-settings-general').then(function testTransitionToGeneral() { casper.thenClick('.gh-nav-settings-general').then(function testTransitionToGeneral() {
casper.waitForSelector(generalTabDetector, function then() { casper.waitForSelector(generalTabDetector, function then() {
casper.on('resource.requested', handleSettingsRequest); casper.on('resource.requested', handleSettingsRequest);
@ -122,22 +116,17 @@ CasperTest.begin('Can save settings', 7, function suite(test) {
casper.failOnTimeout(test, 'waitForSelector `usersTabDetector` timed out')); casper.failOnTimeout(test, 'waitForSelector `usersTabDetector` timed out'));
}); });
casper.thenClick('.btn-blue').waitFor(function successNotification() { casper.thenClick('.btn-blue');
return this.evaluate(function () { casper.waitForResource(/\/users\/\d\/\?include=roles/, function onSuccess() {
return document.querySelectorAll('.gh-notification').length > 0; test.assert(true, 'Saving the user pane triggered a save request');
});
}, function doneWaiting() { }, function doneWaiting() {
test.pass('Waited for notification'); test.fail('Saving the user pane did not trigger a save request');
}, casper.failOnTimeout(test, 'Saving the general pane did not result in a notification')); });
casper.then(function checkSettingsWereSaved() { casper.then(function checkSettingsWereSaved() {
casper.removeListener('resource.requested', handleSettingsRequest); casper.removeListener('resource.requested', handleSettingsRequest);
}); });
casper.waitForSelector('.notification-success', function onSuccess() {
test.assert(true, 'Got success notification');
}, casper.failOnTimeout(test, 'No success notification :('));
CasperTest.beforeDone(function () { CasperTest.beforeDone(function () {
casper.removeListener('resource.requested', handleUserRequest); casper.removeListener('resource.requested', handleUserRequest);
casper.removeListener('resource.requested', handleSettingsRequest); casper.removeListener('resource.requested', handleSettingsRequest);
@ -203,7 +192,6 @@ CasperTest.begin('User settings screen change slug handles duplicate slug', 4, f
}); });
}); });
// TODO: Change number of tests back to 6 once the commented-out tests are fixed
CasperTest.begin('User settings screen validates email', 4, function suite(test) { CasperTest.begin('User settings screen validates email', 4, function suite(test) {
var email; var email;
@ -220,21 +208,14 @@ CasperTest.begin('User settings screen validates email', 4, function suite(test)
casper.then(function setEmailToInvalid() { casper.then(function setEmailToInvalid() {
var brokenEmail = email.replace('.', '-'); var brokenEmail = email.replace('.', '-');
this.fillAndSave('.user-profile', {
casper.fillSelectors('.user-profile', { email: brokenEmail
'#user-email': brokenEmail });
}, false);
}); });
casper.thenClick('.btn-blue'); casper.waitForText('Please supply a valid email address', function onSuccess() {
test.assert(true, 'Invalid email error was shown');
casper.waitForResource('/team/'); }, casper.failOnTimeout(test, 'Invalid email error was not shown'));
// TODO: Re-implement after inlin-errors is merged
// casper.waitForSelector('.notification-error', function onSuccess() {
// test.assert(true, 'Got error notification');
// test.assertSelectorDoesntHaveText('.notification-error', '[object Object]', 'notification text is not broken');
// }, casper.failOnTimeout(test, 'No error notification :('));
casper.then(function resetEmailToValid() { casper.then(function resetEmailToValid() {
casper.fillSelectors('.user-profile', { casper.fillSelectors('.user-profile', {
@ -242,14 +223,13 @@ CasperTest.begin('User settings screen validates email', 4, function suite(test)
}, false); }, false);
}); });
casper.then(function checkEmailErrorWasCleared() {
test.assertTextDoesntExist('Please supply a valid email address', 'Invalid email error was not cleared');
});
casper.thenClick('.view-actions .btn-blue'); casper.thenClick('.view-actions .btn-blue');
casper.waitForResource(/users/); casper.waitForResource(/users/);
casper.waitForSelector('.notification-success', function onSuccess() {
test.assert(true, 'Got success notification');
test.assertSelectorDoesntHaveText('.notification-success', '[object Object]', 'notification text is not broken');
}, casper.failOnTimeout(test, 'No success notification :('));
}); });
// TODO: user needs to be loaded whenever it is edited (multi user) // TODO: user needs to be loaded whenever it is edited (multi user)
@ -278,8 +258,7 @@ CasperTest.begin('User settings screen shows remaining characters for Bio proper
}); });
}); });
// TODO: Change number of tests back to 3 once the commented-out tests are fixed CasperTest.begin('Ensure user bio field length validation', 3, function suite(test) {
CasperTest.begin('Ensure user bio field length validation', 2, function suite(test) {
casper.thenOpenAndWaitForPageLoad('team.user', function testTitleAndUrl() { casper.thenOpenAndWaitForPageLoad('team.user', function testTitleAndUrl() {
test.assertTitle('Team - User - Test Blog', 'Ghost admin has incorrect title'); test.assertTitle('Team - User - Test Blog', 'Ghost admin has incorrect title');
test.assertUrlMatch(/ghost\/team\/test\/$/, 'Ghost doesn\'t require login this time'); test.assertUrlMatch(/ghost\/team\/test\/$/, 'Ghost doesn\'t require login this time');
@ -293,14 +272,12 @@ CasperTest.begin('Ensure user bio field length validation', 2, function suite(te
casper.thenClick('.view-actions .btn-blue'); casper.thenClick('.view-actions .btn-blue');
// TODO: re-implement after inline-errors is complete casper.waitForText('Bio is too long', function onSuccess() {
// casper.waitForSelectorTextChange('.notification-error', function onSuccess() { test.assert(true, 'Bio too long error was shown');
// test.assertSelectorHasText('.notification-error', 'is too long', '.notification-error text is correct'); }, casper.failOnTimeout(test, 'Bio too long error was not shown'));
// }, casper.failOnTimeout(test, 'Bio field length error did not appear', 2000));
}); });
// TODO: Change number of tests back to 3 once the commented-out tests are fixed CasperTest.begin('Ensure user url field validation', 3, function suite(test) {
CasperTest.begin('Ensure user url field validation', 2, function suite(test) {
casper.thenOpenAndWaitForPageLoad('team.user', function testTitleAndUrl() { casper.thenOpenAndWaitForPageLoad('team.user', function testTitleAndUrl() {
test.assertTitle('Team - User - Test Blog', 'Ghost admin has incorrect title'); test.assertTitle('Team - User - Test Blog', 'Ghost admin has incorrect title');
test.assertUrlMatch(/ghost\/team\/test\/$/, 'Ghost doesn\'t require login this time'); test.assertUrlMatch(/ghost\/team\/test\/$/, 'Ghost doesn\'t require login this time');
@ -314,14 +291,12 @@ CasperTest.begin('Ensure user url field validation', 2, function suite(test) {
casper.thenClick('.view-actions .btn-blue'); casper.thenClick('.view-actions .btn-blue');
// TODO: re-implement after inline-errors is complete casper.waitForText('Website is not a valid url', function onSuccess() {
// casper.waitForSelectorTextChange('.notification-error', function onSuccess() { test.assert(true, 'Website invalid error was shown');
// test.assertSelectorHasText('.notification-error', 'not a valid url', '.notification-error text is correct'); }, casper.failOnTimeout(test, 'Website invalid error was not shown'));
// }, casper.failOnTimeout(test, 'Url validation error did not appear', 2000));
}); });
// TODO: Change number of tests back to 3 once the commented-out tests are fixed CasperTest.begin('Ensure user location field length validation', 3, function suite(test) {
CasperTest.begin('Ensure user location field length validation', 2, function suite(test) {
casper.thenOpenAndWaitForPageLoad('team.user', function testTitleAndUrl() { casper.thenOpenAndWaitForPageLoad('team.user', function testTitleAndUrl() {
test.assertTitle('Team - User - Test Blog', 'Ghost admin has incorrect title'); test.assertTitle('Team - User - Test Blog', 'Ghost admin has incorrect title');
test.assertUrlMatch(/ghost\/team\/test\/$/, 'Ghost doesn\'t require login this time'); test.assertUrlMatch(/ghost\/team\/test\/$/, 'Ghost doesn\'t require login this time');
@ -335,8 +310,7 @@ CasperTest.begin('Ensure user location field length validation', 2, function sui
casper.thenClick('.view-actions .btn-blue'); casper.thenClick('.view-actions .btn-blue');
// TODO: re-implement after inline-errors is complete casper.waitForText('Location is too long', function onSuccess() {
// casper.waitForSelectorTextChange('.notification-error', function onSuccess() { test.assert(true, 'Location too long error was shown');
// test.assertSelectorHasText('.notification-error', 'is too long', '.notification-error text is correct'); }, casper.failOnTimeout(test, 'Location too long error was not shown'));
// }, casper.failOnTimeout(test, 'Location field length error did not appear', 2000));
}); });

View file

@ -55,7 +55,7 @@ describe('Notifications API', function () {
jsonResponse.notifications[0].type.should.equal(newNotification.type); jsonResponse.notifications[0].type.should.equal(newNotification.type);
jsonResponse.notifications[0].message.should.equal(newNotification.message); jsonResponse.notifications[0].message.should.equal(newNotification.message);
jsonResponse.notifications[0].status.should.equal('persistent'); jsonResponse.notifications[0].status.should.equal('alert');
done(); done();
}); });
@ -66,7 +66,7 @@ describe('Notifications API', function () {
var newNotification = { var newNotification = {
type: 'info', type: 'info',
message: 'test notification', message: 'test notification',
status: 'persistent' status: 'alert'
}; };
it('deletes a notification', function (done) { it('deletes a notification', function (done) {

View file

@ -2,8 +2,7 @@
/*global CasperTest, casper, email, user, password */ /*global CasperTest, casper, email, user, password */
// TODO: change test number to 12 after inline-errors are fixed CasperTest.begin('Ghost setup fails properly', 11, function suite(test) {
CasperTest.begin('Ghost setup fails properly', 10, function suite(test) {
casper.thenOpenAndWaitForPageLoad('setup', function then() { casper.thenOpenAndWaitForPageLoad('setup', function then() {
test.assertUrlMatch(/ghost\/setup\/one\/$/, 'Landed on the correct URL'); test.assertUrlMatch(/ghost\/setup\/one\/$/, 'Landed on the correct URL');
}); });
@ -12,14 +11,10 @@ CasperTest.begin('Ghost setup fails properly', 10, function suite(test) {
casper.fillAndAdd('#setup', {'blog-title': 'ghost', name: 'slimer', email: email, password: 'short'}); casper.fillAndAdd('#setup', {'blog-title': 'ghost', name: 'slimer', email: email, password: 'short'});
}); });
// TODO: Fix tests to support inline validation // should now show a short password error
// should now throw a short password error casper.waitForText('Password must be at least 8 characters long', function onSuccess() {
// casper.waitForSelector('.notification-error', function onSuccess() { test.assert(true, 'Short password error was shown');
// test.assert(true, 'Got error notification'); }, casper.failOnTimeout(test, 'Short password error was not shown'));
// test.assertSelectorHasText('.notification-error', 'Password must be at least 8 characters long');
// }, function onTimeout() {
// test.assert(false, 'No error notification :(');
// });
casper.then(function setupWithLongPassword() { casper.then(function setupWithLongPassword() {
casper.fillAndAdd('#setup', {'blog-title': 'ghost', name: 'slimer', email: email, password: password}); casper.fillAndAdd('#setup', {'blog-title': 'ghost', name: 'slimer', email: email, password: password});
@ -32,16 +27,24 @@ CasperTest.begin('Ghost setup fails properly', 10, function suite(test) {
casper.thenClick('.gh-flow-content .btn'); casper.thenClick('.gh-flow-content .btn');
}); });
casper.waitForSelector('.notification-error', function onSuccess() { casper.waitForText('No users to invite.', function onSuccess() {
test.assert(true, 'Got error notification'); test.assert(true, 'Got error message');
test.assertSelectorHasText('.notification-error', 'No users to invite.');
test.assertExists('.gh-flow-content .btn-minor', 'Submit button is not minor'); test.assertExists('.gh-flow-content .btn-minor', 'Submit button is not minor');
test.assertSelectorHasText('.gh-flow-content .btn', 'Invite some users', 'Submit button has wrong text'); test.assertSelectorHasText('.gh-flow-content .btn', 'Invite some users', 'Submit button has wrong text');
}, function onTimeout() { }, function onTimeout() {
test.assert(false, 'No error notification for empty invitation list'); test.assert(false, 'No error message for empty invitation list');
}); });
casper.then(function fillInvalidEmail() {
casper.fill('form.gh-flow-invite', {users: 'test'});
casper.thenClick('.gh-flow-content .btn');
});
casper.waitForText('test is not a valid email.', function onSuccess() {
test.assert(true, 'Got invalid email error');
}, casper.failOnTimeout(test, 'Invalid email error not shown'));
casper.then(function fillInvitationForm() { casper.then(function fillInvitationForm() {
casper.fill('form.gh-flow-invite', {users: 'test@example.com'}); casper.fill('form.gh-flow-invite', {users: 'test@example.com'});
test.assertSelectorHasText('.gh-flow-content .btn', 'Invite 1 user', 'One invitation button text is incorrect'); test.assertSelectorHasText('.gh-flow-content .btn', 'Invite 1 user', 'One invitation button text is incorrect');
@ -58,9 +61,9 @@ CasperTest.begin('Ghost setup fails properly', 10, function suite(test) {
casper.wait(5000); casper.wait(5000);
// These invitations will fail, because Casper can't send emails // These invitations will fail, because Casper can't send emails
casper.waitForSelector('.notification-error', function onSuccess() { casper.waitForSelector('.gh-alert', function onSuccess() {
test.assert(true, 'Got error notification'); test.assert(true, 'Got error notification');
test.assertSelectorHasText('.notification-error', 'Failed to send 2 invitations: test@example.com, test2@example.com'); test.assertSelectorHasText('.gh-alert', 'Failed to send 2 invitations: test@example.com, test2@example.com');
}, function onTimeout() { }, function onTimeout() {
test.assert(false, 'No error notification after invite.'); test.assert(false, 'No error notification after invite.');
}); });

View file

@ -74,7 +74,7 @@ describe('Notifications API', function () {
notification.id.should.be.a.Number; notification.id.should.be.a.Number;
notification.id.should.not.equal(99); notification.id.should.not.equal(99);
should.exist(notification.status); should.exist(notification.status);
notification.status.should.equal('persistent'); notification.status.should.equal('alert');
done(); done();
}).catch(done); }).catch(done);