From f31eff86925cc3e284ef31453b83f85acf1fe5ee Mon Sep 17 00:00:00 2001 From: Amir Qayyum Khan Date: Mon, 5 Oct 2015 20:37:40 +0500 Subject: [PATCH] Added warning message when user try to close learning resource panel without saving --- .../test-learning-resource.jsx | 11 +- .../test_learning_resource_panel.jsx | 130 ++++++++++++++++++ ui/jstests/listing/test_listing.jsx | 58 ++++++++ .../learning_resource_panel.jsx | 35 ++++- .../learningresources/learning_resources.jsx | 17 +-- ui/static/ui/js/listing/listing.jsx | 67 +++++++-- 6 files changed, 290 insertions(+), 28 deletions(-) diff --git a/ui/jstests/learningresources/test-learning-resource.jsx b/ui/jstests/learningresources/test-learning-resource.jsx index fd99f80c..502a1769 100644 --- a/ui/jstests/learningresources/test-learning-resource.jsx +++ b/ui/jstests/learningresources/test-learning-resource.jsx @@ -44,9 +44,14 @@ define( function (assert) { var div = document.createElement("div"); assert.ok($(div).html().length === 0); - LearningResources.loader("repo", "1", function () { - }, function () { - }, div); + var options = { + "repoSlug": "repo", + "learningResourceId": "1", + "refreshFromAPI": this.refreshFromAPI, + "markDirty": function() {}, + "closeLearningResourcePanel": function() {} + }; + LearningResources.loader(options, div); assert.ok($(div).html().length > 0); } ); diff --git a/ui/jstests/learningresources/test_learning_resource_panel.jsx b/ui/jstests/learningresources/test_learning_resource_panel.jsx index d1bbcda3..228503fd 100644 --- a/ui/jstests/learningresources/test_learning_resource_panel.jsx +++ b/ui/jstests/learningresources/test_learning_resource_panel.jsx @@ -248,11 +248,136 @@ define(["QUnit", "react", "test_utils", "jquery", "lodash", React.addons.TestUtils.renderIntoDocument(); } ); + + QUnit.test( + 'Assert that mark dirty sets properly when description change.', + function (assert) { + var done = assert.async(); + + var markDirtyState = false; + var markDirty = function (state) { + markDirtyState = state; + }; + + var afterMount = function (component) { + // wait for calls to populate form + waitForAjax(3, function () { + var textarea = React.addons.TestUtils. + findRenderedDOMComponentWithTag( + component, + 'textarea' + ); + React.addons.TestUtils.Simulate.change( + textarea, {target: {value: "x"}} + ); + component.forceUpdate(function () { + assert.equal(component.state.description, "x"); + assert.ok(markDirtyState, "Marked dirty"); + done(); + }); + }); + }; + React.addons.TestUtils.renderIntoDocument(); + } + ); + + QUnit.test( + 'Assert that mark dirty sets properly when terms change.', + function (assert) { + var done = assert.async(); + var markDirtyState = false; + var markDirty = function (state) { + markDirtyState = state; + }; + var closeLearningResourcePanel = function () { + }; + + var afterMount = function (component) { + // wait for calls to populate form + waitForAjax(3, function () { + // two menus: vocabulary and terms. + var $node = $(React.findDOMNode(component)); + + var $allSelects = $node.find("#vocabularies select"); + assert.equal($allSelects.size(), 2); + + var $vocabSelect = $node.find($allSelects).first(); + assert.equal($vocabSelect.size(), 1); + + // first vocab, two options + var $vocabOptions = $vocabSelect.find("option"); + assert.equal($vocabOptions.size(), 2); + + assert.equal($vocabOptions[0].selected, true); + assert.equal($vocabOptions[1].selected, false); + + // TestUtils.Simulate.change only simulates a change event, + // we need to update the value first ourselves + $vocabSelect.val("prerequisite").trigger('change'); + component.forceUpdate(function () { + assert.equal($vocabOptions[0].selected, false); + assert.equal($vocabOptions[1].selected, true); + // re-fetch the selects to get the prerequisite one + $allSelects = $node.find("#vocabularies select"); + var termsSelect = $allSelects[1]; + var $termsOptions = $(termsSelect).find("option"); + assert.equal($termsOptions.size(), 2); + assert.equal($termsOptions[0].selected, true); + assert.equal($termsOptions[1].selected, false); + + // remove selection for this vocabulary to not interfere to the rest of the test + $(termsSelect).val("").trigger('change'); + component.forceUpdate(function () { + // Switch to difficulty + $vocabSelect.val("difficulty").trigger('change'); + component.forceUpdate(function () { + // re-fetch the selects to get the difficulty one + $allSelects = $node.find("#vocabularies select"); + // update the term select + termsSelect = $allSelects[1]; + $termsOptions = $(termsSelect).find("option"); + + assert.equal($termsOptions.size(), 2); + assert.equal($termsOptions[0].selected, false); + assert.equal($termsOptions[1].selected, false); + // the second vocabulary can be a multi select + $(termsSelect) + .val(["hard", "easy"]) + .trigger('change'); + markDirtyState = false; + component.forceUpdate(function () { + // when terms are change then markDirtyState flag is set. + assert.equal(markDirtyState, true); + assert.equal($termsOptions[0].selected, true); + assert.equal($termsOptions[1].selected, true); + done(); + }); + }); + }); + }); + }); + }; + + React.addons.TestUtils.renderIntoDocument(); + } + ); + QUnit.test( 'Assert that LearningResourcePanel saves properly', function (assert) { @@ -300,6 +425,7 @@ define(["QUnit", "react", "test_utils", "jquery", "lodash", React.addons.TestUtils.renderIntoDocument(); @@ -353,6 +479,7 @@ define(["QUnit", "react", "test_utils", "jquery", "lodash", React.addons.TestUtils.renderIntoDocument(); @@ -418,6 +546,7 @@ define(["QUnit", "react", "test_utils", "jquery", "lodash", }; React.addons.TestUtils.renderIntoDocument(); @@ -482,6 +611,7 @@ define(["QUnit", "react", "test_utils", "jquery", "lodash", ); diff --git a/ui/jstests/listing/test_listing.jsx b/ui/jstests/listing/test_listing.jsx index eedf7952..f84b2d0e 100644 --- a/ui/jstests/listing/test_listing.jsx +++ b/ui/jstests/listing/test_listing.jsx @@ -971,5 +971,63 @@ define(['QUnit', 'jquery', 'react', 'test_utils', 'utils', 'listing'], ); }); + + QUnit.test('Open and close learning resource panel', function(assert) { + var done = assert.async(); + TestUtils.initMockjax({ + url: '/api/v1/repositories/test/learning_resources/' + + '1/?remove_content_xml=true', + type: 'GET', + responseText: learningResourceResponse + }); + TestUtils.initMockjax({ + url: '/api/v1/repositories/test/vocabularies/?type_name=course', + type: 'GET', + responseText: vocabularyResponse + }); + + var afterMount = function(component) { + waitForAjax(1, function() { + assert.notOk($('.cd-panel').hasClass("is-visible")); + component.openResourcePanel(1); + waitForAjax(2, function() { + assert.equal(component.state.currentResourceId , 1); + assert.ok($('.cd-panel').hasClass("is-visible")); + component.setState({ + isLearningResourcePanelDirty: true + }, function () { + component.closeLearningResourcePanel(); + assert.ok($('.cd-panel').hasClass("is-visible"), + "LR Panel should not close because it is marked dirty"); + component.setState({ + isLearningResourcePanelDirty: false + }, function() { + component.closeLearningResourcePanel(); + assert.notOk($('.cd-panel').hasClass("is-visible"), + "LR Panel should close"); + done(); + }); + }); + }); + }); + }; + + var options = { + allExports: listingOptions.allExports, + sortingOptions: listingOptions.sortingOptions, + imageDir: listingOptions.imageDir, + pageSize: 25, + repoSlug: listingOptions.repoSlug, + loggedInUsername: listingOptions.loggedInUsername, + updateQueryString: updateQueryString, + getQueryString: getQueryString, + showConfirmationDialog: function() {}, + ref: afterMount + }; + + React.addons.TestUtils.renderIntoDocument( + React.createElement(Listing.ListingContainer, options) + ); + }); } ); diff --git a/ui/static/ui/js/learningresources/learning_resource_panel.jsx b/ui/static/ui/js/learningresources/learning_resource_panel.jsx index ca65940c..12c1d676 100644 --- a/ui/static/ui/js/learningresources/learning_resource_panel.jsx +++ b/ui/static/ui/js/learningresources/learning_resource_panel.jsx @@ -35,6 +35,8 @@ define("learning_resource_panel", ['react', 'jquery', 'lodash', this.setState({ vocabulariesAndTerms: newVocabulariesAndTerms + }, function () { + this.updateDirty(); }); }, @@ -115,8 +117,7 @@ define("learning_resource_panel", ['react', 'jquery', 'lodash', - @@ -133,8 +134,14 @@ define("learning_resource_panel", ['react', 'jquery', 'lodash', this.setState({ description: event.target.value, message: undefined + }, function() { + this.updateDirty(); }); }, + closeLearningResourcePanel: function(event) { + event.preventDefault(); + this.props.closeLearningResourcePanel(); + }, saveLearningResourcePanel: function (event) { event.preventDefault(); this.saveForm(false); @@ -165,8 +172,12 @@ define("learning_resource_panel", ['react', 'jquery', 'lodash', data: JSON.stringify(data) }).then(function () { thiz.setState({ - message: "Form saved successfully!" + message: "Form saved successfully!", + descriptionOriginal: thiz.state.description, + vocabulariesAndTermsOriginal: thiz.state.vocabulariesAndTerms }); + // user can close panel + thiz.updateDirty(); if (closePanel) { thiz.props.closeLearningResourcePanel(); } @@ -179,6 +190,16 @@ define("learning_resource_panel", ['react', 'jquery', 'lodash', thiz.setState({loaded: true}); }); }, + updateDirty: function() { + // When use change description or terms the mark panel dirty + var isDirty = !_.eq( + this.state.description, this.state.descriptionOriginal + ) || !_.eq( + this.state.vocabulariesAndTerms, + this.state.vocabulariesAndTermsOriginal + ); + this.props.markDirty(isDirty); + }, componentDidMount: function () { var thiz = this; @@ -204,6 +225,7 @@ define("learning_resource_panel", ['react', 'jquery', 'lodash', message: undefined, description: description, previewUrl: previewUrl, + descriptionOriginal: description, }); return Utils.getVocabulariesAndTerms( thiz.props.repoSlug, learningResourceType) @@ -230,6 +252,9 @@ define("learning_resource_panel", ['react', 'jquery', 'lodash', thiz.setState({ vocabulariesAndTerms: vocabulariesAndTerms, + vocabulariesAndTermsOriginal: _.cloneDeep( + vocabulariesAndTerms + ) }); if (vocabulariesAndTerms.length) { @@ -255,7 +280,9 @@ define("learning_resource_panel", ['react', 'jquery', 'lodash', return { description: "", vocabulariesAndTerms: [], - selectedVocabulary: {} + selectedVocabulary: {}, + descriptionOriginal: "", + vocabulariesAndTermsOriginal: [] }; } }); diff --git a/ui/static/ui/js/learningresources/learning_resources.jsx b/ui/static/ui/js/learningresources/learning_resources.jsx index f793f851..6248afe8 100644 --- a/ui/static/ui/js/learningresources/learning_resources.jsx +++ b/ui/static/ui/js/learningresources/learning_resources.jsx @@ -4,17 +4,18 @@ define('learning_resources', 'use strict'; return { - loader: function (repoSlug, learningResourceId, refreshFromAPI, - closeLearningResourcePanel, container) { + /** Current list of options are: + repoSlug + learningResourceId + refreshFromAPI + markDirty + closeLearningResourcePanel + */ + loader: function (options, container) { // Unmount and remount the component to ensure that its state // is always up to date with the rest of the app. React.unmountComponentAtNode(container); - React.render(, container); + React.render(, container); } }; }); diff --git a/ui/static/ui/js/listing/listing.jsx b/ui/static/ui/js/listing/listing.jsx index a997bda1..2d32007b 100644 --- a/ui/static/ui/js/listing/listing.jsx +++ b/ui/static/ui/js/listing/listing.jsx @@ -78,9 +78,6 @@ define('listing', showMembersAlert(message, 'danger'); }); } - function closeLearningResourcePanel() { - $('.cd-panel').removeClass('is-visible'); - } var hideTaxonomyPanel = function () { $('.cd-panel-2').removeClass('is-visible'); @@ -158,7 +155,8 @@ define('listing', // Keep track of resource id so we can lazy load panels. currentResourceId: undefined, // What panels are already loaded - loadedPanels: {} + loadedPanels: {}, + isLearningResourcePanelDirty: false }; }, render: function () { @@ -186,6 +184,37 @@ define('listing', }; return React.createElement(ListingResources.ListingPage, options); }, + /** + * Use by close learning resource confirmation popup. + */ + resetAndHideLearningResourcePanel: function() { + $('.cd-panel').removeClass('is-visible'); + this.setState({ + currentResourceId: undefined, + isLearningResourcePanelDirty: false + }); + }, + confirmCloseLearningResourcePanel: function(status) { + if (status) { + this.resetAndHideLearningResourcePanel(); + } + }, + closeLearningResourcePanel: function() { + if (!this.state.isLearningResourcePanelDirty) { + this.resetAndHideLearningResourcePanel(); + } else { + var options = { + actionButtonName: "Close", + actionButtonClass: "btn btn-danger btn-ok", + title: "Confirm Exit", + message: "Are you sure you want to close this panel?", + description: "You have unsaved changes once you click close " + + "all changes will be lost.", + confirmationHandler: this.confirmCloseLearningResourcePanel + }; + this.props.showConfirmationDialog(options); + } + }, /** * Clears exports on page. Assumes DELETE to clear on server already * happened. @@ -201,7 +230,7 @@ define('listing', ManageTaxonomies.loader( this.props.repoSlug, $('#taxonomy-component')[0], - showConfirmationDialog, + this.props.showConfirmationDialog, showTab, setTabName, this.refreshFromAPI @@ -274,12 +303,19 @@ define('listing', thiz.setState({pageLoaded: true}); }); }, + markDirtyLearningResourcePanel: function (isDirty) { + this.setState({isLearningResourcePanelDirty: isDirty}); + }, loadResourceTab: function (resourceId) { + var options = { + "repoSlug": this.props.repoSlug, + "learningResourceId": resourceId, + "refreshFromAPI": this.refreshFromAPI, + "markDirty": this.markDirtyLearningResourcePanel, + "closeLearningResourcePanel": this.closeLearningResourcePanel + }; LearningResources.loader( - this.props.repoSlug, - resourceId, - this.refreshFromAPI, - closeLearningResourcePanel, + options, $("#tab-1")[0] ); }, @@ -299,7 +335,6 @@ define('listing', this.loadResourceTab(resourceId); loadedPanels["tab-1"] = true; this.setState({loadedPanels: loadedPanels}); - $('.cd-panel').addClass('is-visible'); }, updateFacetParam: function (param, selected) { @@ -419,7 +454,9 @@ define('listing', //Close panels on escape keypress $(document).keyup(function (event) { if (event.keyCode === 27) { // escape key maps to keycode `27` - closeLearningResourcePanel(); + if (!_.isUndefined(thiz.state.currentResourceId)) { + thiz.closeLearningResourcePanel(); + } if ($('.cd-panel-2').hasClass('is-visible')) { hideTaxonomyPanel(); } @@ -437,7 +474,9 @@ define('listing', $('.cd-panel').on('click', function (event) { if ($(event.target).is('.cd-panel') || $(event.target).is('.cd-panel-close')) { - closeLearningResourcePanel(); + if (!_.isUndefined(thiz.state.currentResourceId)) { + thiz.closeLearningResourcePanel(); + } event.preventDefault(); } }); @@ -621,7 +660,9 @@ define('listing', repoSlug: listingOptions.repoSlug, loggedInUsername: listingOptions.loggedInUsername, updateQueryString: updateQueryString, - getQueryString: getQueryString + getQueryString: getQueryString, + showConfirmationDialog: showConfirmationDialog + }; React.render(