Persistent notifications

closes #3057
- add Notification model
- update injected Notifications object to handle persistent notifications
- load server notifications on setup if logged in otherwise on successful sign-in
- changed all existing notifications.closeAll calls to closePassive
- fixed dismissable/dismissible spelling in server API & tests
- add notifications.closeNotification method so DELETE calls can be made for server-originating notifications
This commit is contained in:
Kevin Ansfield 2014-06-29 23:45:03 +02:00
parent 9214f25dee
commit 7e2e8b3376
14 changed files with 80 additions and 30 deletions

View File

@ -13,7 +13,7 @@ var NotificationComponent = Ember.Component.extend({
actions: {
closeNotification: function () {
var self = this;
self.notifications.removeObject(self.get('message'));
self.notifications.closeNotification(self.get('message'));
}
}
});

View File

@ -7,8 +7,7 @@ var PostController = Ember.ObjectController.extend({
var featured = this.toggleProperty('featured'),
self = this;
// @TODO This should call closePassive() to only close passive notifications
self.notifications.closeAll();
self.notifications.closePassive();
this.get('model').save().then(function () {
self.notifications.showSuccess('Post successfully marked as ' + (featured ? 'featured' : 'not featured') + '.');

View File

@ -30,8 +30,7 @@ var SettingsGeneralController = Ember.ObjectController.extend({
save: function () {
var self = this;
// @TODO This should call closePassive() to only close passive notifications
self.notifications.closeAll();
self.notifications.closePassive();
return this.get('model').save().then(function (model) {
self.notifications.showSuccess('Settings successfully saved.');

View File

@ -23,8 +23,7 @@ var SettingsUserController = Ember.Controller.extend({
save: function () {
var self = this;
// @TODO This should call closePassive() to only close passive notifications
self.notifications.closeAll();
self.notifications.closePassive();
alert('@TODO: Saving user...');

View File

@ -15,8 +15,7 @@ var SetupController = Ember.ObjectController.extend(ValidationEngine, {
setup: function () {
var self = this;
// @TODO This should call closePassive() to only close passive notifications
self.notifications.closeAll();
self.notifications.closePassive();
this.toggleProperty('submitting');
this.validate({ format: false }).then(function () {

View File

@ -14,8 +14,7 @@ var SignupController = Ember.ObjectController.extend(ValidationEngine, {
signup: function () {
var self = this;
// @TODO This should call closePassive() to only close passive notifications
self.notifications.closeAll();
self.notifications.closePassive();
this.toggleProperty('submitting');
this.validate({ format: false }).then(function () {

View File

@ -166,14 +166,14 @@ var EditorControllerMixin = Ember.Mixin.create(MarkerManager, {
showSaveNotification: function (prevStatus, status, delay) {
var message = this.messageMap.success.post[prevStatus][status];
this.notifications.closeAll();
this.notifications.closePassive();
this.notifications.showSuccess(message, delay);
},
showErrorNotification: function (prevStatus, status, errors, delay) {
var message = this.messageMap.errors.post[prevStatus][status];
this.notifications.closeAll();
this.notifications.closePassive();
message += '<br />' + errors[0].message;
@ -187,8 +187,7 @@ var EditorControllerMixin = Ember.Mixin.create(MarkerManager, {
isNew = this.get('isNew'),
self = this;
// @TODO This should call closePassive() to only close passive notifications
self.notifications.closeAll();
self.notifications.closePassive();
// ensure an incomplete tag is finalised before save
this.get('controllers.post-tags-input').send('addNewTag');

View File

@ -0,0 +1,13 @@
var Notification = DS.Model.extend({
dismissible: DS.attr('boolean'),
location: DS.attr('string'),
status: DS.attr('string'),
type: DS.attr('string'),
message: DS.attr('string'),
typeClass: function () {
return 'notification-' + this.get('type');
}.property('type')
});
export default Notification;

View File

@ -9,8 +9,7 @@ Router.reopen({
rootURL: ghostPaths().subdir + '/ghost/', // admin interface lives under sub-directory /ghost
clearNotifications: function () {
// @TODO This should call closePassive() to only close passive notifications
this.notifications.closeAll();
this.notifications.closePassive();
this.notifications.displayDelayed();
}.on('didTransition')
});

View File

@ -33,6 +33,12 @@ var ApplicationRoute = Ember.Route.extend(Ember.SimpleAuth.ApplicationRouteMixin
});
}.on('init'),
setupController: function () {
Ember.run.next(this, function () {
this.send('loadServerNotifications');
});
},
actions: {
closePopups: function () {
this.get('popover').closePopovers();
@ -51,6 +57,8 @@ var ApplicationRoute = Ember.Route.extend(Ember.SimpleAuth.ApplicationRouteMixin
this.set('user', user);
this.set('controller.user', user);
this.send('loadServerNotifications', true);
},
signedOut: function () {
@ -90,6 +98,17 @@ var ApplicationRoute = Ember.Route.extend(Ember.SimpleAuth.ApplicationRouteMixin
});
},
loadServerNotifications: function (isDelayed) {
var self = this;
if (this.session.isAuthenticated) {
this.store.findAll('notification').then(function (serverNotifications) {
serverNotifications.forEach(function (notification) {
self.notifications.handleNotification(notification, isDelayed);
});
});
}
},
handleErrors: function (errors) {
var self = this;
this.notifications.clear();

View File

@ -1,7 +1,10 @@
import Notification from 'ghost/models/notification';
var Notifications = Ember.ArrayProxy.extend({
delayedNotifications: [],
content: Ember.A(),
timeout: 3000,
pushObject: function (object) {
object.typeClass = 'notification-' + object.type;
// This should be somewhere else.
@ -11,6 +14,10 @@ var Notifications = Ember.ArrayProxy.extend({
this._super(object);
},
handleNotification: function (message, delayed) {
if (!message.status) {
message.status = 'passive';
}
if (!delayed) {
this.pushObject(message);
} else {
@ -63,6 +70,24 @@ var Notifications = Ember.ArrayProxy.extend({
});
self.delayedNotifications = [];
},
closeNotification: function (notification) {
var self = this;
if (notification instanceof Notification) {
notification.deleteRecord();
notification.save().then(function () {
self.removeObject(notification);
});
} else {
this.removeObject(notification);
}
},
closePassive: function () {
this.set('content', this.rejectBy('status', 'passive'));
},
closePersistent: function () {
this.set('content', this.rejectBy('status', 'persistent'));
},
closeAll: function () {
this.clear();
}

View File

@ -39,7 +39,7 @@ notifications = {
return element.id === parseInt(options.id, 10);
});
if (notification && !notification.dismissable) {
if (notification && !notification.dismissible) {
return when.reject(
new errors.NoPermissionError('You do not have permission to dismiss this notification.')
);
@ -78,14 +78,14 @@ notifications = {
* type: 'error', // this can be 'error', 'success', 'warn' and 'info'
* message: 'This is an error', // A string. Should fit in one line.
* location: 'bottom', // A string where this notification should appear. can be 'bottom' or 'top'
* dismissable: true // A Boolean. Whether the notification is dismissable or not.
* dismissible: true // A Boolean. Whether the notification is dismissible or not.
* }] };
* ```
*/
add: function add(object) {
var defaults = {
dismissable: true,
dismissible: true,
location: 'bottom',
status: 'persistent'
},

View File

@ -59,7 +59,7 @@ describe('Notifications API', function () {
should.exist(result.notifications);
notification = result.notifications[0];
notification.dismissable.should.be.true;
notification.dismissible.should.be.true;
should.exist(notification.location);
notification.location.should.equal('bottom');

View File

@ -18,7 +18,7 @@ var url = require('url'),
user: ['id', 'uuid', 'name', 'slug', 'email', 'image', 'cover', 'bio', 'website',
'location', 'accessibility', 'status', 'language', 'meta_title', 'meta_description', 'last_login',
'created_at', 'created_by', 'updated_at', 'updated_by'],
notification: ['type', 'message', 'status', 'id', 'dismissable', 'location'],
notification: ['type', 'message', 'status', 'id', 'dismissible', 'location'],
slugs: ['slugs'],
slug: ['slug'],
accesstoken: ['access_token', 'refresh_token', 'expires_in', 'token_type']