From 6017e9ddf9ed44e9bcceeaa69ef24e347982ac38 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 9 Oct 2015 17:28:22 +0200 Subject: [PATCH] Gets group and property level validation wired up to the fields, ensures things like duplicate field warnings are created with the correct indexed field names. Fixes a few old directives to correctly watch for attribute interpolation changes which is need for some of this validation to work since it's dynamically populating the field names. --- .../components/umblockedfield.directive.js | 16 +++- .../validation/valCustom.directive.js | 2 +- .../validation/valHighlight.directive.js | 6 +- .../validation/valemail.directive.js | 1 - .../valpropertyvalidator.directive.js | 3 - .../validation/valregex.directive.js | 75 ++++++++++--------- .../validation/valserverfield.directive.js | 73 +++++++++--------- .../views/components/umb-groups-builder.html | 12 ++- .../views/components/umb-locked-field.html | 29 +++---- .../Models/ContentEditing/ContentTypeSave.cs | 29 ++++++- 10 files changed, 142 insertions(+), 104 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblockedfield.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblockedfield.directive.js index bd4279a161..63a4dece6e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblockedfield.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblockedfield.directive.js @@ -11,7 +11,7 @@ function LockedFieldDirective($timeout, localizationService) { - function link(scope, el, attr, ngModel) { + function link(scope, el, attr, ngModel) { var input = el.children('.umb-locked-field__input'); @@ -22,6 +22,16 @@ scope.locked = true; } + // if regex validation is not defined as an attr set default state + // if this is set to an empty string then regex validation can be ignored. + if (scope.regexValidation === undefined || scope.regexValidation === null) { + scope.regexValidation = "^[a-zA-Z]\\w.*$"; + } + + if (scope.serverValidationField === undefined || scope.serverValidationField === null) { + scope.serverValidationField = ""; + } + // if locked state is not defined as an attr set default state if (scope.placeholderText === undefined || scope.placeholderText === null) { scope.placeholderText = "Enter value..."; @@ -70,7 +80,9 @@ scope: { model: '=ngModel', locked: "=?", - placeholderText: "=?" + placeholderText: "=?", + regexValidation: "=?", + serverValidationField: "@" }, link: link }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valCustom.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valCustom.directive.js index a402065708..dac010a97f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valCustom.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valCustom.directive.js @@ -57,7 +57,7 @@ angular.module('umbraco.directives.validation') } }; validators[key] = validateFn; - ctrl.$formatters.push(validateFn); + ctrl.$parsers.push(validateFn); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valHighlight.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valHighlight.directive.js index fdcf768947..599cda766c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valHighlight.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valHighlight.directive.js @@ -9,9 +9,7 @@ function valHighlight($timeout) { restrict: "A", link: function (scope, element, attrs, ctrl) { - scope.$watch(function() { - return scope.$eval(attrs.valHighlight); - }, function(newVal, oldVal) { + attrs.$observe("valHighlight", function (newVal) { if (newVal === true) { element.addClass("highlight-error"); $timeout(function () { @@ -23,7 +21,7 @@ function valHighlight($timeout) { element.removeClass("highlight-error"); } }); - + } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valemail.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valemail.directive.js index 88ffd6f0fa..1e81d8edec 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valemail.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valemail.directive.js @@ -29,7 +29,6 @@ function valEmail(valEmailExpression) { } }; - ctrl.$formatters.push(patternValidator); ctrl.$parsers.push(patternValidator); } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js index 77652d7f69..53a1ea67b2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js @@ -59,9 +59,6 @@ function valPropertyValidator(serverValidationManager) { } }; - // Formatters are invoked when the model is modified in the code. - modelCtrl.$formatters.push(validate); - // Parsers are called as soon as the value in the form input is modified modelCtrl.$parsers.push(validate); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js index 651c0a54c7..45a35c5257 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js @@ -13,50 +13,51 @@ function valRegex() { link: function (scope, elm, attrs, ctrl) { var flags = ""; - if (attrs.valRegexFlags) { - try { - flags = scope.$eval(attrs.valRegexFlags); - if (!flags) { - flags = attrs.valRegexFlags; + var regex; + + attrs.$observe("valRegexFlags", function (newVal) { + if (newVal) { + flags = newVal; + } + }); + + attrs.$observe("valRegex", function (newVal) { + if (newVal) { + try { + var resolved = newVal; + if (resolved) { + regex = new RegExp(resolved, flags); + } + else { + regex = new RegExp(attrs.valRegex, flags); + } + } + catch (e) { + regex = new RegExp(attrs.valRegex, flags); } } - catch (e) { - flags = attrs.valRegexFlags; - } - } - var regex; - try { - var resolved = scope.$eval(attrs.valRegex); - if (resolved) { - regex = new RegExp(resolved, flags); - } - else { - regex = new RegExp(attrs.valRegex, flags); - } - } - catch(e) { - regex = new RegExp(attrs.valRegex, flags); - } + }); var patternValidator = function (viewValue) { - //NOTE: we don't validate on empty values, use required validator for that - if (!viewValue || regex.test(viewValue)) { - // it is valid - ctrl.$setValidity('valRegex', true); - //assign a message to the validator - ctrl.errorMsg = ""; - return viewValue; - } - else { - // it is invalid, return undefined (no model update) - ctrl.$setValidity('valRegex', false); - //assign a message to the validator - ctrl.errorMsg = "Value is invalid, it does not match the correct pattern"; - return undefined; + if (regex) { + //NOTE: we don't validate on empty values, use required validator for that + if (!viewValue || regex.test(viewValue)) { + // it is valid + ctrl.$setValidity('valRegex', true); + //assign a message to the validator + ctrl.errorMsg = ""; + return viewValue; + } + else { + // it is invalid, return undefined (no model update) + ctrl.$setValidity('valRegex', false); + //assign a message to the validator + ctrl.errorMsg = "Value is invalid, it does not match the correct pattern"; + return undefined; + } } }; - ctrl.$formatters.push(patternValidator); ctrl.$parsers.push(patternValidator); } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js index c07ee26dec..6fe2dfdf08 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js @@ -11,47 +11,46 @@ function valServerField(serverValidationManager) { restrict: "A", link: function (scope, element, attr, ctrl) { - if (!attr.valServerField) { - throw "valServerField must have a field name for referencing server errors"; - } + var fieldName = null; - var fieldName = attr.valServerField; - var evalfieldName = scope.$eval(attr.valServerField); - if (evalfieldName) { - fieldName = evalfieldName; - } + attr.$observe("valServerField", function (newVal) { + if (newVal && fieldName === null) { + fieldName = newVal; + + //subscribe to the changed event of the view model. This is required because when we + // have a server error we actually invalidate the form which means it cannot be + // resubmitted. So once a field is changed that has a server error assigned to it + // we need to re-validate it for the server side validator so the user can resubmit + // the form. Of course normal client-side validators will continue to execute. + ctrl.$viewChangeListeners.push(function () { + if (ctrl.$invalid) { + ctrl.$setValidity('valServerField', true); + } + }); + + //subscribe to the server validation changes + serverValidationManager.subscribe(null, fieldName, function (isValid, fieldErrors, allErrors) { + if (!isValid) { + ctrl.$setValidity('valServerField', false); + //assign an error msg property to the current validator + ctrl.errorMsg = fieldErrors[0].errorMsg; + } + else { + ctrl.$setValidity('valServerField', true); + //reset the error message + ctrl.errorMsg = ""; + } + }); + + //when the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain + // but they are a different callback instance than the above. + element.bind('$destroy', function () { + serverValidationManager.unsubscribe(null, fieldName); + }); - //subscribe to the changed event of the view model. This is required because when we - // have a server error we actually invalidate the form which means it cannot be - // resubmitted. So once a field is changed that has a server error assigned to it - // we need to re-validate it for the server side validator so the user can resubmit - // the form. Of course normal client-side validators will continue to execute. - ctrl.$viewChangeListeners.push(function () { - if (ctrl.$invalid) { - ctrl.$setValidity('valServerField', true); } }); - - //subscribe to the server validation changes - serverValidationManager.subscribe(null, fieldName, function (isValid, fieldErrors, allErrors) { - if (!isValid) { - ctrl.$setValidity('valServerField', false); - //assign an error msg property to the current validator - ctrl.errorMsg = fieldErrors[0].errorMsg; - } - else { - ctrl.$setValidity('valServerField', true); - //reset the error message - ctrl.errorMsg = ""; - } - }); - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain - // but they are a different callback instance than the above. - element.bind('$destroy', function () { - serverValidationManager.unsubscribe(null, fieldName); - }); } }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html index 3d02980e97..c7fbcb9308 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html @@ -42,6 +42,8 @@ umb-auto-resize required val-server-field="{{'Groups[' + $index + '].Name'}}" /> + + @@ -93,13 +95,21 @@
-
{{ property.alias }}
+ + +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html index 8a70b25c80..a57f7cc1ef 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html @@ -8,20 +8,21 @@ - - + + - Invalid alias + Invalid alias +
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs index 15cc1d6785..17eb6bb35e 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs @@ -60,11 +60,32 @@ namespace Umbraco.Web.Models.ContentEditing if (CompositeContentTypes.Any(x => x.IsNullOrWhiteSpace())) yield return new ValidationResult("Composite Content Type value cannot be null", new[] { "CompositeContentTypes" }); - if (Groups.GroupBy(x => x.Name).Any(x => x.Count() > 1)) - yield return new ValidationResult("Duplicate group names not allowed", new[] { "Groups" }); + var duplicateGroups = Groups.GroupBy(x => x.Name).Where(x => x.Count() > 1).ToArray(); + if (duplicateGroups.Any()) + { + //we need to return the field name with an index so it's wired up correctly + var firstIndex = Groups.IndexOf(duplicateGroups.First().First()); + yield return new ValidationResult("Duplicate group names not allowed", new[] + { + string.Format("Groups[{0}].Name", firstIndex) + }); + } + + var duplicateProperties = Groups.SelectMany(x => x.Properties).Where(x => x.Inherited == false).GroupBy(x => x.Alias).Where(x => x.Count() > 1).ToArray(); + if (duplicateProperties.Any()) + { + //we need to return the field name with an index so it's wired up correctly + var firstProperty = duplicateProperties.First().First(); + var propertyGroup = Groups.Single(x => x.Properties.Contains(firstProperty)); + var groupIndex = Groups.IndexOf(propertyGroup); + var propertyIndex = propertyGroup.Properties.IndexOf(firstProperty); - if (Groups.SelectMany(x => x.Properties).Where(x => x.Inherited == false).GroupBy(x => x.Alias).Any(x => x.Count() > 1)) - yield return new ValidationResult("Duplicate property aliases not allowed", new[] { "Groups" }); + yield return new ValidationResult("Duplicate property aliases not allowed", new[] + { + string.Format("Groups[{0}].Properties[{1}].Alias", groupIndex, propertyIndex) + }); + } + } } } \ No newline at end of file