diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/valpropertymsg.directive.js index 2fcd21d0ec..eef5b168d5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/valpropertymsg.directive.js @@ -25,10 +25,10 @@ function valPropertyMsg(serverValidationService) { link: function (scope, element, attrs, formCtrl) { //assign the form control to our isolated scope so we can watch it's values - scope.formCtrl = formCtrl; + scope.formCtrl = formCtrl; - //flags for use in the below closures - var showValidation = false; + //if there's any remaining errors in the server validation service then we should show them. + var showValidation = serverValidationService.items.length > 0; var hasError = false; //create properties on our custom scope so we can use it in our template @@ -53,7 +53,7 @@ function valPropertyMsg(serverValidationService) { hasError = true; //update the validation message if we don't already have one assigned. if (showValidation && scope.errorMsg === "") { - scope.errorMsg = serverValidationService.getPropertyError(scope.currentProperty, ""); + scope.errorMsg = serverValidationService.getPropertyError(scope.currentProperty, "").errorMsg; } } else { @@ -71,7 +71,7 @@ function valPropertyMsg(serverValidationService) { scope.$on("saving", function (ev, args) { showValidation = true; if (hasError && scope.errorMsg === "") { - scope.errorMsg = serverValidationService.getPropertyError(scope.currentProperty, ""); + scope.errorMsg = serverValidationService.getPropertyError(scope.currentProperty, "").errorMsg; } else if (!hasError) { scope.errorMsg = ""; @@ -94,7 +94,7 @@ function valPropertyMsg(serverValidationService) { scope.$watch("currentProperty.value", function(newValue) { if (formCtrl.$invalid) { scope.errorMsg = ""; - formCtrl.$setValidity('valPropertyMsg', true); + formCtrl.$setValidity('valPropertyMsg', true); } }); @@ -102,16 +102,14 @@ function valPropertyMsg(serverValidationService) { // NOTE: we pass in "" in order to listen for all validation changes to the content property, not for // validation changes to fields in the property this is because some server side validators may not // return the field name for which the error belongs too, just the property for which it belongs. + // It's important to note that we need to subscribe to server validation changes here because we always must + // indicate that a content property is invalid at the property level since developers may not actually implement + // the correct field validation in their property editors. serverValidationService.subscribe(scope.currentProperty, "", function (isValid, propertyErrors, allErrors) { hasError = !isValid; if (hasError) { //set the error message to the server message - scope.errorMsg = propertyErrors[0].errorMsg; - //now that we've used the server validation message, we need to remove it from the - //error collection... it is a 'one-time' usage so that when the field is invalidated - //again, the message we display is the client side message. - //NOTE: 'this' in the subscribe callback context is the validation manager object. - this.removePropertyError(scope.currentProperty); + scope.errorMsg = propertyErrors[0].errorMsg; //flag that the current validator is invalid formCtrl.$setValidity('valPropertyMsg', false); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/valshowvalidation.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/valshowvalidation.directive.js index d7f3ee92c5..49c80958f8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/valshowvalidation.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/valshowvalidation.directive.js @@ -7,19 +7,27 @@ * This is used because we don't want to show validation messages until after the form is submitted and then reset * the process when the form is successful. We do this by listening to the current controller's saving and saved events. **/ -function valShowValidation() { +function valShowValidation(serverValidationService) { return { require: "ngController", restrict: "A", link: function (scope, element, attr, ctrl) { + + //we should show validation if there are any msgs in the server validation collection + if (serverValidationService.items.length > 0) { + element.addClass("show-validation"); + } + //listen for the forms saving event scope.$on("saving", function (ev, args) { element.addClass("show-validation"); }); + //listen for the forms saved event scope.$on("saved", function (ev, args) { element.removeClass("show-validation"); }); + } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/valtogglemsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/valtogglemsg.directive.js index c6588c7731..b63955edac 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/valtogglemsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/valtogglemsg.directive.js @@ -1,4 +1,4 @@ -function valToggleMsg() { +function valToggleMsg(serverValidationService) { return { require: "^form", restrict: "A", @@ -15,8 +15,8 @@ function valToggleMsg() { throw "valToggleMsg requires that the attribute valMsgFor exists on the element"; } - //create a flag for us to be able to reference in the below closures for watching. - var showValidation = false; + //if there's any remaining errors in the server validation service then we should show them. + var showValidation = serverValidationService.items.length > 0; var hasError = false; //add a watch to the validator for the value (i.e. myForm.value.$error.required ) @@ -29,7 +29,7 @@ function valToggleMsg() { element.hide(); } }); - + scope.$on("saving", function(ev, args) { showValidation = true; if (hasError) { diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utills.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js similarity index 97% rename from src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utills.js rename to src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js index 666d499105..a445764f67 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utills.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js @@ -1,127 +1,127 @@ -angular.module('umbraco.mocks'). - factory('mocksUtils', ['$cookieStore', function($cookieStore) { - 'use strict'; - - //by default we will perform authorization - var doAuth = true; - - return { - - /** Creats a mock content object */ - getMockContent: function(id) { - var node = { - name: "My content with id: " + id, - updateDate: new Date(), - publishDate: new Date(), - id: id, - parentId: 1234, - icon: "icon-file-alt", - owner: { name: "Administrator", id: 0 }, - updater: { name: "Per Ploug Krogslund", id: 1 }, - - tabs: [ - { - label: "Child documents", - alias: "tab00", - id: 0, - active: true, - properties: [ - { alias: "list", label: "List", view: "listview", value: "", hideLabel: true } - ] - }, - { - label: "Content", - alias: "tab01", - id: 1, - properties: [ - { alias: "bodyText", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "
askjdkasj lasjd
" }, - { alias: "textarea", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } }, - { alias: "map", label: "Map", view: "googlemaps", value: "37.4419,-122.1419", config: { mapType: "ROADMAP", zoom: 4 } }, - { alias: "media", label: "Media picker", view: "mediapicker", value: "" }, - { alias: "content", label: "Content picker", view: "contentpicker", value: "" } - ] - }, - { - label: "Sample Editor", - alias: "tab02", - id: 2, - properties: [ - { alias: "datepicker", label: "Datepicker", view: "datepicker", config: { rows: 7 } }, - { alias: "tags", label: "Tags", view: "tags", value: "" } - ] - }, - { - label: "Grid", - alias: "tab03", - id: 3, - properties: [ - { alias: "grid", label: "Grid", view: "grid", controller: "umbraco.grid", value: "test", hideLabel: true } - ] - }, { - label: "WIP", - alias: "tab04", - id: 4, - properties: [ - { - alias: "tes", label: "Stuff", view: "test", controller: "umbraco.embeddedcontent", value: "", - - config: { - fields: [ - { alias: "embedded", label: "Embbeded", view: "textstring", value: "" }, - { alias: "embedded2", label: "Embbeded 2", view: "contentpicker", value: "" }, - { alias: "embedded3", label: "Embbeded 3", view: "textarea", value: "" }, - { alias: "embedded4", label: "Embbeded 4", view: "datepicker", value: "" } - ] - } - } - ] - } - ] - }; - - return node; - }, - - /** generally used for unit tests, calling this will disable the auth check and always return true */ - disableAuth: function() { - doAuth = false; - }, - - /** generally used for unit tests, calling this will enabled the auth check */ - enabledAuth: function() { - doAuth = true; - }, - - /** Checks for our mock auth cookie, if it's not there, returns false */ - checkAuth: function () { - if (doAuth) { - var mockAuthCookie = $cookieStore.get("mockAuthCookie"); - if (!mockAuthCookie) { - return false; - } - return true; - } - else { - return true; - } - }, - - /** Creates/sets the auth cookie with a value indicating the user is now authenticated */ - setAuth: function() { - //set the cookie for loging - $cookieStore.put("mockAuthCookie", "Logged in!"); - }, - - urlRegex: function(url) { - url = url.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - return new RegExp("^" + url); - }, - - getParameterByName: function(url, name) { - name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); - var regex = new RegExp("[\\?&]" + name + "=([^]*)"), - results = regex.exec(url); - return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); - } - }; +angular.module('umbraco.mocks'). + factory('mocksUtils', ['$cookieStore', function($cookieStore) { + 'use strict'; + + //by default we will perform authorization + var doAuth = true; + + return { + + /** Creats a mock content object */ + getMockContent: function(id) { + var node = { + name: "My content with id: " + id, + updateDate: new Date(), + publishDate: new Date(), + id: id, + parentId: 1234, + icon: "icon-file-alt", + owner: { name: "Administrator", id: 0 }, + updater: { name: "Per Ploug Krogslund", id: 1 }, + + tabs: [ + { + label: "Child documents", + alias: "tab00", + id: 0, + active: true, + properties: [ + { alias: "list", label: "List", view: "listview", value: "", hideLabel: true } + ] + }, + { + label: "Content", + alias: "tab01", + id: 1, + properties: [ + { alias: "bodyText", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "askjdkasj lasjd
" }, + { alias: "textarea", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } }, + { alias: "map", label: "Map", view: "googlemaps", value: "37.4419,-122.1419", config: { mapType: "ROADMAP", zoom: 4 } }, + { alias: "media", label: "Media picker", view: "mediapicker", value: "" }, + { alias: "content", label: "Content picker", view: "contentpicker", value: "" } + ] + }, + { + label: "Sample Editor", + alias: "tab02", + id: 2, + properties: [ + { alias: "datepicker", label: "Datepicker", view: "datepicker", config: { rows: 7 } }, + { alias: "tags", label: "Tags", view: "tags", value: "" } + ] + }, + { + label: "Grid", + alias: "tab03", + id: 3, + properties: [ + { alias: "grid", label: "Grid", view: "grid", controller: "umbraco.grid", value: "test", hideLabel: true } + ] + }, { + label: "WIP", + alias: "tab04", + id: 4, + properties: [ + { + alias: "tes", label: "Stuff", view: "test", controller: "umbraco.embeddedcontent", value: "", + + config: { + fields: [ + { alias: "embedded", label: "Embbeded", view: "textstring", value: "" }, + { alias: "embedded2", label: "Embbeded 2", view: "contentpicker", value: "" }, + { alias: "embedded3", label: "Embbeded 3", view: "textarea", value: "" }, + { alias: "embedded4", label: "Embbeded 4", view: "datepicker", value: "" } + ] + } + } + ] + } + ] + }; + + return node; + }, + + /** generally used for unit tests, calling this will disable the auth check and always return true */ + disableAuth: function() { + doAuth = false; + }, + + /** generally used for unit tests, calling this will enabled the auth check */ + enabledAuth: function() { + doAuth = true; + }, + + /** Checks for our mock auth cookie, if it's not there, returns false */ + checkAuth: function () { + if (doAuth) { + var mockAuthCookie = $cookieStore.get("mockAuthCookie"); + if (!mockAuthCookie) { + return false; + } + return true; + } + else { + return true; + } + }, + + /** Creates/sets the auth cookie with a value indicating the user is now authenticated */ + setAuth: function() { + //set the cookie for loging + $cookieStore.put("mockAuthCookie", "Logged in!"); + }, + + urlRegex: function(url) { + url = url.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + return new RegExp("^" + url); + }, + + getParameterByName: function(url, name) { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + var regex = new RegExp("[\\?&]" + name + "=([^]*)"), + results = regex.exec(url); + return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); + } + }; }]); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidation.service.js index 8a32f7151b..f0b9ae0e67 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidation.service.js @@ -8,11 +8,84 @@ * is for user defined properties (called Properties) and the other is for field properties which are attached to the native * model objects (not user defined). The methods below are named according to these rules: Properties vs Fields. */ -function serverValidationService() { +function serverValidationService($timeout) { var callbacks = []; + /** calls the callback specified with the errors specified, used internally */ + function executeCallback(self, errorsForCallback, callback) { + + callback.apply(self, [ + false, //pass in a value indicating it is invalid + errorsForCallback, //pass in the errors for this item + self.items]); //pass in all errors in total + } + + function getFieldErrors(self, fieldName) { + //find errors for this field name + return _.filter(self.items, function (item) { + return (item.propertyAlias === null && item.fieldName === fieldName); + }); + } + + function getPropertyErrors(self, contentProperty, fieldName) { + //find all errors for this property + return _.filter(self.items, function (item) { + return (item.propertyAlias === contentProperty.alias && item.fieldName === fieldName); + }); + } + return { + + executeAndClearAllSubscriptions: function() { + + var self = this; + + $timeout(function () { + + for (var cb in callbacks) { + if (callbacks[cb].propertyAlias === null) { + //its a field error callback + var fieldErrors = getFieldErrors(self, callbacks[cb].fieldName); + if (fieldErrors.length > 0) { + executeCallback(self, fieldErrors, callbacks[cb].callback); + } + } + else { + //its a property error + var propErrors = getPropertyErrors(self, { alias: callbacks[cb].propertyAlias }, callbacks[cb].fieldName); + if (propErrors.length > 0) { + executeCallback(self, propErrors, callbacks[cb].callback); + } + } + } + + ////iterate all items, detect if the error is a field vs property error and then + //// execute any callbacks registered for that particular error. + //for (var i in self.items) { + // if (self.items[i].propertyAlias === null) { + // //its a field error + // var cbs1 = self.getFieldCallbacks(self.items[i].fieldName); + // for (var cb1 in cbs1) { + // executeCallback(self, self.items[i], cbs1[cb1].callback); + // } + // } + // else { + // //its a property error + // var cbs2 = self.getPropertyCallbacks({ alias: self.items[i].propertyAlias }, self.items[i].fieldName); + // for (var cb2 in cbs2) { + // executeCallback(self, self.items[i], cbs2[cb2].callback); + // } + // } + //} + + //now that they are all executed, we're gonna clear all of the errors we have + self.clear(); + + }); + + }, + /** * @ngdoc function * @name subscribe @@ -25,13 +98,17 @@ function serverValidationService() { * a particular field, otherwise we can only pinpoint that there is an error for a content property, not the * property's specific field. This is used with the val-server directive in which the directive specifies the * field alias to listen for. - * If contentProperty is null, then this subscription is for a field property (not a user defined property) + * If contentProperty is null, then this subscription is for a field property (not a user defined property). + * During the call to subscribe we will check if there are any current validation errors for the subscription and + * execute the specified callback. */ subscribe: function (contentProperty, fieldName, callback) { if (!callback) { return; } + var self = this; + if (contentProperty === null) { //don't add it if it already exists var exists1 = _.find(callbacks, function (item) { @@ -39,6 +116,17 @@ function serverValidationService() { }); if (!exists1) { callbacks.push({ propertyAlias: null, fieldName: fieldName, callback: callback }); + + ////TODO: Figure out how the heck to clear the validation collection!!!!!!!!!!!!!!!!!! + + ////find errors for this callback and execute the callback after this current digest + //$timeout(function() { + // var fieldErrors = getFieldErrors(self, fieldName); + // if (fieldErrors.length > 0) { + // executeCallback(self, fieldErrors, callback); + // } + //}); + } } else if (contentProperty !== undefined) { @@ -48,6 +136,14 @@ function serverValidationService() { }); if (!exists2) { callbacks.push({ propertyAlias: contentProperty.alias, fieldName: fieldName, callback: callback }); + + ////find errors for this callback and execute the callback after this current digest + //$timeout(function() { + // var propErrors = getPropertyErrors(self, contentProperty, fieldName); + // if (propErrors.length > 0) { + // executeCallback(self, propErrors, callback); + // } + //}); } } }, @@ -136,17 +232,12 @@ function serverValidationService() { } //find all errors for this item - var errorsForCallback = _.filter(this.items, function (item) { - return (item.propertyAlias === null && item.fieldName === fieldName); - }); + var errorsForCallback = getFieldErrors(this, fieldName); //we should now call all of the call backs registered for this error var cbs = this.getFieldCallbacks(fieldName); //call each callback for this error for (var cb in cbs) { - cbs[cb].callback.apply(this, [ - false, //pass in a value indicating it is invalid - errorsForCallback, //pass in the errors for this item - this.items]); //pass in all errors in total + executeCallback(this, errorsForCallback, cbs[cb].callback); } }, @@ -174,17 +265,12 @@ function serverValidationService() { } //find all errors for this item - var errorsForCallback = _.filter(this.items, function (item) { - return (item.propertyAlias === contentProperty.alias && item.fieldName === fieldName); - }); + var errorsForCallback = getPropertyErrors(this, contentProperty, fieldName); //we should now call all of the call backs registered for this error var cbs = this.getPropertyCallbacks(contentProperty, fieldName); //call each callback for this error for (var cb in cbs) { - cbs[cb].callback.apply(this, [ - false, //pass in a value indicating it is invalid - errorsForCallback, //pass in the errors for this item - this.items]); //pass in all errors in total + executeCallback(this, errorsForCallback, cbs[cb].callback); } }, @@ -218,7 +304,7 @@ function serverValidationService() { * Clears all errors and notifies all callbacks that all server errros are now valid - used when submitting a form */ reset: function () { - this.items = []; + this.clear(); for (var cb in callbacks) { callbacks[cb].callback.apply(this, [ true, //pass in a value indicating it is VALID @@ -227,6 +313,19 @@ function serverValidationService() { } }, + /** + * @ngdoc function + * @name clear + * @methodOf umbraco.services.serverValidationService + * @function + * + * @description + * Clears all errors + */ + clear: function() { + this.items = []; + }, + /** * @ngdoc function * @name getPropertyError @@ -242,7 +341,7 @@ function serverValidationService() { return (item.propertyAlias === contentProperty.alias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); //return generic property error message if the error doesn't exist - return err ? err : "Property has errors"; + return err ? err : { errorMsg: "Property has errors" }; }, /** @@ -260,7 +359,7 @@ function serverValidationService() { return (item.propertyAlias === null && item.fieldName === fieldName); }); //return generic property error message if the error doesn't exist - return err ? err : "Field has errors"; + return err ? err : { errorMsg: "Field has errors" }; }, /** diff --git a/src/Umbraco.Web.UI.Client/src/common/services/utill.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js similarity index 98% rename from src/Umbraco.Web.UI.Client/src/common/services/utill.service.js rename to src/Umbraco.Web.UI.Client/src/common/services/util.service.js index 170736cdde..f26dfd6c80 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/utill.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js @@ -729,7 +729,6 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser //add to notifications notificationsService.error("Validation", modelState[e][0]); - } }, @@ -749,9 +748,15 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser if (err.status === 403) { //now we need to look through all the validation errors if (err.data && (err.data.ModelState)) { + this.handleValidationErrors(err.data, err.data.ModelState); - this.redirectToCreatedContent(err.data.id, err.data.ModelState); + if (!this.redirectToCreatedContent(err.data.id, err.data.ModelState)) { + //we are not redirecting because this is not new content, it is existing content. In this case + // we need to clear the server validation items. When we are creating new content we cannot clear + // the server validation items because we redirect and they need to persist until the validation is re-bound. + serverValidationService.clear(); + } //indicates we've handled the server result return true; @@ -795,6 +800,8 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser $location.search(null); //change to new path $location.path("/" + $routeParams.section + "/" + $routeParams.method + "/" + id); + //don't add a browser history for this + $location.replace(); return true; } return false; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/contentedit.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/contentedit.controller.js index 46da63be46..18ee93d86e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/contentedit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/contentedit.controller.js @@ -8,8 +8,13 @@ */ function ContentEditController($scope, $routeParams, $location, contentResource, notificationsService, angularHelper, serverValidationService, contentEditingHelper) { - //get the data to show, scaffold for new or get existing + //This is important with our routing: + // * When we create something the id will be 'create' and a 'parentId' query string will be supplied so we know where to create it + // * After it is create there might be validation errors but the item has still be saved to the server, we cannot just change the route + // to the normal editing route for the new ID because our server validation errors will no long exists + if ($routeParams.create) { + //we are creating so get an empty content item contentResource.getScaffold($routeParams.id, $routeParams.doctype) .then(function(data) { $scope.contentLoaded = true; @@ -17,10 +22,18 @@ function ContentEditController($scope, $routeParams, $location, contentResource, }); } else { + //we are editing so get the content item from the server contentResource.getById($routeParams.id) .then(function(data) { $scope.contentLoaded = true; $scope.content = data; + + //in one particular special case, after we've created a new item we redirect back to the edit + // route but there might be server validation errors in the collection which we need to display + // after the redirect, so we will bind all subscriptions which will show the server validation errors + // if there are any and then clear them so the collection no longer persists them. + serverValidationService.executeAndClearAllSubscriptions(); + }); } @@ -58,11 +71,10 @@ function ContentEditController($scope, $routeParams, $location, contentResource, notificationsService.success("Published", "Content has been saved and published"); $scope.$broadcast("saved", { scope: $scope }); - contentEditingHelper.redirectToCreatedContent(data.id); + contentEditingHelper.redirectToCreatedContent($scope.content.id); }, function (err) { - $location.search(null); //TODO: only update the content that has changed! - $scope.content = err.data; + //$scope.content = err.data; contentEditingHelper.handleSaveError(err); }); }; @@ -78,12 +90,16 @@ function ContentEditController($scope, $routeParams, $location, contentResource, contentResource.saveContent(cnt, $routeParams.create, $scope.files) .then(function (data) { + //TODO: only update the content that has changed! $scope.content = data; + notificationsService.success("Saved", "Content has been saved"); $scope.$broadcast("saved", { scope: $scope }); + + contentEditingHelper.redirectToCreatedContent($scope.content.id); }, function (err) { //TODO: only update the content that has changed! - $scope.content = err.data; + //$scope.content = err.data; contentEditingHelper.handleSaveError(err); });