diff --git a/app/partials/confirmSessionReset.html b/app/partials/confirmSessionReset.html new file mode 100644 index 0000000..dcdc187 --- /dev/null +++ b/app/partials/confirmSessionReset.html @@ -0,0 +1,12 @@ +

Session Reset

+

You are about to reset your session. Doing so will return you to the Select File and Validate page and you will need to resubmit your HMDA File for validation.

+

Do you really want to reset your session?

+ +
+
+ +
+
+ +
+
diff --git a/app/scripts/app.js b/app/scripts/app.js index 9b374c1..4085cb4 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -12,6 +12,7 @@ require('angular-fileupload'); require('./modules/config'); require('./modules/HMDAEngine'); require('./modules/hmdaFilters'); +require('ng-dialog'); /** * @ngdoc overview @@ -30,6 +31,7 @@ angular 'ngRoute', 'ngSanitize', 'ngTouch', + 'ngDialog', 'services.config', 'filereader', 'HMDAEngine', @@ -73,7 +75,7 @@ angular redirectTo: '/' }); }) - .run(function ($rootScope, $location, Configuration, HMDAEngine) { + .run(function ($rootScope, $window, $location, Configuration, HMDAEngine) { // Set the location of the HMDA Engine API HMDAEngine.setAPIURL(Configuration.apiUrl); @@ -86,6 +88,11 @@ angular $location.path('/'); } }); + + // Warn the user on browser refresh that they are about to destroy their session + $window.onbeforeunload = function() { + return 'You are about to reset your session.\n\nDoing so will return you to the Select File and Validate page and you will need to resubmit your HMDA File for validation.'; + }; }); require('./services'); diff --git a/app/scripts/controllers/summarySyntacticalValidity.js b/app/scripts/controllers/summarySyntacticalValidity.js index c1a0207..f3083e4 100644 --- a/app/scripts/controllers/summarySyntacticalValidity.js +++ b/app/scripts/controllers/summarySyntacticalValidity.js @@ -7,7 +7,7 @@ * # SummarySyntacticalValidityCtrl * Controller for the Syntactical and Validity Summary view */ -module.exports = /*@ngInject*/ function ($scope, $location, $q, $timeout, HMDAEngine, Wizard) { +module.exports = /*@ngInject*/ function ($scope, $location, $q, $timeout, HMDAEngine, Wizard, ngDialog) { // Populate the $scope $scope.errors = {}; @@ -20,7 +20,13 @@ module.exports = /*@ngInject*/ function ($scope, $location, $q, $timeout, HMDAEn $scope.validityErrors = editErrors.validity || {}; $scope.previous = function() { - $location.path('/'); + ngDialog.openConfirm({ + template: 'partials/confirmSessionReset.html' + }).then(function (value) { + if (value === 'reset') { + $location.path('/'); + } + }); }; $scope.hasNext = function() { diff --git a/app/scripts/controllers/validationSummary.js b/app/scripts/controllers/validationSummary.js index d7d04eb..5178fc2 100644 --- a/app/scripts/controllers/validationSummary.js +++ b/app/scripts/controllers/validationSummary.js @@ -7,7 +7,7 @@ * # ValidationSummaryCtrl * Controller of the hmdaPilotApp */ -module.exports = /*@ngInject*/ function ($scope, $location, FileMetadata, HMDAEngine) { +module.exports = /*@ngInject*/ function ($scope, $location, FileMetadata, HMDAEngine, ngDialog) { $scope.fileMetadata = FileMetadata.get(); $scope.transmittalSheet = HMDAEngine.getHmdaJson().hmdaFile.transmittalSheet; @@ -17,7 +17,12 @@ module.exports = /*@ngInject*/ function ($scope, $location, FileMetadata, HMDAEn }; $scope.startOver = function() { - // Go to the next page - $location.path('/selectFile'); + ngDialog.openConfirm({ + template: 'partials/confirmSessionReset.html' + }).then(function (value) { + if (value === 'reset') { + $location.path('/selectFile'); + } + }); }; }; diff --git a/app/scripts/directives/wizardNav.js b/app/scripts/directives/wizardNav.js index e678c87..6b755df 100644 --- a/app/scripts/directives/wizardNav.js +++ b/app/scripts/directives/wizardNav.js @@ -7,7 +7,7 @@ * # Wizard Nav directive * Directive for displaying the wizard navigation. */ -module.exports = /*@ngInject*/ function (StepFactory, Wizard) { +module.exports = /*@ngInject*/ function ($location, StepFactory, Wizard, ngDialog) { function getStepClass(step) { if (step.isActive) { @@ -35,6 +35,23 @@ module.exports = /*@ngInject*/ function (StepFactory, Wizard) { return step; } + function controller($scope) { + $scope.$on('$locationChangeStart', function(event, newUrl) { + if (newUrl.indexOf('#/selectFile') !== -1 ) { + ngDialog.openConfirm({ + template: 'partials/confirmSessionReset.html' + }).then(function (value) { + if (value === 'reset') { + $location.path('/'); + } + }); + event.preventDefault(); + } + + return; + }); + } + return { restrict: 'E', templateUrl: 'partials/wizardNav.html', @@ -64,6 +81,7 @@ module.exports = /*@ngInject*/ function (StepFactory, Wizard) { scope.steps = Wizard.getSteps(); } }); - } + }, + controller: /*@ngInject*/ controller }; }; diff --git a/app/styles/dialog.less b/app/styles/dialog.less new file mode 100644 index 0000000..8c19d4c --- /dev/null +++ b/app/styles/dialog.less @@ -0,0 +1,121 @@ +@-webkit-keyframes ngdialog-flyin { + 0% { + opacity: 0; + -webkit-transform: translateY(-40px); + transform: translateY(-40px); + } + + 100% { + opacity: 1; + -webkit-transform: translateY(0); + transform: translateY(0); + } +} + +@keyframes ngdialog-flyin { + 0% { + opacity: 0; + -webkit-transform: translateY(-40px); + -ms-transform: translateY(-40px); + transform: translateY(-40px); + } + + 100% { + opacity: 1; + -webkit-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} + +@-webkit-keyframes ngdialog-flyout { + 0% { + opacity: 1; + -webkit-transform: translateY(0); + transform: translateY(0); + } + + 100% { + opacity: 0; + -webkit-transform: translateY(-40px); + transform: translateY(-40px); + } +} + +@keyframes ngdialog-flyout { + 0% { + opacity: 1; + -webkit-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } + + 100% { + opacity: 0; + -webkit-transform: translateY(-40px); + -ms-transform: translateY(-40px); + transform: translateY(-40px); + } +} + +.ngdialog.ngdialog-theme-default { + padding-bottom: 160px; + padding-top: 160px; + + &.ngdialog-closing .ngdialog-content { + -webkit-animation: ngdialog-flyout .5s; + animation: ngdialog-flyout .5s; + } + + .ngdialog-content { + -webkit-animation: ngdialog-flyin .5s; + animation: ngdialog-flyin .5s; + background: @white; + border-radius: 5px; + color: @black; + margin: 0 auto; + max-width: 100%; + padding: 1em; + position: relative; + .u-w40pct; + } + + .ngdialog-close { + border-radius: 5px; + cursor: pointer; + position: absolute; + right: 0; + top: 0; + } + + .ngdialog-close:before { + background: transparent; + border-radius: 3px; + color: @gray-50; + content: '\00D7'; + font-size: 26px; + font-weight: 400; + height: 30px; + line-height: 26px; + position: absolute; + right: 3px; + text-align: center; + top: 3px; + width: 30px; + } + + .ngdialog-close:hover:before, + .ngdialog-close:active:before { + color: @gray; + } + + .ngdialog-message { + margin-bottom: .5em; + } + + .dialog-buttons { + div:nth-child(2) { + text-align: right; + } + } +} diff --git a/app/styles/hmda-pilot.less b/app/styles/hmda-pilot.less index 20ceef3..82b25a8 100644 --- a/app/styles/hmda-pilot.less +++ b/app/styles/hmda-pilot.less @@ -22,6 +22,10 @@ @import (less) "brand-palette.less"; @import (less) "cf-theme-overrides.less"; +// ngDialog CSS libraries +@import (inline) "../../node_modules/ng-dialog/css/ngDialog.css"; +@import (less) "dialog.less"; + // HMDA Pilot components @import (less) "utils.less"; @import (less) "wizard.less"; diff --git a/package.json b/package.json index 32f473b..3b3f0a6 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "angular-sanitize": "1.3.13", "angular-touch": "1.3.13", "hmda-rule-engine": "cfpb/hmda-rule-engine#milestone8", + "ng-dialog": "^0.3.12", "normalize-css": "^2.3.1" }, "napa": { @@ -39,6 +40,7 @@ "devDependencies": { "angular-mocks": "1.3.13", "browserify-istanbul": "^0.1.2", + "browserify-shim": "^3.8.3", "grunt": "^0.4.1", "grunt-autoprefixer": "^0.7.3", "grunt-browserify": "^3.2.1", @@ -85,5 +87,14 @@ "scripts": { "test": "grunt travis-coveralls", "install": "napa" + }, + "browserify": { + "transform": [ "browserify-shim" ] + }, + "browser": { + "angular": "./node_modules/angular/angular.js" + }, + "browserify-shim": { + "angular": "angular" } } diff --git a/test/spec/controllers/summarySyntacticalValidity.js b/test/spec/controllers/summarySyntacticalValidity.js index 9c3c392..d7d3e1d 100644 --- a/test/spec/controllers/summarySyntacticalValidity.js +++ b/test/spec/controllers/summarySyntacticalValidity.js @@ -11,6 +11,7 @@ describe('Controller: SummarySyntacticalValidityCtrl', function () { timeout, Q, Wizard, + mockNgDialog, mockErrors = { syntactical: {}, validity: {}, @@ -32,6 +33,17 @@ describe('Controller: SummarySyntacticalValidityCtrl', function () { location = $location; controller = $controller; timeout = $timeout; + + var mockNgDialogPromise = { + then: function(callback) { + callback('reset'); + } + }; + mockNgDialog = { + openConfirm: function() { } + }; + spyOn(mockNgDialog, 'openConfirm').and.returnValue(mockNgDialogPromise); + Q = $q; Wizard = _Wizard_; Wizard.initSteps(); @@ -40,10 +52,23 @@ describe('Controller: SummarySyntacticalValidityCtrl', function () { $location: location, $timeout: timeout, HMDAEngine: mockEngine, - Wizard: _Wizard_ + Wizard: _Wizard_, + ngDialog: mockNgDialog }); })); + beforeEach(inject(function ($templateCache) { + var templateUrl = 'partials/confirmSessionReset.html'; + var asynchronous = false; + + var req = new XMLHttpRequest(); + req.onload = function () { + $templateCache.put(templateUrl, this.responseText); + }; + req.open('get', '/base/app/' + templateUrl, asynchronous); + req.send(); + })); + it('should include the syntactical errors in the scope', function () { expect(scope.syntacticalErrors).toEqual({}); }); @@ -210,7 +235,8 @@ describe('Controller: SummarySyntacticalValidityCtrl', function () { scope.$digest(); }); - it('should direct the user to the home (/) page', function () { + it('should display the confirmation dialog', function () { + expect(mockNgDialog.openConfirm).toHaveBeenCalled(); expect(location.path()).toBe('/'); }); }); diff --git a/test/spec/controllers/validationSummary.js b/test/spec/controllers/validationSummary.js index c65bf60..fc004ab 100644 --- a/test/spec/controllers/validationSummary.js +++ b/test/spec/controllers/validationSummary.js @@ -7,26 +7,50 @@ describe('Controller: ValidationSummaryCtrl', function () { var scope, location, + mockNgDialog, mockEngine = { getHmdaJson: function() { return {hmdaFile: { transmittalSheet: {}}}; } }; beforeEach(angular.mock.module('hmdaPilotApp')); - beforeEach(inject(function ($rootScope, $location, $controller, _FileMetadata_) { + beforeEach(inject(function ($rootScope, $q, $location, $controller, _FileMetadata_) { scope = $rootScope.$new(); location = $location; var FileMetadata = _FileMetadata_; FileMetadata.setFilename('test.foo'); + var mockNgDialogPromise = { + then: function(callback) { + callback('reset'); + } + }; + mockNgDialog = { + openConfirm: function() { } + }; + spyOn(mockNgDialog, 'openConfirm').and.returnValue(mockNgDialogPromise); + $controller('ValidationSummaryCtrl', { $scope: scope, $location: location, FileMetadata: _FileMetadata_, - HMDAEngine: mockEngine + HMDAEngine: mockEngine, + ngDialog: mockNgDialog }); })); + beforeEach(inject(function ($templateCache) { + var templateUrl = 'partials/confirmSessionReset.html'; + var asynchronous = false; + + var req = new XMLHttpRequest(); + req.onload = function () { + $templateCache.put(templateUrl, this.responseText); + }; + req.open('get', '/base/app/' + templateUrl, asynchronous); + req.send(); + })); + describe('initial scope', function() { it('should include the file metadata', function() { expect(scope.fileMetadata).toBeDefined(); @@ -46,9 +70,9 @@ describe('Controller: ValidationSummaryCtrl', function () { }); describe('startOver()', function() { - it('should direct the user to the /selectFile page', function () { + it('should display the confirmation dialog', function () { scope.startOver(); - scope.$digest(); + expect(mockNgDialog.openConfirm).toHaveBeenCalled(); expect(location.path()).toBe('/selectFile'); }); }); diff --git a/test/spec/directives/wizardNav.js b/test/spec/directives/wizardNav.js index 06f60a3..154c205 100644 --- a/test/spec/directives/wizardNav.js +++ b/test/spec/directives/wizardNav.js @@ -11,8 +11,10 @@ describe('Directive: WizardNav', function () { var element, scope, + location, StepFactory, - Wizard; + Wizard, + ngDialog; beforeEach(inject(function ($templateCache) { var templateUrl = 'partials/wizardNav.html'; @@ -26,11 +28,32 @@ describe('Directive: WizardNav', function () { req.send(); })); - beforeEach(inject(function ($rootScope, $compile, _StepFactory_, _Wizard_) { + beforeEach(inject(function ($templateCache) { + var templateUrl = 'partials/confirmSessionReset.html'; + var asynchronous = false; + + var req = new XMLHttpRequest(); + req.onload = function () { + $templateCache.put(templateUrl, this.responseText); + }; + req.open('get', '/base/app/' + templateUrl, asynchronous); + req.send(); + })); + + beforeEach(inject(function ($rootScope, $location, $compile, _StepFactory_, _Wizard_, _ngDialog_) { StepFactory = _StepFactory_; Wizard = _Wizard_; + ngDialog = _ngDialog_; + + var mockNgDialogPromise = { + then: function(callback) { + callback('reset'); + } + }; + spyOn(ngDialog, 'openConfirm').and.returnValue(mockNgDialogPromise); scope = $rootScope.$new(); + location = $location; var stepData = [ new StepFactory('Step 1', 'step1'), @@ -66,4 +89,21 @@ describe('Directive: WizardNav', function () { it('should allow a completed step to be focusable', function () { expect(jQuery('li.complete', element).hasClass('focusable')).toBeTruthy(); }); + + describe('when the navigating to /selectFile', function() { + it('should display a confirmation dialog', function() { + scope.$broadcast('$locationChangeStart', '#/selectFile'); + scope.$digest(); + expect(ngDialog.openConfirm).toHaveBeenCalled(); + expect(location.path()).toBe('/'); + }); + }); + + describe('when the navigating anywhere else', function() { + it('should not display the confirmation dialog', function() { + scope.$broadcast('$locationChangeStart', '#/test'); + scope.$digest(); + expect(ngDialog.openConfirm).not.toHaveBeenCalled(); + }); + }); });