From b508995ff0e4bcd269d4837986509b0e45a0dea4 Mon Sep 17 00:00:00 2001 From: George Schneeloch Date: Fri, 23 Oct 2015 11:12:19 -0400 Subject: [PATCH] Refactored listing code for testing --- ui/jstests/test_listing.js | 466 +++++++++++++++++- ui/static/ui/js/listing.js | 959 ++++++++++++++++++++----------------- 2 files changed, 964 insertions(+), 461 deletions(-) diff --git a/ui/jstests/test_listing.js b/ui/jstests/test_listing.js index 8789001f..eedf7952 100644 --- a/ui/jstests/test_listing.js +++ b/ui/jstests/test_listing.js @@ -341,6 +341,33 @@ define(['QUnit', 'jquery', 'react', 'test_utils', 'utils', 'listing'], } ] }; + var learningResourceResponse = { + "id": 1, + "learning_resource_type": "course", + "static_assets": [], + "title": "title", + "materialized_path": "/course", + "content_xml": "", + "url_path": "", + "parent": null, + "copyright": "", + "xa_nr_views": 0, + "xa_nr_attempts": 0, + "xa_avg_grade": 0.0, + "xa_histogram_grade": 0.0, + "terms": ["required"] + }; + var learningResourceResponseMinusContentXml = $.extend( + {}, learningResourceResponse); + + // Substituted in for window.location + var queryString; + var updateQueryString = function (query) { + queryString = query; + }; + var getQueryString = function () { + return queryString; + }; QUnit.module('Tests for listing page', { beforeEach: function() { @@ -384,6 +411,7 @@ define(['QUnit', 'jquery', 'react', 'test_utils', 'utils', 'listing'], "
" + "
" + "
" + + "
" + "
" + "
" + "
" + @@ -392,10 +420,21 @@ define(['QUnit', 'jquery', 'react', 'test_utils', 'utils', 'listing'], "
")); + + queryString = ""; }, afterEach: function() { TestUtils.cleanup(); @@ -506,34 +545,431 @@ define(['QUnit', 'jquery', 'react', 'test_utils', 'utils', 'listing'], }); }); - QUnit.test("Assert that lone query strings don't have a question mark", + QUnit.test("Assert facet checkboxes", function(assert) { var done = assert.async(); - var oldLocation = window.location.toString(); - var container = $("#listing")[0]; - Listing.loader(listingOptions, container); + var afterMount = function(component) { + var container = React.findDOMNode(component); + waitForAjax(1, function() { + assert.equal(queryString, ""); + assert.equal( + component.state.resources.length, + searchResponseAll.count + ); + + // Select course facet + $(container).find("ins").first().click(); + waitForAjax(1, function() { + assert.equal( + queryString, + "?selected_facets=course_exact%3A8.01" + ); + assert.equal( + component.state.resources.length, + searchResponseCourseFacet.count + ); + + $(container).find("ins").first().click(); + waitForAjax(1, function() { + // Note lack of '?' + assert.equal(queryString, ""); + assert.equal( + component.state.resources.length, + searchResponseAll.count + ); + done(); + }); + }); + }); + + }; + var options = { + allExports: listingOptions.allExports, + sortingOptions: listingOptions.sortingOptions, + imageDir: listingOptions.imageDir, + pageSize: 25, + repoSlug: listingOptions.repoSlug, + loggedInUsername: listingOptions.loggedInUsername, + updateQueryString: updateQueryString, + getQueryString: getQueryString, + ref: afterMount + }; + + React.addons.TestUtils.renderIntoDocument( + React.createElement(Listing.ListingContainer, options) + ); + } + ); + + QUnit.test("Check sorting options", function(assert) { + var done = assert.async(); + + // Note that results here are not meaningful + TestUtils.initMockjax({ + url: '/api/v1/repositories/test/search/' + + '?selected_facets=course_exact%3A8.01&' + + 'selected_facets=run_exact%3A2014_Fall', + type: 'GET', + responseText: searchResponseAll + }); + TestUtils.initMockjax({ + url: '/api/v1/repositories/test/search/' + + '?selected_facets=course_exact%3A8.01&' + + 'selected_facets=run_exact%3A2014_Fall&' + + 'sortby=avg_grade', + type: 'GET', + responseText: searchResponseAll + }); + TestUtils.initMockjax({ + url: '/api/v1/repositories/test/search/?sortby=avg_grade&q=text', + type: 'GET', + responseText: searchResponseAll + }); + + var afterMount = function (component) { + var node = React.findDOMNode(component); + + waitForAjax(1, function () { + // Select course facet + $(node).find("ins").first().click(); + waitForAjax(1, function () { + assert.equal(queryString, "?selected_facets=course_exact%3A8.01"); + + // Select second course facet. + // Checkboxes change between refreshes but that won't matter here. + // This will make sure we can handle two checkboxes. + $($(node).find("ins")[1]).click(); + waitForAjax(1, function() { + assert.equal( + queryString, + "?selected_facets=course_exact%3A8.01&" + + "selected_facets=run_exact%3A2014_Fall" + ); + + // Sort by title + React.addons.TestUtils.Simulate.click( + $(node).find("a:contains('Average')")[0] + ); + waitForAjax(1, function() { + assert.equal( + queryString, + "?selected_facets=course_exact%3A8.01&" + + "selected_facets=run_exact%3A2014_Fall&" + + "sortby=avg_grade" + ); + + // Filter by text + $("#id_q").val("text"); + $("#search_button").click(); + waitForAjax(1, function() { + // Sort was kept but facets were lost + assert.equal(queryString, "?sortby=avg_grade&q=text"); + + done(); + }); + }); + }); + }); + }); + }; + + var options = { + allExports: listingOptions.allExports, + sortingOptions: listingOptions.sortingOptions, + imageDir: listingOptions.imageDir, + pageSize: 25, + repoSlug: listingOptions.repoSlug, + loggedInUsername: listingOptions.loggedInUsername, + updateQueryString: updateQueryString, + getQueryString: getQueryString, + ref: afterMount + }; + + React.addons.TestUtils.renderIntoDocument( + React.createElement(Listing.ListingContainer, options) + ); + }); + + QUnit.test("Test pagination", function(assert) { + var done = assert.async(); + var pageSize = 2; + + var afterMount = function(component) { + // Initial state + assert.equal( + component.state.numPages, + 0 + ); waitForAjax(1, function() { - assert.equal(window.location.toString(), oldLocation); + assert.equal( + component.state.numPages, + Math.ceil(searchResponseAll.count / pageSize) + ); + done(); + }); + }; + + var options = { + allExports: listingOptions.allExports, + sortingOptions: listingOptions.sortingOptions, + imageDir: listingOptions.imageDir, + pageSize: pageSize, + repoSlug: listingOptions.repoSlug, + loggedInUsername: listingOptions.loggedInUsername, + updateQueryString: updateQueryString, + getQueryString: getQueryString, + ref: afterMount + }; + + React.addons.TestUtils.renderIntoDocument( + React.createElement(Listing.ListingContainer, options) + ); + + }); + + QUnit.test("Assert search textbox", function (assert) { + var done = assert.async(); + + // This is a failure but shouldn't affect changing of query string + TestUtils.initMockjax({ + url: '/api/v1/repositories/test/search/?q=text+with+spaces', + type: 'GET', + responseText: searchResponseCourseFacet, + status: 400 + }); + + var afterMount = function (component) { + waitForAjax(1, function () { + assert.equal(queryString, ""); + assert.equal( + component.state.resources.length, + searchResponseAll.count + ); + + // Set search text + $("#id_q").val("text with spaces"); + $("#search_button").click(); + waitForAjax(1, function () { + assert.equal(queryString, "?q=text+with+spaces"); + assert.equal( + component.state.resources.length, + searchResponseAll.count + ); + + $("#id_q").val(""); + $("#search_button").click(); + waitForAjax(1, function () { + // Note lack of '?' + assert.equal(queryString, ""); + assert.equal( + component.state.resources.length, + searchResponseAll.count + ); + done(); + }); + }); + }); + }; + + var options = { + allExports: listingOptions.allExports, + sortingOptions: listingOptions.sortingOptions, + imageDir: listingOptions.imageDir, + pageSize: 25, + repoSlug: listingOptions.repoSlug, + loggedInUsername: listingOptions.loggedInUsername, + updateQueryString: updateQueryString, + getQueryString: getQueryString, + ref: afterMount + }; + + React.addons.TestUtils.renderIntoDocument( + React.createElement(Listing.ListingContainer, options) + ); + }); + + QUnit.test('Assert loader behavior', function(assert) { + var done = assert.async(); + + // Success on initial load but fail after clicking checkbox + TestUtils.replaceMockjax({ + url: '/api/v1/repositories/test/search/' + + '?selected_facets=course_exact%3A8.01', + type: 'GET', + responseText: searchResponseCourseFacet, + status: 400 + }); + + var afterMount = function (component) { + var container = React.findDOMNode(component); + assert.equal(component.state.pageLoaded, false); + waitForAjax(1, function () { + assert.equal(component.state.pageLoaded, true); // Select course facet $(container).find("ins").first().click(); - waitForAjax(1, function() { - assert.equal( - window.location.toString(), - oldLocation + "?selected_facets=course_exact%3A8.01" + component.forceUpdate(function() { + assert.equal(component.state.pageLoaded, false); + waitForAjax(1, function () { + assert.equal(component.state.pageLoaded, true); + done(); + }); + }); + }); + + }; + + var options = { + allExports: listingOptions.allExports, + sortingOptions: listingOptions.sortingOptions, + imageDir: listingOptions.imageDir, + pageSize: 25, + repoSlug: listingOptions.repoSlug, + loggedInUsername: listingOptions.loggedInUsername, + updateQueryString: updateQueryString, + getQueryString: getQueryString, + ref: afterMount + }; + + React.addons.TestUtils.renderIntoDocument( + React.createElement(Listing.ListingContainer, options) + ); + }); + + QUnit.test('Check isEmail', function(assert) { + assert.ok(Listing.isEmail("staff@edx.org")); + assert.ok(Listing.isEmail("staff@mit.edu")); + assert.ok(!Listing.isEmail("@mit.edu")); + assert.ok(!Listing.isEmail("other")); + }); + + QUnit.test('Assert resource tab lazy loading', function(assert) { + var done = assert.async(); + + TestUtils.initMockjax({ + url: '/api/v1/repositories/test/learning_resources/' + + '1/?remove_content_xml=true', + type: 'GET', + responseText: learningResourceResponseMinusContentXml + }); + TestUtils.initMockjax({ + url: '/api/v1/repositories/test/learning_resources/1/', + type: 'GET', + responseText: learningResourceResponse + }); + TestUtils.initMockjax({ + url: '/api/v1/repositories/test/vocabularies/?type_name=course', + type: 'GET', + responseText: vocabularyResponse + }); + var afterMount = function(component) { + var node = React.findDOMNode(component); + waitForAjax(1, function() { + React.addons.TestUtils.Simulate.click( + $(node).find("h2 .cd-btn")[0] + ); + + waitForAjax(2, function() { + assert.equal(component.state.currentResourceId, 1); + assert.deepEqual( + component.state.loadedPanels, + {"tab-1": true} ); - $(container).find("ins").first().click(); + $("a[href='#tab-2']").click(); waitForAjax(1, function() { - // Note lack of '?' - assert.equal(window.location.toString(), oldLocation); + assert.deepEqual( + component.state.loadedPanels, + { + "tab-1": true, + "tab-2": true + } + ); done(); }); }); }); - } - ); + }; + + var options = { + allExports: listingOptions.allExports, + sortingOptions: listingOptions.sortingOptions, + imageDir: listingOptions.imageDir, + pageSize: 25, + repoSlug: listingOptions.repoSlug, + loggedInUsername: listingOptions.loggedInUsername, + updateQueryString: updateQueryString, + getQueryString: getQueryString, + ref: afterMount + }; + + React.addons.TestUtils.renderIntoDocument( + React.createElement(Listing.ListingContainer, options) + ); + }); + + QUnit.test('Open and close exports panel', function(assert) { + var done = assert.async(); + + TestUtils.initMockjax({ + url: '/api/v1/repositories/test/learning_resource_exports/test/', + type: 'GET', + responseText: { + "count": 1, + "next": null, + "previous": null, + "results": [{"id": 123}] + } + }); + TestUtils.initMockjax({ + url: '/api/v1/repositories/test/learning_resources/?id=123', + type: 'GET', + responseText: { + "count": 1, + "next": null, + "previous": null, + "results": [ + learningResourceResponse + ] + } + }); + var afterMount = function(component) { + var node = React.findDOMNode(component); + waitForAjax(1, function() { + assert.ok(!$('.cd-panel-exports').hasClass("is-visible")); + React.addons.TestUtils.Simulate.click( + $(node).find("button:contains('Export')")[0] + ); + waitForAjax(2, function() { + + assert.ok($('.cd-panel-exports').hasClass("is-visible")); + + $(".cd-panel-exports .cd-panel-close").click(); + assert.ok(!$('.cd-panel-exports').hasClass("is-visible")); + + done(); + }); + }); + }; + + var options = { + allExports: listingOptions.allExports, + sortingOptions: listingOptions.sortingOptions, + imageDir: listingOptions.imageDir, + pageSize: 25, + repoSlug: listingOptions.repoSlug, + loggedInUsername: listingOptions.loggedInUsername, + updateQueryString: updateQueryString, + getQueryString: getQueryString, + ref: afterMount + }; + + React.addons.TestUtils.renderIntoDocument( + React.createElement(Listing.ListingContainer, options) + ); + + }); } ); diff --git a/ui/static/ui/js/listing.js b/ui/static/ui/js/listing.js index b37a4c82..a997bda1 100644 --- a/ui/static/ui/js/listing.js +++ b/ui/static/ui/js/listing.js @@ -1,218 +1,229 @@ define('listing', - ['csrf', 'jquery', 'lodash', 'uri', 'history', 'manage_taxonomies', + ['csrf', 'react', 'jquery', 'lodash', 'uri', 'history', 'manage_taxonomies', 'learning_resources', 'static_assets', 'utils', - 'lr_exports', 'listing_resources', 'pagination', 'xml_panel', + 'lr_exports', 'listing_resources', 'xml_panel', 'bootstrap', 'icheck'], - function (CSRF, $, _, URI, History, + function (CSRF, React, $, _, URI, History, ManageTaxonomies, LearningResources, StaticAssets, - Utils, Exports, ListingResources, Pagination, XmlPanel) { + Utils, Exports, ListingResources, XmlPanel) { 'use strict'; - var loader = function (listingOptions, container) { - var repoSlug = listingOptions.repoSlug; - var loggedInUsername = listingOptions.loggedInUsername; - - // empty results and facetCounts to start with - listingOptions = $.extend({}, listingOptions); - listingOptions.resources = []; - listingOptions.facetCounts = {}; - - CSRF.setupCSRF(); - - var EMAIL_EXTENSION = '@mit.edu'; - - function formatGroupName(string) { - string = string.charAt(0).toUpperCase() + string.slice(1); - return string.substring(0, string.length - 1); - } - - /* This is going to grow up to check whether - * the name is a valid Kerberos account - */ - function isEmail(email) { - var regex = /^[-a-z0-9~!$%^&*_=+}{\'?]+(\.[-a-z0-9~!$%^&*_=+}{\'?]+)*@([a-z0-9_][-a-z0-9_]*(\.[-a-z0-9_]+)*\.(aero|arpa|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro|travel|mobi|[a-z][a-z])|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,5})?$/i; - return regex.test(email); - } - - function formatUserGroups(userList, dest) { - var $dest = $(dest); - for (var i = 0; i < userList.length; i++) { - $dest.append( - '
' + - '
' + - '
' + - userList[i].username + EMAIL_EXTENSION + '
' + - '
\n' + - '
' + - '
' + - formatGroupName(userList[i].group_type) + '
' + - '
\n' + - '
' + - '' + - '
' + - '
\n'); - } - } - - function resetUserGroupForm() { - $('input[name=\'members-username\']').val(''); - $('select[name=\'members-group_type\']').prop('selectedIndex', 0); - } - - function showMembersAlert(message, mtype) { - //default value for mtype - mtype = typeof mtype !== 'undefined' ? mtype : 'success'; - //reset all the classes - $('#members-alert').html( - '
' + - '×\n' + message + - '
'); - } - - function showUpdateAllMembers() { - //retrieve all the members - var url = $('.cd-panel-members').data('members-url'); - return Utils.getCollection(url) - .then(function (results) { - $('#cd-panel-members-list').empty(); - formatUserGroups(results, '#cd-panel-members-list'); - }) - .fail(function () { - var message = 'Unable to retrieve list of members.'; - showMembersAlert(message, 'danger'); - }); - } - function closeLearningResourcePanel() { - $('.cd-panel').removeClass('is-visible'); + var EMAIL_EXTENSION = '@mit.edu'; + + function formatGroupName(string) { + string = string.charAt(0).toUpperCase() + string.slice(1); + return string.substring(0, string.length - 1); + } + + /* This is going to grow up to check whether + * the name is a valid Kerberos account + */ + function isEmail(email) { + var regex = /^[-a-z0-9~!$%^&*_=+}{\'?]+(\.[-a-z0-9~!$%^&*_=+}{\'?]+)*@([a-z0-9_][-a-z0-9_]*(\.[-a-z0-9_]+)*\.(aero|arpa|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro|travel|mobi|[a-z][a-z])|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,5})?$/i; + return regex.test(email); + } + + function formatUserGroups(userList, dest) { + var $dest = $(dest); + for (var i = 0; i < userList.length; i++) { + $dest.append( + '
' + + '
' + + '
' + + userList[i].username + EMAIL_EXTENSION + '
' + + '
\n' + + '
' + + '
' + + formatGroupName(userList[i].group_type) + '
' + + '
\n' + + '
' + + '' + + '
' + + '
\n'); } + } + + function resetUserGroupForm() { + $('input[name=\'members-username\']').val(''); + $('select[name=\'members-group_type\']').prop('selectedIndex', 0); + } + + function showMembersAlert(message, mtype) { + //default value for mtype + mtype = typeof mtype !== 'undefined' ? mtype : 'success'; + //reset all the classes + $('#members-alert').html( + '
' + + '×\n' + message + + '
'); + } + + function showUpdateAllMembers() { + //retrieve all the members + var url = $('.cd-panel-members').data('members-url'); + return Utils.getCollection(url) + .then(function (results) { + $('#cd-panel-members-list').empty(); + formatUserGroups(results, '#cd-panel-members-list'); + }) + .fail(function () { + var message = 'Unable to retrieve list of members.'; + showMembersAlert(message, 'danger'); + }); + } + function closeLearningResourcePanel() { + $('.cd-panel').removeClass('is-visible'); + } - var hideTaxonomyPanel = function() { - $('.cd-panel-2').removeClass('is-visible'); - }; + var hideTaxonomyPanel = function () { + $('.cd-panel-2').removeClass('is-visible'); + }; - var showTab = function(tabId) { - $("a[href='#" + tabId + "'").tab('show'); - }; + var showTab = function (tabId) { + $("a[href='#" + tabId + "']").tab('show'); + }; - var setTabName = function(tabId, newTabName) { - $("a[href='#" + tabId + "']").find("span").text(newTabName); - }; + var setTabName = function (tabId, newTabName) { + $("a[href='#" + tabId + "']").find("span").text(newTabName); + }; - var loadManageTaxonomies = function () { - ManageTaxonomies.loader( - repoSlug, - $('#taxonomy-component')[0], - showConfirmationDialog, - showTab, - setTabName, - refreshFromAPI - ); - }; + var showConfirmationDialog = function (options) { + var container = $("#confirmation-container")[0]; + Utils.showConfirmationDialog( + options, + container + ); + }; - $('[data-toggle=popover]').popover(); - //Close panels on escape keypress - $(document).keyup(function(event) { - if (event.keyCode === 27) { // escape key maps to keycode `27` - closeLearningResourcePanel(); - if ($('.cd-panel-2').hasClass('is-visible')) { - hideTaxonomyPanel(); - } - if ($('.cd-panel-exports').hasClass('is-visible')) { - $('.cd-panel-exports').removeClass('is-visible'); - } - if ($('.cd-panel-members').hasClass('is-visible')) { - $('.cd-panel-members').removeClass('is-visible'); + var ListingContainer = React.createClass({ + displayName: 'ListingContainer', + // make data structure to store query parameters + makeQueryMap: function() { + var queryMap = {}; + // Populate queryMap with query string key value pairs. + _.each(URI(this.props.getQueryString()).query(true), function (v, k) { + if (!Array.isArray(v)) { + // URI().query(true) will put two or more values with the same + // key in an array and not use an array for single values. + // Put everything into arrays for consistency's sake. + queryMap[k] = [v]; + } else { + queryMap[k] = v; } - event.preventDefault(); - } - }); - - //close the lateral panel - $('.cd-panel').on('click', function (event) { - if ($(event.target).is('.cd-panel') || - $(event.target).is('.cd-panel-close')) { - closeLearningResourcePanel(); - event.preventDefault(); + }); + return queryMap; + }, + // Return query portion of URL + makeQueryString: function(queryMap) { + var newQuery = "?" + URI().search(queryMap).query(); + if (newQuery === "?") { + newQuery = ""; } - }); - - //open the lateral panel - $('.btn-taxonomies').on('click', function (event) { - event.preventDefault(); - loadManageTaxonomies(); - $('.cd-panel-2').addClass('is-visible'); - }); - - //close the lateral panel - $('.cd-panel-2').on('click', function (event) { - if ($(event.target).is('.cd-panel-2') || - $(event.target).is('.cd-panel-close')) { - hideTaxonomyPanel(); - event.preventDefault(); + return newQuery; + }, + // Update URL in browser + updateQuery: function(queryMap) { + var newQuery = this.makeQueryString(queryMap); + this.props.updateQueryString(newQuery); + }, + getPageNum: function() { + var queryMap = this.makeQueryMap(); + var pageNum = 1; + if (queryMap.page !== undefined) { + pageNum = parseInt(queryMap.page[0]); } - }); - - /** - * Render resource items in the UI - * - * @returns {ReactElement} The rendered resource items - */ - var renderListingResources; - - // queryMap is the canonical place to manage query parameters for the UI. - // The URL in the browser will be updated with these changes in - // refreshFromAPI. - var queryMap = {}; - - // Controls the loader on the listing page. - var pageLoaded = false; - - // Populate queryMap with query string key value pairs. - _.each(URI(window.location).query(true), function(v, k) { - if (!Array.isArray(v)) { - // URI().query(true) will put two or more values with the same - // key in an array and not use an array for single values. - // Put everything into arrays for consistency's sake. - queryMap[k] = [v]; - } else { - queryMap[k] = v; - } - }); - - // This should get updated after the first API call - var numPages = 0; - var pageNum = 1; - if (queryMap.page !== undefined) { - pageNum = parseInt(queryMap.page[0]); - } - + return pageNum; + }, + getInitialState: function () { + return { + // empty resources and facetCounts to start with + resources: [], + facetCounts: {}, + // Controls the loader on the listing page. + pageLoaded: false, + allExports: this.props.allExports, + sortingOptions: this.props.sortingOptions, + // This should get updated after the first API call + numPages: 0, + // Will be set on refresh + selectedFacets: {}, + selectedMissingFacets: {}, + // Keep track of resource id so we can lazy load panels. + currentResourceId: undefined, + // What panels are already loaded + loadedPanels: {} + }; + }, + render: function () { + var options = { + repoSlug: this.props.repoSlug, + allExports: this.state.allExports, + sortingOptions: this.state.sortingOptions, + loggedInUsername: this.props.loggedInUsername, + imageDir: this.props.imageDir, + pageSize: this.props.pageSize, + openExportsPanel: this.openExportsPanel, + openResourcePanel: this.openResourcePanel, + updateFacets: this.updateFacets, + updateMissingFacets: this.updateMissingFacets, + selectedFacets: this.state.selectedFacets, + selectedMissingFacets: this.state.selectedMissingFacets, + updateSort: this.updateSort, + pageNum: this.getPageNum(), + numPages: this.state.numPages, + updatePage: this.updatePage, + loaded: this.state.pageLoaded, + resources: this.state.resources, + facetCounts: this.state.facetCounts, + ref: "listingResources" + }; + return React.createElement(ListingResources.ListingPage, options); + }, /** * Clears exports on page. Assumes DELETE to clear on server already * happened. */ - var clearExports = function () { + clearExports: function () { // Clear export links. - listingOptions = $.extend({}, listingOptions); - listingOptions.allExports = []; - - var listingResources = renderListingResources(); - listingResources.clearSelectedExports(); - }; + this.setState({allExports: []}); - var openExportsPanel = function(exportCount) { + // TODO: we shouldn't need a ref here, state should be moved up here + this.refs.listingResources.clearSelectedExports(); + }, + loadManageTaxonomies: function () { + ManageTaxonomies.loader( + this.props.repoSlug, + $('#taxonomy-component')[0], + showConfirmationDialog, + showTab, + setTabName, + this.refreshFromAPI + ); + }, + openExportsPanel: function (exportCount) { $('.cd-panel-exports').addClass('is-visible'); - Exports.loader(repoSlug, loggedInUsername, clearExports, - $("#exports_content")[0]); + Exports.loader( + this.props.repoSlug, + this.props.loggedInUsername, + this.clearExports, + $("#exports_content")[0] + ); Exports.loadExportsHeader(exportCount, $("#exports_heading")[0]); - }; - + }, + getPanelLoaders: function() { + return { + "tab-1": this.loadResourceTab, + "tab-2": this.loadXmlTab, + "tab-3": this.loadStaticAssetsTab + }; + }, /** * When called, query search API using query parameters of this URL * and update listing with updated resources. @@ -220,104 +231,79 @@ define('listing', * @returns {jQuery.Deferred} A promise that's resolved or rejected when * the AJAX call completes and the rerender is triggered. */ - var refreshFromAPI; - - // Will be set in refreshFromAPI - var selectedFacets; - var selectedMissingFacets; + refreshFromAPI: function() { + this.setState({pageLoaded: false}); - var loadResourceTab = function(resourceId) { - LearningResources.loader( - repoSlug, - resourceId, - refreshFromAPI, - closeLearningResourcePanel, - $("#tab-1")[0] - ); - }; - - var loadXmlTab = function(resourceId) { - XmlPanel.loader(repoSlug, resourceId, $("#tab-2")[0]); - }; - - var loadStaticAssetsTab = function(resourceId) { - StaticAssets.loader(repoSlug, resourceId, $("#tab-3")[0]); - }; - - // Keep track of resource id so we can lazy load panels. - var currentResourceId; - - var panelLoaders = { - "tab-1": loadResourceTab, - "tab-2": loadXmlTab, - "tab-3": loadStaticAssetsTab - }; - - var loadedPanels; - var openResourcePanel = function(resourceId) { - // Reset loaded panels - loadedPanels = {}; - currentResourceId = resourceId; - - // Load the resource tab - showTab("tab-1"); - loadResourceTab(currentResourceId); - loadedPanels["tab-1"] = true; - - $('.cd-panel').addClass('is-visible'); - }; - - _.each(panelLoaders, function(loader, tag) { - $("a[href='#" + tag + "']").click(function() { - // If tab not already loaded, load it now - if (!loadedPanels[tag]) { - loader(currentResourceId); - loadedPanels[tag] = true; - } - }); - }); - - refreshFromAPI = function() { - pageLoaded = false; - - var newQuery = "?" + URI().search(queryMap).query(); - if (newQuery === "?") { - // Don't put ? in URL if empty - History.replaceState(null, document.title, "."); - newQuery = ""; - } else { - History.replaceState(null, document.title, newQuery); - } + var queryMap = this.makeQueryMap(); + var newQuery = this.makeQueryString(queryMap); + // Query string for repository page is the same used for the search API var url = "/api/v1/repositories/" + - listingOptions.repoSlug + "/search/" + newQuery; - - renderListingResources(); - return $.get(url).then(function(collection) { - listingOptions = $.extend({}, listingOptions); - listingOptions.resources = collection.results; - listingOptions.facetCounts = collection.facet_counts; - selectedFacets = collection.selected_facets; - selectedMissingFacets = collection.selected_missing_facets; + this.props.repoSlug + "/search/" + newQuery; + + var thiz = this; + return $.get(url).then(function (collection) { + thiz.setState({ + resources: collection.results, + facetCounts: collection.facet_counts, + selectedFacets: collection.selected_facets, + selectedMissingFacets: collection.selected_missing_facets + }); - numPages = Math.ceil(collection.count / listingOptions.pageSize); + var numPages = Math.ceil(collection.count / thiz.props.pageSize); + var oldPageNum = thiz.getPageNum(); + var pageNum = oldPageNum; if (pageNum > numPages) { pageNum = numPages - 1; if (pageNum < 1) { pageNum = 1; } } - }).fail(function(error) { + thiz.setState({ + numPages: numPages + }); + if (pageNum !== oldPageNum) { + queryMap.page = pageNum; + // Update URL string again for different pageNum + thiz.updateQuery(queryMap); + } + }).fail(function (error) { // Propagate error return $.Deferred().reject(error); }).always(function() { - pageLoaded = true; - renderListingResources(); + thiz.setState({pageLoaded: true}); }); - }; + }, + loadResourceTab: function (resourceId) { + LearningResources.loader( + this.props.repoSlug, + resourceId, + this.refreshFromAPI, + closeLearningResourcePanel, + $("#tab-1")[0] + ); + }, + loadXmlTab: function (resourceId) { + XmlPanel.loader(this.props.repoSlug, resourceId, $("#tab-2")[0]); + }, + loadStaticAssetsTab: function (resourceId) { + StaticAssets.loader(this.props.repoSlug, resourceId, $("#tab-3")[0]); + }, + openResourcePanel: function (resourceId) { + // Reset loaded panels + var loadedPanels = {}; + this.setState({currentResourceId: resourceId}); + + // Load the resource tab + showTab("tab-1"); + this.loadResourceTab(resourceId); + loadedPanels["tab-1"] = true; + this.setState({loadedPanels: loadedPanels}); - var updateFacetParam = function(param, selected) { - queryMap = $.extend({}, queryMap); + $('.cd-panel').addClass('is-visible'); + }, + updateFacetParam: function (param, selected) { + var queryMap = this.makeQueryMap(); queryMap.page = undefined; if (!queryMap.selected_facets) { @@ -326,18 +312,18 @@ define('listing', // Remove facet. If selected we'll add it back again with a push(). queryMap.selected_facets = _.filter( - queryMap.selected_facets, function(facet) { + queryMap.selected_facets, function (facet) { return facet !== param; } ); if (selected) { - queryMap.selected_facets.push(param); + queryMap.selected_facets = queryMap.selected_facets.concat(param); } - return refreshFromAPI(); - }; - + this.updateQuery(queryMap); + return this.refreshFromAPI(); + }, /** * Update queryMap with updated facet information, then refresh from API. * @@ -348,18 +334,16 @@ define('listing', * @returns {jQuery.Deferred} Promise which is resolved or rejected after * refresh occurs. */ - var updateFacets = function(facetId, valueId, selected) { + updateFacets: function (facetId, valueId, selected) { var param = facetId + "_exact:" + valueId; - return updateFacetParam(param, selected); - }; - - var updateMissingFacets = function(facetId, selected) { + return this.updateFacetParam(param, selected); + }, + updateMissingFacets: function (facetId, selected) { var param = "_missing_:" + facetId + "_exact"; - return updateFacetParam(param, selected); - }; - + return this.updateFacetParam(param, selected); + }, /** * Update sorting and refresh from API. * @@ -367,46 +351,38 @@ define('listing', * @return {jQuery.Deferred} A promise which is resolved or rejected after * refresh has occurred. */ - var updateSort = function(value) { - queryMap = $.extend({}, queryMap); + updateSort: function (value) { + var queryMap = this.makeQueryMap(); queryMap.sortby = value; - listingOptions = $.extend({}, listingOptions); - var allOptions = listingOptions.sortingOptions.all.concat([ - listingOptions.sortingOptions.current + var sortingOptions = this.state.sortingOptions; + var allOptions = sortingOptions.all.concat([ + sortingOptions.current ]); - var current = _.filter(allOptions, function(pair) { + var current = _.filter(allOptions, function (pair) { return pair[0] === value; }); - var all = _.filter(allOptions, function(pair) { + var all = _.filter(allOptions, function (pair) { return pair[0] !== value; }); - listingOptions.sortingOptions.all = all; - listingOptions.sortingOptions.current = current[0]; - - refreshFromAPI(); - }; - - /** - * Rerender listing resources - * @returns {ReactComponent} - */ - renderListingResources = function() { - return ListingResources.loader(listingOptions, - container, openExportsPanel, openResourcePanel, - updateFacets, updateMissingFacets, - selectedFacets, selectedMissingFacets, updateSort, pageNum, numPages, - updatePage, pageLoaded - ); - }; + this.setState({ + sortingOptions: { + all: all, + current: current[0] + } + }); + this.updateQuery(queryMap); + return this.refreshFromAPI(); + }, /** * Update search parameter then refresh from API. * * @param search {String} The search phrase * @returns {jQuery.Deferred} Promise which evalutes after refresh occurs. */ - var updateSearch = function (search) { + updateSearch: function (search) { + var queryMap = this.makeQueryMap(); queryMap.page = undefined; if (search !== '') { queryMap.q = [search]; @@ -417,156 +393,247 @@ define('listing', // clear facets queryMap.selected_facets = undefined; - return refreshFromAPI(); - }; + this.updateQuery(queryMap); + return this.refreshFromAPI(); + }, + /** + * Update page number and refresh from API. + * @param newPageNum {Number} New page number + * @return {jQuery.Deferred} Promise which resolves or rejects after + * refresh has occurred. + */ + updatePage: function (newPageNum) { + var queryMap = this.makeQueryMap(); + queryMap.page = [newPageNum.toString()]; + + this.updateQuery(queryMap); + return this.refreshFromAPI(); + }, - // If search is executed update query parameter and refresh from API. - $("#search_button").click(function(e) { - e.preventDefault(); + componentDidMount: function () { + CSRF.setupCSRF(); - var search = $("#id_q").val(); - updateSearch(search); - }); + var thiz = this; - // Close exports panel. - $('.cd-panel-exports').on('click', function (event) { - if ($(event.target).is('.cd-panel-exports') || - $(event.target).is('.cd-panel-close')) { - $('.cd-panel-exports').removeClass('is-visible'); + $('[data-toggle=popover]').popover(); + //Close panels on escape keypress + $(document).keyup(function (event) { + if (event.keyCode === 27) { // escape key maps to keycode `27` + closeLearningResourcePanel(); + if ($('.cd-panel-2').hasClass('is-visible')) { + hideTaxonomyPanel(); + } + if ($('.cd-panel-exports').hasClass('is-visible')) { + $('.cd-panel-exports').removeClass('is-visible'); + } + if ($('.cd-panel-members').hasClass('is-visible')) { + $('.cd-panel-members').removeClass('is-visible'); + } + event.preventDefault(); + } + }); + + //close the lateral panel + $('.cd-panel').on('click', function (event) { + if ($(event.target).is('.cd-panel') || + $(event.target).is('.cd-panel-close')) { + closeLearningResourcePanel(); + event.preventDefault(); + } + }); + + //open the lateral panel + $('.btn-taxonomies').on('click', function (event) { event.preventDefault(); - } - }); - - //open the lateral panel for members - $('.btn-members').on('click', function (event) { - event.preventDefault(); - //remove any alert - $('#members-alert').empty(); - //reset the form values - resetUserGroupForm(); - //make panel visible - $('.cd-panel-members').addClass('is-visible'); - //retrieve all the members - showUpdateAllMembers(); - // - }); - //close the lateral panel for members - $('.cd-panel-members').on('click', function (event) { - if ($(event.target).is('.cd-panel-members') || - $(event.target).is('.cd-panel-close')) { - $('.cd-panel-members').removeClass('is-visible'); + thiz.loadManageTaxonomies(); + $('.cd-panel-2').addClass('is-visible'); + }); + + //close the lateral panel + $('.cd-panel-2').on('click', function (event) { + if ($(event.target).is('.cd-panel-2') || + $(event.target).is('.cd-panel-close')) { + hideTaxonomyPanel(); + event.preventDefault(); + } + }); + + _.each(this.getPanelLoaders(), function (loader, tag) { + $("a[href='#" + tag + "']").click(function () { + // If tab not already loaded, load it now + if (!thiz.state.loadedPanels[tag]) { + loader(thiz.state.currentResourceId); + var loadedPanels = $.extend({}, thiz.state.loadedPanels); + loadedPanels[tag] = true; + thiz.setState({ + loadedPanels: loadedPanels + }); + } + }); + }); + + // If search is executed update query parameter and refresh from API. + $("#search_button").click(function (e) { + e.preventDefault(); + + var search = $("#id_q").val(); + thiz.updateSearch(search); + }); + + // Close exports panel. + $('.cd-panel-exports').on('click', function (event) { + if ($(event.target).is('.cd-panel-exports') || + $(event.target).is('.cd-panel-close')) { + $('.cd-panel-exports').removeClass('is-visible'); + event.preventDefault(); + } + }); + + //open the lateral panel for members + $('.btn-members').on('click', function (event) { event.preventDefault(); - } - }); - //add button for the members - $(document).on('click', '.cd-panel-members-add', function () { - var url = $('.cd-panel-members').data('members-url'); - var username = $('input[name=\'members-username\']').val(); - var groupType = $('select[name=\'members-group_type\']').val(); - // /api/v1/repositories/my-rep/members/groups//users/ - url += 'groups/' + groupType + '/users/'; - //test that username is not an email - if (isEmail(username)) { - var message = 'Please type only your username before the @'; - showMembersAlert(message, 'warning'); - return; - } - var email = username + EMAIL_EXTENSION; - if (!isEmail(email)) { - var emailMessage = '' + email + - ' does not seem to be a valid email'; - showMembersAlert(emailMessage, 'danger'); - return; - } - $.ajax({ - url: url, - type: 'POST', - data: {username: username} - }) - .then(function() { - //retrieve the members lists - return showUpdateAllMembers(); - }) - .then(function () { - //reset the values + //remove any alert + $('#members-alert').empty(); + //reset the form values resetUserGroupForm(); - //show alert - var message = '' + email + - ' added to group ' + - formatGroupName(groupType) + ''; - showMembersAlert(message); - }) - .fail(function (data) { - //show alert - var message = 'Error adding user ' + email + - ' to group ' + formatGroupName(groupType); - if (data && data.responseJSON && data.responseJSON.username) { - message = message + '
' + data.responseJSON.username[0]; + //make panel visible + $('.cd-panel-members').addClass('is-visible'); + //retrieve all the members + showUpdateAllMembers(); + }); + + //close the lateral panel for members + $('.cd-panel-members').on('click', function (event) { + if ($(event.target).is('.cd-panel-members') || + $(event.target).is('.cd-panel-close')) { + $('.cd-panel-members').removeClass('is-visible'); + event.preventDefault(); } - showMembersAlert(message, 'danger'); }); - }); - //remove button for the members - $(document).on('click', '.cd-panel-members-remove', function () { - var url = $('.cd-panel-members').data('members-url'); - var username = $(this).data('username'); - var groupType = $(this).data('group_type'); - var email = username + EMAIL_EXTENSION; - // /api/v1/repositories/my-rep/members/groups//users// - url += 'groups/' + groupType + '/users/' + username + '/'; - $.ajax({ - url: url, - type: 'DELETE' - }) - .then(function() { - //retrieve the members lists - return showUpdateAllMembers(); - }) - .then(function () { - //show alert - var message = '' + email + - ' deleted from group ' + - formatGroupName(groupType) + ''; - showMembersAlert(message); - }) - .fail(function (data) { - //show alert - var message = 'Error deleting user ' + - email + ' from group ' + - formatGroupName(groupType) + ''; - if (data && data.responseJSON && data.responseJSON.detail) { - message += '
' + data.responseJSON.detail; + + //add button for the members + $(document).on('click', '.cd-panel-members-add', function () { + var url = $('.cd-panel-members').data('members-url'); + var username = $('input[name=\'members-username\']').val(); + var groupType = $('select[name=\'members-group_type\']').val(); + // /api/v1/repositories/my-rep/members/groups//users/ + url += 'groups/' + groupType + '/users/'; + //test that username is not an email + if (isEmail(username)) { + var message = 'Please type only your username before the @'; + showMembersAlert(message, 'warning'); + return; } - showMembersAlert(message, 'danger'); + var email = username + EMAIL_EXTENSION; + if (!isEmail(email)) { + var emailMessage = '' + email + + ' does not seem to be a valid email'; + showMembersAlert(emailMessage, 'danger'); + return; + } + $.ajax({ + url: url, + type: 'POST', + data: {username: username} + }) + .then(function () { + //retrieve the members lists + return showUpdateAllMembers(); + }) + .then(function () { + //reset the values + resetUserGroupForm(); + //show alert + var message = '' + email + + ' added to group ' + + formatGroupName(groupType) + ''; + showMembersAlert(message); + }) + .fail(function (data) { + //show alert + var message = 'Error adding user ' + email + + ' to group ' + formatGroupName(groupType); + if (data && data.responseJSON && data.responseJSON.username) { + message = message + '
' + data.responseJSON.username[0]; + } + showMembersAlert(message, 'danger'); + }); }); - }); - /** - * Update page number and refresh from API. - * @param newPageNum {Number} New page number - * @return {jQuery.Deferred} Promise which resolves or rejects after - * refresh has occurred. - */ - var updatePage = function(newPageNum) { - queryMap.page = [newPageNum.toString()]; - pageNum = parseInt(newPageNum); + //remove button for the members + $(document).on('click', '.cd-panel-members-remove', function () { + var url = $('.cd-panel-members').data('members-url'); + var username = $(this).data('username'); + var groupType = $(this).data('group_type'); + var email = username + EMAIL_EXTENSION; + // /api/v1/repositories/my-rep/members/groups//users// + url += 'groups/' + groupType + '/users/' + username + '/'; + $.ajax({ + url: url, + type: 'DELETE' + }) + .then(function () { + //retrieve the members lists + return showUpdateAllMembers(); + }) + .then(function () { + //show alert + var message = '' + email + + ' deleted from group ' + + formatGroupName(groupType) + ''; + showMembersAlert(message); + }) + .fail(function (data) { + //show alert + var message = 'Error deleting user ' + + email + ' from group ' + + formatGroupName(groupType) + ''; + if (data && data.responseJSON && data.responseJSON.detail) { + message += '
' + data.responseJSON.detail; + } + showMembersAlert(message, 'danger'); + }); + }); - return refreshFromAPI(); - }; + // Initial refresh to populate page. + thiz.refreshFromAPI(); + } + }); - var showConfirmationDialog = function(options) { - var container = $("#confirmation-container")[0]; - Utils.showConfirmationDialog( - options, - container - ); + var updateQueryString = function(newQuery) { + if (newQuery.length === 0) { + History.replaceState(null, document.title, "."); + } else { + History.replaceState(null, document.title, newQuery); + } + }; + + var getQueryString = function() { + return URI(window.location).search(); + }; + + var loader = function (listingOptions, container) { + var options = { + allExports: listingOptions.allExports, + sortingOptions: listingOptions.sortingOptions, + imageDir: listingOptions.imageDir, + pageSize: listingOptions.pageSize, + repoSlug: listingOptions.repoSlug, + loggedInUsername: listingOptions.loggedInUsername, + updateQueryString: updateQueryString, + getQueryString: getQueryString }; - // Initial refresh to populate page. - refreshFromAPI(); + React.render( + React.createElement(ListingContainer, options), + container + ); }; return { + ListingContainer: ListingContainer, + isEmail: isEmail, + formatGroupName: formatGroupName, loader: loader }; });