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 : {
"click .column-sort-link": "onToggleColumn" "click .column-sort-link": "onToggleColumn"
}, },
initialize : function() { initialize : function() {
PagingView.prototype.initialize.call(this); PagingView.prototype.initialize.call(this);
var collection = this.collection; var collection = this.collection;
this.template = _.template($("#asset-library-tpl").text()); this.template = _.template($("#asset-library-tpl").text());
this.listenTo(collection, 'destroy', this.handleDestroy); this.listenTo(collection, 'destroy', this.handleDestroy);
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() {
this.$el.html(this.template()); // Wait until the content is loaded the first time to render
this.tableBody = this.$('#asset-table-body'); return this;
this.pagingHeader = new PagingHeader({view: this, el: $('#asset-paging-header')}); },
this.pagingFooter = new PagingFooter({view: this, el: $('#asset-paging-footer')});
this.pagingHeader.render();
this.pagingFooter.render();
// Hide the contents until the collection has loaded the first time getTableBody: function() {
this.$('.asset-library').hide(); var tableBody = this.tableBody;
this.$('.no-asset-content').hide(); if (!tableBody) {
this.hideLoadingIndicator();
return this; // Create the table
}, this.$el.html(this.template());
tableBody = this.$('#asset-table-body');
this.tableBody = tableBody;
this.pagingHeader = new PagingHeader({view: this, el: $('#asset-paging-header')});
this.pagingFooter = new PagingFooter({view: this, el: $('#asset-paging-footer')});
this.pagingHeader.render();
this.pagingFooter.render();
renderPageItems: function() { // Hide the contents until the collection has loaded the first time
var self = this, this.$('.asset-library').hide();
assets = this.collection, this.$('.no-asset-content').hide();
hasAssets = assets.length > 0; }
self.tableBody.empty(); return tableBody;
if (hasAssets) { },
assets.each(
function(asset) { renderPageItems: function() {
var view = new AssetView({model: asset}); var self = this,
self.tableBody.append(view.render().el); assets = this.collection,
hasAssets = assets.length > 0,
tableBody = this.getTableBody();
tableBody.empty();
if (hasAssets) {
assets.each(
function(asset) {
var view = new AssetView({model: asset});
tableBody.append(view.render().el);
}
);
}
self.$('.asset-library').toggle(hasAssets);
self.$('.no-asset-content').toggle(!hasAssets);
return this;
},
onError: function() {
this.hideLoadingIndicator();
},
handleDestroy: function(model) {
this.collection.fetch({reset: true}); // reload the collection to get a fresh page full of items
analytics.track('Deleted Asset', {
'course': course_location_analytics,
'id': model.get('url')
}); });
} },
self.$('.asset-library').toggle(hasAssets);
self.$('.no-asset-content').toggle(!hasAssets);
return this;
},
handleDestroy: function(model, collection, options) { addAsset: function (model) {
this.collection.fetch({reset: true}); // reload the collection to get a fresh page full of items // Switch the sort column back to the default (most recent date added) and show the first page
analytics.track('Deleted Asset', { // so that the new asset is shown at the top of the page.
'course': course_location_analytics, this.setInitialSortColumn('js-asset-date-col');
'id': model.get('url') this.setPage(0);
});
},
addAsset: function (model) { analytics.track('Uploaded a File', {
// Switch the sort column back to the default (most recent date added) and show the first page 'course': course_location_analytics,
// so that the new asset is shown at the top of the page. 'asset_url': model.get('url')
this.setInitialSortColumn('js-asset-date-col'); });
this.setPage(0); },
analytics.track('Uploaded a File', { onToggleColumn: function(event) {
'course': course_location_analytics, var columnName = event.target.id;
'asset_url': model.get('url') this.toggleSortOrder(columnName);
}
}); });
},
onToggleColumn: function(event) {
var columnName = event.target.id;
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));
collection.bind('reset', _.bind(this.onPageRefresh, this)); collection.bind('reset', _.bind(this.onPageRefresh, this));
}, },
onPageRefresh: function() { onPageRefresh: function() {
var sortColumn = this.sortColumn; var sortColumn = this.sortColumn;
this.renderPageItems(); this.renderPageItems();
this.$('.column-sort-link').removeClass('current-sort'); this.$('.column-sort-link').removeClass('current-sort');
this.$('#' + sortColumn).addClass('current-sort'); this.$('#' + sortColumn).addClass('current-sort');
}, },
setPage: function(page) { setPage: function(page) {
var self = this, var self = this,
collection = self.collection, collection = self.collection,
oldPage = collection.currentPage; oldPage = collection.currentPage;
collection.goTo(page, { collection.goTo(page, {
reset: true, reset: true,
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();
}); }
}, });
},
nextPage: function() { onError: function() {
var collection = this.collection, // Do nothing by default
currentPage = collection.currentPage, },
lastPage = collection.totalPages - 1;
if (currentPage < lastPage) {
this.setPage(currentPage + 1);
}
},
previousPage: function() { nextPage: function() {
var collection = this.collection, var collection = this.collection,
currentPage = collection.currentPage; currentPage = collection.currentPage,
if (currentPage > 0) { lastPage = collection.totalPages - 1;
this.setPage(currentPage - 1); if (currentPage < lastPage) {
} this.setPage(currentPage + 1);
}, }
},
previousPage: function() {
var collection = this.collection,
currentPage = collection.currentPage;
if (currentPage > 0) {
this.setPage(currentPage - 1);
}
},
/** /**
* Registers information about a column that can be sorted. * Registers information about a column that can be sorted.
* @param columnName The element name of the column. * @param columnName The element name of the column.
* @param displayName The display name for the column in the current locale. * @param displayName The display name for the column in the current locale.
* @param fieldName The database field name that is represented by this column. * @param fieldName The database field name that is represented by this column.
* @param defaultSortDirection The default sort direction for the column * @param defaultSortDirection The default sort direction for the column
*/ */
registerSortableColumn: function(columnName, displayName, fieldName, defaultSortDirection) { registerSortableColumn: function(columnName, displayName, fieldName, defaultSortDirection) {
this.sortableColumns[columnName] = { this.sortableColumns[columnName] = {
displayName: displayName, displayName: displayName,
fieldName: fieldName, fieldName: fieldName,
defaultSortDirection: defaultSortDirection defaultSortDirection: defaultSortDirection
}; };
}, },
sortableColumnInfo: function(sortColumn) { sortableColumnInfo: function(sortColumn) {
var sortInfo = this.sortableColumns[sortColumn]; var sortInfo = this.sortableColumns[sortColumn];
if (!sortInfo) { if (!sortInfo) {
throw "Unregistered sort column '" + sortColumn + '"'; throw "Unregistered sort column '" + sortColumn + '"';
} }
return sortInfo; return sortInfo;
}, },
sortDisplayName: function() { sortDisplayName: function() {
var sortColumn = this.sortColumn, var sortColumn = this.sortColumn,
sortInfo = this.sortableColumnInfo(sortColumn); sortInfo = this.sortableColumnInfo(sortColumn);
return sortInfo.displayName; return sortInfo.displayName;
}, },
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) {
var collection = this.collection, var collection = this.collection,
sortInfo = this.sortableColumns[sortColumn]; sortInfo = this.sortableColumns[sortColumn];
collection.sortField = sortInfo.fieldName; collection.sortField = sortInfo.fieldName;
collection.sortDirection = sortInfo.defaultSortDirection; collection.sortDirection = sortInfo.defaultSortDirection;
this.sortColumn = sortColumn; this.sortColumn = sortColumn;
}, },
toggleSortOrder: function(sortColumn) { toggleSortOrder: function(sortColumn) {
var collection = this.collection, var collection = this.collection,
sortInfo = this.sortableColumnInfo(sortColumn), sortInfo = this.sortableColumnInfo(sortColumn),
sortField = sortInfo.fieldName, sortField = sortInfo.fieldName,
defaultSortDirection = sortInfo.defaultSortDirection; defaultSortDirection = sortInfo.defaultSortDirection;
if (collection.sortField === sortField) { if (collection.sortField === sortField) {
collection.sortDirection = collection.sortDirection === 'asc' ? 'desc' : 'asc'; collection.sortDirection = collection.sortDirection === 'asc' ? 'desc' : 'asc';
} else { } else {
collection.sortField = sortField; collection.sortField = sortField;
collection.sortDirection = defaultSortDirection; collection.sortDirection = defaultSortDirection;
}
this.sortColumn = sortColumn;
this.setPage(0);
} }
this.sortColumn = sortColumn; });
this.setPage(0);
}
});
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