diff --git a/core/client/controllers/posts.js b/core/client/controllers/posts.js
new file mode 100644
index 0000000000..a24e98712c
--- /dev/null
+++ b/core/client/controllers/posts.js
@@ -0,0 +1,65 @@
+import { getRequestErrorMessage } from 'ghost/utils/ajax';
+
+var PostsController = Ember.ArrayController.extend({
+ // set from PostsRoute
+ paginationSettings: null,
+
+ // holds the next page to load during infinite scroll
+ nextPage: null,
+
+ // indicates whether we're currently loading the next page
+ isLoading: null,
+
+ init: function () {
+ this._super();
+
+ var metadata = this.store.metadataFor('post');
+ this.set('nextPage', metadata.pagination.next);
+ },
+
+ /**
+ * Takes an ajax response, concatenates any error messages, then generates an error notification.
+ * @param {jqXHR} response The jQuery ajax reponse object.
+ * @return
+ */
+ reportLoadError: function (response) {
+ var message = 'A problem was encountered while loading more posts';
+
+ if (response) {
+ // Get message from response
+ message += ': ' + getRequestErrorMessage(response);
+ } else {
+ message += '.';
+ }
+
+ this.notifications.showError(message);
+ },
+
+ actions: {
+ /**
+ * Loads the next paginated page of posts into the ember-data store. Will cause the posts list UI to update.
+ * @return
+ */
+ loadNextPage: function () {
+ var self = this,
+ store = this.get('store'),
+ nextPage = this.get('nextPage'),
+ paginationSettings = this.get('paginationSettings');
+
+ if (nextPage) {
+ this.set('isLoading', true);
+ this.set('paginationSettings.page', nextPage);
+ store.find('post', paginationSettings).then(function () {
+ var metadata = store.metadataFor('post');
+
+ self.set('nextPage', metadata.pagination.next);
+ self.set('isLoading', false);
+ }, function (response) {
+ self.reportLoadError(response);
+ });
+ }
+ }
+ }
+});
+
+export default PostsController;
diff --git a/core/client/routes/posts.js b/core/client/routes/posts.js
index 5c3af9741a..5f9153ccac 100644
--- a/core/client/routes/posts.js
+++ b/core/client/routes/posts.js
@@ -1,11 +1,27 @@
import styleBody from 'ghost/mixins/style-body';
import AuthenticatedRoute from 'ghost/routes/authenticated';
+var paginationSettings = {
+ status: 'all',
+ staticPages: 'all',
+ page: 1,
+ limit: 15
+};
+
var PostsRoute = AuthenticatedRoute.extend(styleBody, {
classNames: ['manage'],
model: function () {
- return this.store.find('post', { status: 'all', staticPages: 'all' });
+ // using `.filter` allows the template to auto-update when new models are pulled in from the server.
+ // we just need to 'return true' to allow all models by default.
+ return this.store.filter('post', paginationSettings, function () {
+ return true;
+ });
+ },
+
+ setupController: function (controller, model) {
+ this._super(controller, model);
+ controller.set('paginationSettings', paginationSettings);
},
actions: {
diff --git a/core/client/routes/posts/post.js b/core/client/routes/posts/post.js
index f5e0f1d13d..920b5b2129 100644
--- a/core/client/routes/posts/post.js
+++ b/core/client/routes/posts/post.js
@@ -1,5 +1,11 @@
export default Ember.Route.extend({
model: function (params) {
- return this.store.find('post', params.post_id);
+ var post = this.modelFor('posts').findBy('id', params.post_id);
+
+ if (!post) {
+ this.transitionTo('posts.index');
+ }
+
+ return post;
}
});
diff --git a/core/client/templates/posts.hbs b/core/client/templates/posts.hbs
index 7d4a488271..d38136de66 100644
--- a/core/client/templates/posts.hbs
+++ b/core/client/templates/posts.hbs
@@ -6,7 +6,7 @@
{{#link-to "new" class="button button-add" title="New Post"}}New Post{{/link-to}}
-
+ {{#view 'content-list-content-view' tagName="section"}}
{{#each itemController="posts/post" itemView="post-item-view" itemTagName="li"}}
{{!-- @TODO: Restore functionality where 'featured' and 'page' classes are added for proper posts --}}
@@ -30,7 +30,7 @@
{{/link-to}}
{{/each}}
-
+ {{/view}}
{{outlet}}
diff --git a/core/client/utils/ajax.js b/core/client/utils/ajax.js
index 5b05dc365c..faac205034 100644
--- a/core/client/utils/ajax.js
+++ b/core/client/utils/ajax.js
@@ -1,4 +1,43 @@
/* global ic */
-export default window.ajax = function () {
+
+var ajax = window.ajax = function () {
return ic.ajax.request.apply(null, arguments);
};
+
+// Used in API request fail handlers to parse a standard api error
+// response json for the message to display
+var getRequestErrorMessage = function (request) {
+ var message,
+ msgDetail;
+
+ // Can't really continue without a request
+ if (!request) {
+ return null;
+ }
+
+ // Seems like a sensible default
+ message = request.statusText;
+
+ // If a non 200 response
+ if (request.status !== 200) {
+ try {
+ // Try to parse out the error, or default to "Unknown"
+ if (request.responseJSON.errors && Ember.isArray(request.responseJSON.errors)) {
+
+ message = request.responseJSON.errors.map(function (errorItem) {
+ return errorItem.message;
+ }).join('; ');
+ } else {
+ message = request.responseJSON.error || "Unknown Error";
+ }
+ } catch (e) {
+ msgDetail = request.status ? request.status + " - " + request.statusText : "Server was not available";
+ message = "The server returned an error (" + msgDetail + ").";
+ }
+ }
+
+ return message;
+};
+
+export { getRequestErrorMessage, ajax };
+export default ajax;
diff --git a/core/client/views/content-list-content-view.js b/core/client/views/content-list-content-view.js
new file mode 100644
index 0000000000..2cd97c4ef3
--- /dev/null
+++ b/core/client/views/content-list-content-view.js
@@ -0,0 +1,29 @@
+var PostsListView = Ember.View.extend({
+ classNames: ['content-list-content'],
+
+ checkScroll: function (event) {
+ var element = event.target,
+ triggerPoint = 100,
+ controller = this.get('controller'),
+ isLoading = controller.get('isLoading');
+
+ // If we haven't passed our threshold, exit
+ if (isLoading || (element.scrollTop + element.clientHeight + triggerPoint <= element.scrollHeight)) {
+ return;
+ }
+
+ controller.send('loadNextPage');
+ },
+
+ didInsertElement: function () {
+ var el = this.$();
+ el.bind('scroll', Ember.run.bind(this, this.checkScroll));
+ },
+
+ willDestroyElement: function () {
+ var el = this.$();
+ el.unbind('scroll');
+ }
+});
+
+export default PostsListView;