diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index d54c843c33..0115b6bee9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -83,7 +83,8 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel return labels.propertyHasErrors; } - // return true if there is only a single error left on the property form of either valPropertyMsg or valServer + // check the current errors in the form (and recursive sub forms), if there is 1 or 2 errors + // we can check if those are valPropertyMsg or valServer and can clear our error in those cases. function checkAndClearError() { var errCount = angularHelper.countAllFormErrors(formCtrl); @@ -135,31 +136,55 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel return false; } - // We need to subscribe to any changes to our model (based on user input) - // 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. + // a custom $validator function called on when each child ngModelController changes a value. + function resetServerValidityValidator(fieldController) { + const storedFieldController = fieldController; // pin a reference to this + return (modelValue, viewValue) => { + // if the ngModelController value has changed, then we can check and clear the error + if (storedFieldController.$dirty) { + if (checkAndClearError()) { + resetError(); + } + } + return true; // this validator is always 'valid' + }; + } + + // collect all ng-model controllers recursively within the umbProperty form + // until it reaches the next nested umbProperty form + function collectAllNgModelControllersRecursively(controls, ngModels) { + controls.forEach(ctrl => { + if (angularHelper.isForm(ctrl)) { + // if it's not another umbProperty form then recurse + if (ctrl.$name !== formCtrl.$name) { + collectAllNgModelControllersRecursively(ctrl.$getControls(), ngModels); + } + } + else if (ctrl.hasOwnProperty('$modelValue') && Utilities.isObject(ctrl.$validators)) { + ngModels.push(ctrl); + } + }); + } + + // We start the watch when there's server validation errors detected. + // We watch on the current form's properties and on first watch or if they are dynamically changed + // we find all ngModel controls recursively on this form (but stop recursing before we get to the next) + // umbProperty form). Then for each ngModelController we assign a new $validator. This $validator + // will execute whenever the value is changed which allows us to check and reset the server validator function startWatch() { - - //if there's not already a watch - if (!watcher) { - watcher = scope.$watch("currentProperty.value", - function (newValue, oldValue) { - if (angular.equals(newValue, oldValue)) { - return; - } - if (checkAndClearError()) { - resetError(); - } - else if (showValidation && scope.errorMsg === "") { - formCtrl.$setValidity('valPropertyMsg', false, formCtrl); - scope.errorMsg = getErrorMsg(); - } - }, true); + watcher = scope.$watchCollection( + () => formCtrl, + function (updatedFormController) { + var ngModels = []; + collectAllNgModelControllersRecursively(updatedFormController.$getControls(), ngModels); + ngModels.forEach(x => { + if (!x.$validators.serverValidityResetter) { + x.$validators.serverValidityResetter = resetServerValidityValidator(x); + } + }); + }); } } @@ -174,11 +199,13 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel function resetError() { stopWatch(); hasError = false; - formCtrl.$setValidity('valPropertyMsg', true, formCtrl); + formCtrl.$setValidity('valPropertyMsg', true); scope.errorMsg = ""; } + // This deals with client side validation changes and is executed anytime validators change on the containing + // valFormManager. This allows us to know when to display or clear the property error data for non-server side errors. function checkValidationStatus() { if (formCtrl.$invalid) { //first we need to check if the valPropertyMsg validity is invalid @@ -270,13 +297,22 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel // the correct field validation in their property editors. function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { + var hadError = hasError; hasError = !isValid; if (hasError) { //set the error message to the server message scope.errorMsg = propertyErrors.length > 1 ? labels.propertyHasErrors : propertyErrors[0].errorMsg || labels.propertyHasErrors; //flag that the current validator is invalid - formCtrl.$setValidity('valPropertyMsg', false, formCtrl); + formCtrl.$setValidity('valPropertyMsg', false); startWatch(); + + + if (propertyErrors.length === 1 && hadError && !formCtrl.$pristine) { + var propertyValidationPath = umbPropCtrl.getValidationPath(); + console.log("only 1 left, clearing! " + propertyValidationPath); + serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, "", currentSegment); + resetError(); + } } else { resetError(); @@ -289,7 +325,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel "", serverValidationManagerCallback, currentSegment, - { matchType: "suffix" } // match property validation path prefix + { matchType: "prefix" } // match property validation path prefix )); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js index 766f3f6755..b8e1b5cac8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js @@ -43,10 +43,10 @@ function valServerMatch(serverValidationManager) { //subscribe to the server validation changes function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { if (!isValid) { - formCtrl.$setValidity('valServerMatch', false, formCtrl); + formCtrl.$setValidity('valServerMatch', false); } else { - formCtrl.$setValidity('valServerMatch', true, formCtrl); + formCtrl.$setValidity('valServerMatch', true); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js index 8c384455bb..fd620bac18 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js @@ -18,8 +18,13 @@ function angularHelper($q) { } keys.forEach(validationKey => { var ctrls = formCtrl.$error[validationKey]; - ctrls.forEach(ctrl => { - if (isForm(ctrl)) { + ctrls.forEach(ctrl => { + if (!ctrl) { + // this happens when $setValidity('err', true) is called on a form controller without specifying the 3rd parameter for the control/form + // which just means that this is an error on the formCtrl itself + allErrors.push(formCtrl); // add the error + } + else if (isForm(ctrl)) { // sometimes the control in error is the same form so we cannot recurse else we'll cause an infinite loop // and in this case it means the error is assigned directly to the form, not a control if (ctrl === formCtrl) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index 53309d63f5..e6f114634f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -382,9 +382,7 @@ function serverValidationManager($timeout) { fieldName: fieldName, errorMsg: errorMsg }); - } - - notifyCallbacks(); + } } /** @@ -446,6 +444,7 @@ function serverValidationManager($timeout) { throw "modelState is not an object"; } + var hasPropertyErrors = false; for (const [key, value] of Object.entries(modelState)) { //This is where things get interesting.... @@ -506,6 +505,7 @@ function serverValidationManager($timeout) { // add a generic error for the property addPropertyError(propertyValidationKey, culture, htmlFieldReference, value && Array.isArray(value) && value.length > 0 ? value[0] : null, segment); + hasPropertyErrors = true; } else { @@ -514,6 +514,11 @@ function serverValidationManager($timeout) { addFieldError(key, value[0]); } } + + if (hasPropertyError) { + // ensure all callbacks are called after property errors are added + notifyCallbacks(); + } } function createPropertyValidationKey(propertyAlias, parentValidationPath) { @@ -718,7 +723,11 @@ function serverValidationManager($timeout) { getVariantCallbacks: getVariantCallbacks, addFieldError: addFieldError, - addPropertyError: addPropertyError, + + addPropertyError: function (propertyAlias, culture, fieldName, errorMsg, segment) { + addPropertyError(propertyAlias, culture, fieldName, errorMsg, segment); + notifyCallbacks(); // ensure all callbacks are called + }, /** * @ngdoc function diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index 716693c778..0f2b1402dc 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -38,6 +38,15 @@ position: relative; text-align: left; background: @white; + border: 1px solid @gray-9; + border-radius: @baseBorderRadius; + transition: border-color 120ms; + margin-bottom: 4px; + margin-top: 4px; + + &.--error { + border-color: @formErrorBorder !important; + } } .umb-nested-content__item.ui-sortable-placeholder { @@ -54,7 +63,7 @@ } .umb-nested-content__header-bar { - border-bottom: 1px solid @gray-9; + cursor: pointer; background-color: @white; @@ -207,9 +216,9 @@ .umb-nested-content__content { border-top: 1px solid transparent; - border-bottom: 1px solid @gray-9; - border-left: 1px solid @gray-9; - border-right: 1px solid @gray-9; + border-bottom: 1px solid transparent; + border-left: 1px solid transparent; + border-right: 1px solid transparent; border-radius: 0 0 3px 3px; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less index babbcbf2b7..a31ffc172c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less @@ -54,9 +54,7 @@ } &.--error { - color: @ui-active-type; - border-color: @ui-active; - background-color: @ui-active; + border-color: @formErrorBorder !important; } } .blockelement-inlineblock-editor__inner { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less index 8e4709af5d..f9fde1da97 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less @@ -39,8 +39,6 @@ } &.--error { - color: @ui-active-type; - border-color: @ui-active; - background-color: @ui-active; + border-color: @formErrorBorder !important; } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html index da6e466b50..896f8cf4a4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html @@ -6,30 +6,41 @@