Pagination for Users Management screen

closes #3222
- implementing server-side pagination for /users API
- passing /users?limit=none will return all users
- passing /users?status=invited will filter base on user status
- creating 3 mixins (route, controller and view) to keep pagination logic DRY
- updating route, controller and view for Posts to use new mixing
- implementing infinite scrolling for Users Management screen (using new mixins)
- Users Management screen displays all invited users, but paginates active users
This commit is contained in:
Maurice Williams 2014-07-20 12:42:03 -04:00
parent 95d3925c3e
commit fb170d1725
10 changed files with 198 additions and 86 deletions

View File

@ -1,6 +1,6 @@
import { getRequestErrorMessage } from 'ghost/utils/ajax';
import PaginationControllerMixin from 'ghost/mixins/pagination-controller';
var PostsController = Ember.ArrayController.extend({
var PostsController = Ember.ArrayController.extend(PaginationControllerMixin, {
// this will cause the list to re-sort when any of these properties change on any of the models
sortProperties: ['status', 'published_at', 'updated_at'],
@ -66,64 +66,11 @@ var PostsController = Ember.ArrayController.extend({
return statusResult;
},
// 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();
//let the PaginationControllerMixin know what type of model we will be paginating
//this is necesariy because we do not have access to the model inside the Controller::init method
this._super({'modelType': 'post'});
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, true);
} 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);
});
}
}
}
});

View File

