Notifications on front end

Should close #37. There are persistent and passive notifications.

Persistent ones:
* are stored on `ghost.notifications`.
* have an api made to add / remove them with client side ajax logic (probably not the most elegant, but works)
* uses a modified `flashes.hbs` template
* will only disappear if user closes the bar
* stack

Passive
* added with backbone view / collection combo
* stack
* disappears on navigation and when user closes it
This commit is contained in:
Gabor Javorszky 2013-06-27 04:52:56 +01:00 committed by ErisDS
parent d337e16afe
commit b77a8fd0d9
9 changed files with 184 additions and 41 deletions

View File

@ -341,4 +341,4 @@ var path = require('path'),
grunt.registerTask("default", ['sass:admin', 'handlebars']);
};
module.exports = configureGrunt;
module.exports = configureGrunt;

View File

@ -38,10 +38,50 @@
$(window).one('centered', fadeInAndFocus);
// Allow notifications to be dismissed
$(document).on('click', '.js-notification .close', function () {
$(document).on('click', '.js-notification.notification-passive .close', function () {
$(this).parent().fadeOut(200, function () { $(this).remove(); });
});
$(document).on('click', '.js-notification.notification-persistent .close', function () {
var self = this;
$.ajax({
type: "DELETE",
url: '/api/v0.1/notifications/' + $(this).data('id')
}).done(function (result) {
if ($(self).parent().parent().hasClass('js-bb-notification')) {
$(self).parent().parent().fadeOut(200, function () { $(self).remove(); });
} else {
$(self).parent().fadeOut(200, function () { $(self).remove(); });
}
});
});
/**
* Example of how to add a persistent notification.
*/
// $(document).on('click', '.add-persistent-notification', function (event) {
// event.preventDefault();
// var msg = {
// type: 'error',
// message: 'This is an error',
// status: 'persistent',
// id: 'per-' + $('.notification-persistent').length + 1
// };
// $.ajax({
// type: "POST",
// url: '/api/v0.1/notifications/',
// data: msg
// }).done(function (result) {
// var fcv;
// fcv = new Ghost.Views.FlashCollectionView({
// model: [msg]
// });
// console.log(fcv);
// });
// });
$(document).ready(function () {
// ## Set interactions for all menus
@ -79,4 +119,4 @@
});
}());
}());

View File

@ -1,6 +1,6 @@
// # Article Editor
/*global window, document, $, _, Backbone, Ghost, Showdown, CodeMirror, shortcut, Countable */
/*global window, document, $, _, Backbone, Ghost, Showdown, CodeMirror, shortcut, Countable, JST */
(function () {
"use strict";
@ -93,26 +93,55 @@
this.savePost({
status: keys[newIndex]
}).then(function () {
window.alert('Your post: ' + model.get('title') + ' has been ' + keys[newIndex]);
this.addSubview(new Ghost.Views.FlashCollectionView({
model: [{
type: 'success',
message: 'Your post: ' + model.get('title') + ' has been ' + keys[newIndex],
status: 'passive'
}]
}));
// window.alert('Your post: ' + model.get('title') + ' has been ' + keys[newIndex]);
});
},
handleStatus: function (e) {
e.preventDefault();
var status = $(e.currentTarget).attr('data-set-status'),
model = this.model;
model = this.model,
self = this;
if (status === 'publish-on') {
return window.alert('Scheduled publishing not supported yet.');
this.addSubview(new Ghost.Views.FlashCollectionView({
model: [{
type: 'alert',
message: 'Scheduled publishing not supported yet.',
status: 'passive'
}]
}));
// return window.alert('Scheduled publishing not supported yet.');
}
if (status === 'queue') {
return window.alert('Scheduled publishing not supported yet.');
this.addSubview(new Ghost.Views.FlashCollectionView({
model: [{
type: 'alert',
message: 'Scheduled publishing not supported yet.',
status: 'passive'
}]
}));
// return window.alert('Scheduled publishing not supported yet.');
}
this.savePost({
status: status
}).then(function () {
window.alert('Your post: ' + model.get('title') + ' has been ' + status);
self.addSubview(new Ghost.Views.FlashCollectionView({
model: [{
type: 'success',
message: 'Your post: ' + model.get('title') + ' has been ' + status,
status: 'passive'
}]
}));
// window.alert('Your post: ' + model.get('title') + ' has been ' + status);
});
},
@ -120,11 +149,26 @@
if (e) {
e.preventDefault();
}
var model = this.model;
var model = this.model,
self = this;
this.savePost().then(function () {
window.alert('Your post was saved as ' + model.get('status'));
self.addSubview(new Ghost.Views.FlashCollectionView({
model: [{
type: 'success',
message: 'Your post was saved as ' + model.get('status'),
status: 'passive'
}]
}));
// window.alert('Your post was saved as ' + model.get('status'));
}, function () {
window.alert(model.validationError);
self.addSubview(new Ghost.Views.FlashCollectionView({
model: [{
type: 'error',
message: model.validationError,
status: 'passive'
}]
}));
// window.alert(model.validationError);
});
},
@ -254,4 +298,52 @@
}
});
}());
/**
* This is the view to generate the markup for the individual
* notification. Will be included into #flashbar.
*
* States can be
* - persistent
* - passive
*
* Types can be
* - error
* - success
* - alert
* - (empty)
*
*/
Ghost.Views.FlashView = Ghost.View.extend({
templateName: 'notification',
className: 'js-bb-notification',
template: function (data) {
return JST[this.templateName](data);
},
render: function() {
var html = this.template(this.model);
this.$el.html(html);
return this;
}
});
/**
* This handles Notification groups
*/
Ghost.Views.FlashCollectionView = Ghost.View.extend({
el: '#flashbar',
initialize: function() {
this.render();
},
render: function() {
_.each(this.model, function (item) {
this.renderItem(item);
}, this);
},
renderItem: function (item) {
var itemView = new Ghost.Views.FlashView({ model: item });
this.$el.prepend(itemView.render().el);
}
});
}());

