Commit f3c23e89 by Andy Armstrong

Merge pull request #3027 from edx/andya/asset-loading-indicator

Add a loading indicator to the Files & Uploads page.
parents 664f78dc f92c3372
...@@ -236,6 +236,20 @@ define ["jasmine", "js/spec/create_sinon", "squire"], ...@@ -236,6 +236,20 @@ define ["jasmine", "js/spec/create_sinon", "squire"],
create_sinon.respondWithJson(requests, @mockAssetsResponse) create_sinon.respondWithJson(requests, @mockAssetsResponse)
return requests return requests
it "should show a status indicator while loading", ->
appendSetFixtures('<div class="ui-loading"/>')
expect($('.ui-loading').is(':visible')).toBe(true)
setup.call(this)
expect($('.ui-loading').is(':visible')).toBe(false)
it "should hide the status indicator if an error occurs while loading", ->
requests = create_sinon.requests(this)
appendSetFixtures('<div class="ui-loading"/>')
expect($('.ui-loading').is(':visible')).toBe(true)
@view.setPage(0)
create_sinon.respondWithError(requests)
expect($('.ui-loading').is(':visible')).toBe(false)
it "should render both assets", -> it "should render both assets", ->
requests = setup.call(this) requests = setup.call(this)
expect(@view.$el).toContainText("test asset 1") expect(@view.$el).toContainText("test asset 1")
......
define(["sinon"], function(sinon) { define(["sinon"], function(sinon) {
var fakeServer, fakeRequests, respondWithJson, respondWithError;
/* These utility methods are used by Jasmine tests to create a mock server or /* These utility methods are used by Jasmine tests to create a mock server or
* get reference to mock requests. In either case, the cleanup (restore) is done with * get reference to mock requests. In either case, the cleanup (restore) is done with
* an after function. * an after function.
...@@ -15,7 +17,7 @@ define(["sinon"], function(sinon) { ...@@ -15,7 +17,7 @@ define(["sinon"], function(sinon) {
* Get a reference to the mocked server, and respond * Get a reference to the mocked server, and respond
* to all requests with the specified statusCode. * to all requests with the specified statusCode.
*/ */
var fakeServer = function (statusCode, that) { fakeServer = function (statusCode, that) {
var server = sinon.fakeServer.create(); var server = sinon.fakeServer.create();
that.after(function() { that.after(function() {
server.restore(); server.restore();
...@@ -29,9 +31,9 @@ define(["sinon"], function(sinon) { ...@@ -29,9 +31,9 @@ define(["sinon"], function(sinon) {
* return a reference to the Array. This allows tests * return a reference to the Array. This allows tests
* to respond for individual requests. * to respond for individual requests.
*/ */
var fakeRequests = function (that) { fakeRequests = function (that) {
var requests = []; var requests = [],
var xhr = sinon.useFakeXMLHttpRequest(); xhr = sinon.useFakeXMLHttpRequest();
xhr.onCreate = function(request) { xhr.onCreate = function(request) {
requests.push(request); requests.push(request);
}; };
...@@ -43,16 +45,24 @@ define(["sinon"], function(sinon) { ...@@ -43,16 +45,24 @@ define(["sinon"], function(sinon) {
return requests; return requests;
}; };
var respondWithJson = function(requests, jsonResponse, requestIndex) { respondWithJson = function(requests, jsonResponse, requestIndex) {
requestIndex = requestIndex || requests.length - 1; requestIndex = requestIndex || requests.length - 1;
requests[requestIndex].respond(200, requests[requestIndex].respond(200,
{ "Content-Type": "application/json" }, { "Content-Type": "application/json" },
JSON.stringify(jsonResponse)); JSON.stringify(jsonResponse));
}; };
respondWithError = function(requests, requestIndex) {
requestIndex = requestIndex || requests.length - 1;
requests[requestIndex].respond(500,
{ "Content-Type": "application/json" },
JSON.stringify({ }));
};
return { return {
"server": fakeServer, "server": fakeServer,
"requests": fakeRequests, "requests": fakeRequests,
"respondWithJson": respondWithJson "respondWithJson": respondWithJson,
"respondWithError": respondWithError
}; };
}); });
define(["js/views/paging", "js/views/asset", "js/views/paging_header", "js/views/paging_footer"], define(["jquery", "underscore", "gettext", "js/views/paging", "js/views/asset", "js/views/paging_header", "js/views/paging_footer"],
function(PagingView, AssetView, PagingHeader, PagingFooter) { function($, _, gettext, PagingView, AssetView, PagingHeader, PagingFooter) {
var AssetsView = PagingView.extend({ var AssetsView = PagingView.extend({
// takes AssetCollection as model // takes AssetCollection as model
events : { events : {
...@@ -16,11 +16,23 @@ var AssetsView = PagingView.extend({ ...@@ -16,11 +16,23 @@ var AssetsView = PagingView.extend({
this.registerSortableColumn('js-asset-name-col', gettext('Name'), 'display_name', 'asc'); this.registerSortableColumn('js-asset-name-col', gettext('Name'), 'display_name', 'asc');
this.registerSortableColumn('js-asset-date-col', gettext('Date Added'), 'date_added', 'desc'); this.registerSortableColumn('js-asset-date-col', gettext('Date Added'), 'date_added', 'desc');
this.setInitialSortColumn('js-asset-date-col'); this.setInitialSortColumn('js-asset-date-col');
this.showLoadingIndicator();
}, },
render: function() { render: function() {
// Wait until the content is loaded the first time to render
return this;
},
getTableBody: function() {
var tableBody = this.tableBody;
if (!tableBody) {
this.hideLoadingIndicator();
// Create the table
this.$el.html(this.template()); this.$el.html(this.template());
this.tableBody = this.$('#asset-table-body'); tableBody = this.$('#asset-table-body');
this.tableBody = tableBody;
this.pagingHeader = new PagingHeader({view: this, el: $('#asset-paging-header')}); this.pagingHeader = new PagingHeader({view: this, el: $('#asset-paging-header')});
this.pagingFooter = new PagingFooter({view: this, el: $('#asset-paging-footer')}); this.pagingFooter = new PagingFooter({view: this, el: $('#asset-paging-footer')});
this.pagingHeader.render(); this.pagingHeader.render();
...@@ -29,28 +41,34 @@ var AssetsView = PagingView.extend({ ...@@ -29,28 +41,34 @@ var AssetsView = PagingView.extend({
// Hide the contents until the collection has loaded the first time // Hide the contents until the collection has loaded the first time
this.$('.asset-library').hide(); this.$('.asset-library').hide();
this.$('.no-asset-content').hide(); this.$('.no-asset-content').hide();
}
return this; return tableBody;
}, },
renderPageItems: function() { renderPageItems: function() {
var self = this, var self = this,
assets = this.collection, assets = this.collection,
hasAssets = assets.length > 0; hasAssets = assets.length > 0,
self.tableBody.empty(); tableBody = this.getTableBody();
tableBody.empty();
if (hasAssets) { if (hasAssets) {
assets.each( assets.each(
function(asset) { function(asset) {
var view = new AssetView({model: asset}); var view = new AssetView({model: asset});
self.tableBody.append(view.render().el); tableBody.append(view.render().el);
}); }
);
} }
self.$('.asset-library').toggle(hasAssets); self.$('.asset-library').toggle(hasAssets);
self.$('.no-asset-content').toggle(!hasAssets); self.$('.no-asset-content').toggle(!hasAssets);
return this; return this;
}, },
handleDestroy: function(model, collection, options) { onError: function() {
this.hideLoadingIndicator();
},
handleDestroy: function(model) {
this.collection.fetch({reset: true}); // reload the collection to get a fresh page full of items this.collection.fetch({reset: true}); // reload the collection to get a fresh page full of items
analytics.track('Deleted Asset', { analytics.track('Deleted Asset', {
'course': course_location_analytics, 'course': course_location_analytics,
...@@ -74,7 +92,7 @@ var AssetsView = PagingView.extend({ ...@@ -74,7 +92,7 @@ var AssetsView = PagingView.extend({
var columnName = event.target.id; var columnName = event.target.id;
this.toggleSortOrder(columnName); this.toggleSortOrder(columnName);
} }
}); });
return AssetsView; return AssetsView;
}); // end define(); }); // end define();
...@@ -46,6 +46,14 @@ define(["jquery", "underscore", "backbone", "js/utils/handle_iframe_binding"], ...@@ -46,6 +46,14 @@ define(["jquery", "underscore", "backbone", "js/utils/handle_iframe_binding"],
event.preventDefault(); event.preventDefault();
target.closest('.expand-collapse').toggleClass('expand').toggleClass('collapse'); target.closest('.expand-collapse').toggleClass('expand').toggleClass('collapse');
target.closest('.is-collapsible, .window').toggleClass('collapsed'); target.closest('.is-collapsible, .window').toggleClass('collapsed');
},
showLoadingIndicator: function() {
$('.ui-loading').show();
},
hideLoadingIndicator: function() {
$('.ui-loading').hide();
} }
}); });
......
define(["backbone", "js/views/feedback_alert", "gettext"], function(Backbone, AlertView, gettext) { define(["underscore", "js/views/baseview", "js/views/feedback_alert", "gettext"],
function(_, BaseView, AlertView, gettext) {
var PagingView = Backbone.View.extend({ var PagingView = BaseView.extend({
// takes a Backbone Paginator as a model // takes a Backbone Paginator as a model
sortableColumns: {}, sortableColumns: {},
initialize: function() { initialize: function() {
Backbone.View.prototype.initialize.call(this); BaseView.prototype.initialize.call(this);
var collection = this.collection; var collection = this.collection;
collection.bind('add', _.bind(this.onPageRefresh, this)); collection.bind('add', _.bind(this.onPageRefresh, this));
collection.bind('remove', _.bind(this.onPageRefresh, this)); collection.bind('remove', _.bind(this.onPageRefresh, this));
...@@ -29,12 +30,17 @@ define(["backbone", "js/views/feedback_alert", "gettext"], function(Backbone, Al ...@@ -29,12 +30,17 @@ define(["backbone", "js/views/feedback_alert", "gettext"], function(Backbone, Al
success: function() { success: function() {
window.scrollTo(0, 0); window.scrollTo(0, 0);
}, },
error: function(collection, response, options) { error: function(collection) {
collection.currentPage = oldPage; collection.currentPage = oldPage;
self.onError();
} }
}); });
}, },
onError: function() {
// Do nothing by default
},
nextPage: function() { nextPage: function() {
var collection = this.collection, var collection = this.collection,
currentPage = collection.currentPage, currentPage = collection.currentPage,
...@@ -82,12 +88,9 @@ define(["backbone", "js/views/feedback_alert", "gettext"], function(Backbone, Al ...@@ -82,12 +88,9 @@ define(["backbone", "js/views/feedback_alert", "gettext"], function(Backbone, Al
}, },
sortDirectionName: function() { sortDirectionName: function() {
var collection = this.collection; var collection = this.collection,
if (collection.sortDirection === 'asc') { ascending = collection.sortDirection === 'asc';
return gettext("ascending"); return ascending ? gettext("ascending") : gettext("descending");
} else {
return gettext("descending");
}
}, },
setInitialSortColumn: function(sortColumn) { setInitialSortColumn: function(sortColumn) {
...@@ -115,4 +118,4 @@ define(["backbone", "js/views/feedback_alert", "gettext"], function(Backbone, Al ...@@ -115,4 +118,4 @@ define(["backbone", "js/views/feedback_alert", "gettext"], function(Backbone, Al
}); });
return PagingView; return PagingView;
}); // end define(); }); // end define();
...@@ -27,7 +27,7 @@ require(["domReady", "jquery", "js/models/asset", "js/collections/asset", ...@@ -27,7 +27,7 @@ require(["domReady", "jquery", "js/models/asset", "js/collections/asset",
var assets = new AssetCollection(); var assets = new AssetCollection();
assets.url = "${asset_callback_url}"; assets.url = "${asset_callback_url}";
var assetsView = new AssetsView({collection: assets, el: $('#asset-library')}); var assetsView = new AssetsView({collection: assets, el: $('.assets-wrapper')});
assetsView.render(); assetsView.render();
assetsView.setPage(0); assetsView.setPage(0);
...@@ -148,7 +148,12 @@ require(["domReady", "jquery", "js/models/asset", "js/collections/asset", ...@@ -148,7 +148,12 @@ require(["domReady", "jquery", "js/models/asset", "js/collections/asset",
<div class="wrapper-content wrapper"> <div class="wrapper-content wrapper">
<section class="content"> <section class="content">
<article id="asset-library" class="content-primary" role="main"></article> <article class="content-primary" role="main">
<div class="assets-wrapper"/>
<div class="ui-loading">
<p><span class="spin"><i class="icon-refresh"></i></span> <span class="copy">${_("Loading&hellip;")}</span></p>
</div>
</article>
<aside class="content-supplementary" role="complimentary"> <aside class="content-supplementary" role="complimentary">
<div class="bit"> <div class="bit">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment