diff --git a/src/popup/app/app.js b/src/popup/app/app.js index ea4f9b958a..7217c52833 100644 --- a/src/popup/app/app.js +++ b/src/popup/app/app.js @@ -4,6 +4,7 @@ 'angular-jwt', 'ngAnimate', + 'bit.directives', 'bit.services', 'bit.global', diff --git a/src/popup/app/directives/directivesModule.js b/src/popup/app/directives/directivesModule.js new file mode 100644 index 0000000000..ca42509753 --- /dev/null +++ b/src/popup/app/directives/directivesModule.js @@ -0,0 +1,2 @@ +angular + .module('bit.directives', []); diff --git a/src/popup/app/directives/fieldDirective.js b/src/popup/app/directives/fieldDirective.js new file mode 100644 index 0000000000..b9066666a3 --- /dev/null +++ b/src/popup/app/directives/fieldDirective.js @@ -0,0 +1,30 @@ +angular + .module('bit.directives') + + .directive('bitField', function () { + var linkFn = function (scope, element, attrs, ngModel) { + ngModel.$registerError = registerError; + ngModel.$validators.validate = validator; + + function validator() { + ngModel.$setValidity('bit', true); + return true; + } + + function registerError() { + ngModel.$setValidity('bit', false); + } + }; + + return { + require: 'ngModel', + restrict: 'A', + compile: function (elem, attrs) { + if (!attrs.name || attrs.name === '') { + throw 'bit-field element does not have a valid name attribute'; + } + + return linkFn; + } + }; + }); diff --git a/src/popup/app/directives/formDirective.js b/src/popup/app/directives/formDirective.js new file mode 100644 index 0000000000..cefdf1b699 --- /dev/null +++ b/src/popup/app/directives/formDirective.js @@ -0,0 +1,35 @@ +angular + .module('bit.directives') + + .directive('bitForm', function ($rootScope, validationService) { + return { + require: 'form', + restrict: 'A', + link: function (scope, element, attrs, formCtrl) { + var watchPromise = attrs.bitForm || null; + if (watchPromise) { + scope.$watch(watchPromise, formSubmitted.bind(null, formCtrl, scope)); + } + } + }; + + function formSubmitted(form, scope, promise) { + if (!promise || !promise.then) { + return; + } + + // reset errors + form.$errors = null; + + // start loading + form.$loading = true; + + promise.then(function success(response) { + form.$loading = false; + }, function failure(reason) { + form.$loading = false; + validationService.addErrors(form, reason); + scope.$broadcast('show-errors-check-validity'); + }); + } + }); diff --git a/src/popup/app/services/cipherService.js b/src/popup/app/services/cipherService.js index f06aacfcbb..3ae595c304 100644 --- a/src/popup/app/services/cipherService.js +++ b/src/popup/app/services/cipherService.js @@ -4,23 +4,25 @@ .factory('cipherService', function (cryptoService, $q) { var _service = {}; - _service.encryptSite = function (site, callback) { + _service.encryptSite = function (site) { var model = {}; - cryptoService.encrypt(site.name, function (nameCipherString) { - model.name = nameCipherString; - cryptoService.encrypt(site.uri, function (uriCipherString) { - model.uri = uriCipherString; - cryptoService.encrypt(site.username, function (usernameCipherString) { - model.username = usernameCipherString; - cryptoService.encrypt(site.password, function (passwordCipherString) { - model.password = passwordCipherString; - cryptoService.encrypt(site.notes, function (notesCipherString) { - model.notes = notesCipherString; - callback(model); - }); - }); - }); + return $q(function (resolve, reject) { + encrypt(site.name).then(function (cs) { + model.name = cs; + return encrypt(site.uri); + }).then(function (cs) { + model.uri = cs; + return encrypt(site.username); + }).then(function (cs) { + model.username = cs; + return encrypt(site.password); + }).then(function (cs) { + model.password = cs; + return encrypt(site.notes); + }).then(function (cs) { + model.notes = cs; + resolve(model); }); }); }; @@ -73,5 +75,13 @@ }); } + function encrypt(plaintextString) { + return $q(function (resolve, reject) { + cryptoService.encrypt(plaintextString, function (cipherString) { + resolve(cipherString); + }); + }); + } + return _service; }); diff --git a/src/popup/app/services/validationService.js b/src/popup/app/services/validationService.js new file mode 100644 index 0000000000..1af09d107a --- /dev/null +++ b/src/popup/app/services/validationService.js @@ -0,0 +1,62 @@ +angular + .module('bit.services') + + .factory('validationService', function () { + var _service = {}; + + _service.addErrors = function (form, reason) { + var data = reason.data; + var defaultErrorMessage = 'An unexpected error has occured.'; + form.$errors = []; + + if (!data || !angular.isObject(data)) { + form.$errors.push(defaultErrorMessage); + return; + } + + if (!data.validationErrors) { + if (data.message) { + form.$errors.push(data.message); + } + else { + form.$errors.push(defaultErrorMessage); + } + + return; + } + + for (var key in data.validationErrors) { + if (!data.validationErrors.hasOwnProperty(key)) { + continue; + } + + for (var i = 0; i < data.validationErrors[key].length; i++) { + _service.addError(form, key, data.validationErrors[key][i]); + } + } + }; + + _service.addError = function (form, key, errorMessage, clearExistingErrors) { + if (clearExistingErrors || !form.$errors) { + form.$errors = []; + } + + var pushError = true; + for (var i = 0; i < form.$errors.length; i++) { + if (form.$errors[i] === errorMessage) { + pushError = false; + break; + } + } + + if (pushError) { + form.$errors.push(errorMessage); + } + + if (key && key !== '' && form[key] && form[key].$registerError) { + form[key].$registerError(); + } + }; + + return _service; + }); diff --git a/src/popup/app/vault/vaultAddSiteController.js b/src/popup/app/vault/vaultAddSiteController.js index 78bb46fc5b..daf061e008 100644 --- a/src/popup/app/vault/vaultAddSiteController.js +++ b/src/popup/app/vault/vaultAddSiteController.js @@ -1,16 +1,29 @@ angular .module('bit.vault') - .controller('vaultAddSiteController', function ($scope, $state, siteService, cipherService) { + .controller('vaultAddSiteController', function ($scope, $state, siteService, cipherService, $q) { $scope.site = { folderId: null }; + $('#name').focus(); + $('.list-section-item').click(function (e) { + e.preventDefault(); + $(this).find('input[type="text"], textarea, select').focus(); + var checkbox = $(this).find('input[type="checkbox"]'); + if (checkbox.length > 0) { + checkbox.prop('checked', !checkbox.is(':checked')); + } + }); + + $scope.savePromise = null; $scope.save = function (model) { - cipherService.encryptSite(model, function (siteModel) { + $scope.savePromise = cipherService.encryptSite(model).then(function (siteModel) { var site = new Site(siteModel, true); - siteService.saveWithServer(site, function () { - $scope.close(); + return site; + }).then(function (site) { + return saveSite(site, function (site) { + alert('Saved ' + site.id + '!'); }); }); }; @@ -18,4 +31,14 @@ $scope.close = function () { $state.go('tabs.vault', { animation: 'out-slide-down' }); }; + + function saveSite(site) { + return $q(function (resolve, reject) { + siteService.saveWithServer(site, function (site) { + resolve(site); + }, function (error) { + reject(error); + }); + }); + } }); diff --git a/src/popup/app/vault/views/vaultAddSite.html b/src/popup/app/vault/views/vaultAddSite.html index 2a971b3afb..acd0c055b5 100644 --- a/src/popup/app/vault/views/vaultAddSite.html +++ b/src/popup/app/vault/views/vaultAddSite.html @@ -1,10 +1,11 @@ -
+
- Close + Close
- + +
Add Site
@@ -17,19 +18,19 @@
- +
- +
- +
- +
Generate Password @@ -41,7 +42,7 @@
- @@ -49,7 +50,7 @@
- +
@@ -59,7 +60,7 @@
- +
diff --git a/src/popup/index.html b/src/popup/index.html index f412162204..2d1d7113e8 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -26,10 +26,15 @@ + + + + + diff --git a/src/popup/less/components.less b/src/popup/less/components.less index 63c345f6e4..1f05afec09 100644 --- a/src/popup/less/components.less +++ b/src/popup/less/components.less @@ -42,6 +42,12 @@ display: block; float: left; } + + .fa-spinner { + padding: 15px; + display: block; + float: left; + } } .right { @@ -54,6 +60,12 @@ display: block; float: right; } + + .fa-spinner { + padding: 15px; + display: block; + float: right; + } } } diff --git a/src/services/siteService.js b/src/services/siteService.js index f4566b5706..643e60eb52 100644 --- a/src/services/siteService.js +++ b/src/services/siteService.js @@ -51,19 +51,19 @@ function initSiteService() { }); }; - SiteService.prototype.saveWithServer = function (site, callback) { - if (!callback || typeof callback !== 'function') { - throw 'callback function required'; - } - + SiteService.prototype.saveWithServer = function (site, successCallback, errorCallback) { var self = this, request = new SiteRequest(site); if (!site.id) { - self.apiService.postSite(request, apiSuccess, handleError); + self.apiService.postSite(request, apiSuccess, function (response) { + handleError(response, errorCallback) + }); } else { - self.apiService.putSite(site.id, request, apiSuccess, handleError); + self.apiService.putSite(site.id, request, apiSuccess, function (response) { + handleError(response, errorCallback) + }); } function apiSuccess(response) { @@ -71,7 +71,9 @@ function initSiteService() { userService.getUserId(function (userId) { var data = new SiteData(response, userId); self.upsert(data, function () { - callback(site); + if (successCallback) { + successCallback(site); + } }); }); } @@ -161,18 +163,27 @@ function initSiteService() { }); }; - SiteService.prototype.deleteWithServer = function (id, callback) { + SiteService.prototype.deleteWithServer = function (id, successCallback, errorCallback) { if (!callback || typeof callback !== 'function') { throw 'callback function required'; } var self = this; self.apiService.deleteCipher(id, function (response) { - self.delete(id, callback); - }, handleError); + self.delete(id, successCallback); + }, function (response) { + handleError(response, errorCallback) + }); }; - function handleError() { - // TODO: check for unauth or forbidden and logout + function handleError(error, callback) { + if (error.status == 401 || error.status == 403) { + // TODO: logout + + } + + if (callback) { + callback(error); + } } };