View File

@ -13,6 +13,7 @@ var Ghost = require('../ghost'),
dataProvider = ghost.dataProvider,
posts,
users,
notifications,
settings,
requestHandler,
cachedSettingsRequestHandler,
@ -58,6 +59,20 @@ users = {
}
};
// # Notifications
notifications = {
destroy: function destroy(i) {
ghost.notifications = _.reject(ghost.notifications, function (element) {
return element.id === i.id;
});
return when(ghost.notifications);
},
add: function add(notification) {
return when(ghost.notifications.push(notification));
}
};
// # Settings
// Turn a settings collection into a single object/hashmap
@ -147,6 +162,7 @@ cachedSettingsRequestHandler = function (apiMethod) {
module.exports.posts = posts;
module.exports.users = users;
module.exports.notifications = notifications;
module.exports.settings = settings;
module.exports.requestHandler = requestHandler;
module.exports.cachedSettingsRequestHandler = cachedSettingsRequestHandler;
module.exports.cachedSettingsRequestHandler = cachedSettingsRequestHandler;

View File

@ -110,6 +110,7 @@ adminControllers = {
});
},
'editor': function (req, res) {
console.log(res.locals);
if (req.params.id !== undefined) {
api.posts.read({id: parseInt(req.params.id, 10)})
.then(function (post) {

View File

@ -0,0 +1,4 @@
<section class="notification{{#if type}}-{{type}}{{/if}} notification-{{status}} js-notification">
{{message}}
<a class="close" href="#"><span class="hidden">Close</span></a>
</section>

View File

@ -27,7 +27,9 @@
{{/unless}}
<main role="main" id="main">
{{> flashes}}
<aside id="flashbar">
{{> flashes}}
</aside>
{{{body}}}
</main>
@ -72,4 +74,4 @@
Ghost.init();
</script>
</body>
</html>
</html>

View File

@ -1,26 +1,8 @@
{{#if messages}}
{{#each messages.error}}
<section class="notification-error js-notification">
{{.}}
<a class="close" href="#"><span class="hidden">Close</span></a>
{{#each messages}}
<section class="notification{{#if type}}-{{type}}{{/if}} notification-{{status}} js-notification">
{{message}}
<a class="close" href="#" data-id="{{id}}"><span class="hidden">Close</span></a>
</section>
{{/each}}
{{#each messages.success}}
<section class="notification-success js-notification">
{{.}}
<a class="close" href="#"><span class="hidden">Close</span></a>
</section>
{{/each}}
{{#each messages.warn}}
<section class="notification-alert js-notification">
{{.}}
<a class="close" href="#"><span class="hidden">Close</span></a>
</section>
{{/each}}
{{#each messages.info}}
<section class="notification js-notification">
{{.}}
<a class="close" href="#"><span class="hidden">Close</span></a>
</section>
{{/each}}
{{/if}}
{{/if}}

View File

@ -96,7 +96,7 @@ ghostLocals = function (req, res, next) {
} else {
_.extend(res.locals, {
// pass the admin flash messages, settings and paths
messages: req.flash(),
messages: ghost.notifications,
settings: ghost.settings(),
availableThemes: ghost.paths().availableThemes,
availablePlugins: ghost.paths().availablePlugins
@ -173,6 +173,12 @@ when.all([ghost.init(), filters.loadCoreFilters(ghost), helpers.loadCoreHelpers(
});
ghost.app().get('/ghost/', auth, admin.index);
// Notifications routes
ghost.app().del('/api/v0.1/notifications/:id', authAPI, disableCachedResult, api.requestHandler(api.notifications.destroy));
ghost.app().post('/api/v0.1/notifications/', authAPI, disableCachedResult, api.requestHandler(api.notifications.add));
/**
* Frontend routes..
* @todo dynamic routing, homepage generator, filters ETC ETC
@ -183,6 +189,7 @@ when.all([ghost.init(), filters.loadCoreFilters(ghost), helpers.loadCoreHelpers(
ghost.app().listen(
ghost.config().env[process.env.NODE_ENV || 'development'].url.port,
ghost.config().env[process.env.NODE_ENV || 'development'].url.host,
@ -195,5 +202,4 @@ when.all([ghost.init(), filters.loadCoreFilters(ghost), helpers.loadCoreHelpers(
loading.resolve();
}
);
}, errors.logAndThrowError);