@ -1,4 +1,12 @@
var UsersIndexController = Ember.ArrayController.extend({
import PaginationControllerMixin from 'ghost/mixins/pagination-controller';
var UsersIndexController = Ember.ArrayController.extend(PaginationControllerMixin, {
init: function () {
//let the PaginationControllerMixin know what type of model we will be paginating
//this is necesariy because we do not have access to the model inside the Controller::init method
this._super({'modelType': 'user'});
},
users: Ember.computed.alias('model'),
activeUsers: Ember.computed.filterBy('users', 'active', true).property('users'),

View File

@ -0,0 +1,76 @@
import { getRequestErrorMessage } from 'ghost/utils/ajax';
var PaginationControllerMixin = Ember.Mixin.create({
// set from PaginationRouteMixin
paginationSettings: null,
// holds the next page to load during infinite scroll
nextPage: null,
// indicates whether we're currently loading the next page
isLoading: null,
/**
*
* @param options: {
* modelType: <String> name of the model that will be paginated
* }
*/
init: function (options) {
this._super();
var metadata = this.store.metadataFor(options.modelType);
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 records';
if (response) {
// Get message from response
message += ': ' + getRequestErrorMessage(response, true);
} 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'),
recordType = this.get('model').get('type'),
nextPage = this.get('nextPage'),
paginationSettings = this.get('paginationSettings');
if (nextPage) {
this.set('isLoading', true);
this.set('paginationSettings.page', nextPage);
store.find(recordType, paginationSettings).then(function () {
var metadata = store.metadataFor(recordType);
self.set('nextPage', metadata.pagination.next);
self.set('isLoading', false);
}, function (response) {
self.reportLoadError(response);
});
}
}
}
});
export default PaginationControllerMixin;

View File

@ -0,0 +1,23 @@
var defaultPaginationSettings = {
page: 1,
limit: 15
};
var PaginationRoute = Ember.Mixin.create({
/**
* Sets up pagination details
* @param {settings}: object that specifies additional pagination details
*/
setupPagination: function (settings) {
settings = settings || {};
settings = _.defaults(settings, defaultPaginationSettings);
this.set('paginationSettings', settings);
this.controller.set('paginationSettings', settings);
}
});
export default PaginationRoute;

View File

@ -0,0 +1,39 @@
var PaginationViewInfiniteScrollMixin = Ember.Mixin.create({
/**
* Determines if we are past a scroll point where we need to fetch the next page
* @param event The scroll event
*/
checkScroll: function (event) {
var element = event.target,
triggerPoint = 100,
controller = this.get('controller'),
isLoading = controller.get('isLoading');
// If we haven't passed our threshold or we are already fetching content, exit
if (isLoading || (element.scrollTop + element.clientHeight + triggerPoint <= element.scrollHeight)) {
return;
}
controller.send('loadNextPage');
},
/**
* Bind to the scroll event once the element is in the DOM
*/
didInsertElement: function () {
var el = this.$();
el.on('scroll', Ember.run.bind(this, this.checkScroll));
},
/**
* Unbind from the scroll event when the element is no longer in the DOM
*/
willDestroyElement: function () {
var el = this.$();
el.off('scroll');
}
});
export default PaginationViewInfiniteScrollMixin;

View File

@ -1,6 +1,7 @@
import styleBody from 'ghost/mixins/style-body';
import ShortcutsRoute from 'ghost/mixins/shortcuts-route';
import loadingIndicator from 'ghost/mixins/loading-indicator';
import PaginationRouteMixin from 'ghost/mixins/pagination-route';
var paginationSettings = {
status: 'all',
@ -9,7 +10,7 @@ var paginationSettings = {
page: 1
};
var PostsRoute = Ember.Route.extend(Ember.SimpleAuth.AuthenticatedRouteMixin, ShortcutsRoute, styleBody, loadingIndicator, {
var PostsRoute = Ember.Route.extend(Ember.SimpleAuth.AuthenticatedRouteMixin, ShortcutsRoute, styleBody, loadingIndicator, PaginationRouteMixin, {
classNames: ['manage'],
model: function () {
@ -22,7 +23,7 @@ var PostsRoute = Ember.Route.extend(Ember.SimpleAuth.AuthenticatedRouteMixin, Sh
setupController: function (controller, model) {
this._super(controller, model);
controller.set('paginationSettings', paginationSettings);
this.setupPagination(paginationSettings);
},
shortcuts: {

View File

@ -1,7 +1,34 @@
var UsersIndexRoute = Ember.Route.extend(Ember.SimpleAuth.AuthenticatedRouteMixin, {
import PaginationRouteMixin from 'ghost/mixins/pagination-route';
var activeUsersPaginationSettings = {
include: 'roles',
page: 1,
limit: 20
};
var invitedUsersPaginationSettings = {
include: 'roles',
limit: 'all',
status: 'invited'
};
var UsersIndexRoute = Ember.Route.extend(Ember.SimpleAuth.AuthenticatedRouteMixin, PaginationRouteMixin, {
setupController: function (controller, model) {
this._super(controller, model.active);
this.setupPagination(activeUsersPaginationSettings);
},
model: function () {
return this.store.find('user');
// 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 Ember.RSVP.hash({
inactive: this.store.filter('user', invitedUsersPaginationSettings, function () {
return true;
}),
active: this.store.filter('user', activeUsersPaginationSettings, function () {
return true;
})
});
}
});

View File

@ -6,7 +6,7 @@
</section>
</header>
<section class="content settings-users">
{{#view "settings/users/users-list-view" tagName="section" class="content settings-users" }}
{{#if invitedUsers}}
<section class="object-list invited-users">
@ -62,4 +62,4 @@
{{/each}}
</section>
</section>
{{/view}}

View File

@ -1,34 +1,17 @@
import setScrollClassName from 'ghost/utils/set-scroll-classname';
import PaginationViewMixin from 'ghost/mixins/pagination-view-infinite-scroll';
var PostsListView = Ember.View.extend({
var PostsListView = Ember.View.extend(PaginationViewMixin, {
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 () {
this._super();
var el = this.$();
el.on('scroll', Ember.run.bind(this, this.checkScroll));
el.on('scroll', Ember.run.bind(el, setScrollClassName, {
target: el.closest('.content-list'),
offset: 10
}));
},
willDestroyElement: function () {
var el = this.$();
el.off('scroll');
}
});

View File

@ -0,0 +1,8 @@
//import setScrollClassName from 'ghost/utils/set-scroll-classname';
import PaginationViewMixin from 'ghost/mixins/pagination-view-infinite-scroll';
var UsersListView = Ember.View.extend(PaginationViewMixin, {
classNames: ['settings-users']
});
export default UsersListView;