From c4e7929e684a330ea92d207bc85f067e33994004 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Jul 2020 12:50:15 +1000 Subject: [PATCH] Converts umbProperty to a component, gets nested valPropertyMsg validators clearing (as a prototype), need to check TODOs, test inline editing, etc... --- .../property/umbproperty.directive.js | 136 ++++++++++-------- .../validation/valformmanager.directive.js | 129 +++++++++++------ .../validation/valpropertymsg.directive.js | 103 ++++++++----- .../validation/valserver.directive.js | 7 +- .../services/servervalidationmgr.service.js | 29 ++-- src/Umbraco.Web.UI.Client/src/less/main.less | 2 +- .../components/property/umb-property.html | 21 +-- .../NuCache/DataSource/DatabaseDataSource.cs | 6 +- 8 files changed, 267 insertions(+), 166 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index 8b3f51f0f9..1d100494f6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -3,8 +3,81 @@ * @name umbraco.directives.directive:umbProperty * @restrict E **/ -angular.module("umbraco.directives") - .directive('umbProperty', function (userService, serverValidationManager, udiService) { +(function () { + 'use strict'; + + angular + .module("umbraco.directives") + .component('umbProperty', { + templateUrl: 'views/components/property/umb-property.html', + controller: UmbPropertyController, + controllerAs: 'vm', + transclude: true, + require: { + parentUmbProperty: '?^^umbProperty' + }, + bindings: { + property: "=", + elementUdi: "@", + // optional, if set this will be used for the property alias validation path (hack required because NC changes the actual property.alias :/) + propertyAlias: "@", + showInherit: "<", + inheritsFrom: "<" + } + }); + + + + function UmbPropertyController($scope, userService, serverValidationManager, udiService, angularHelper) { + + const vm = this; + + vm.$onInit = onInit; + + vm.setPropertyError = function (errorMsg) { + vm.property.propertyErrorMessage = errorMsg; + }; + + vm.propertyActions = []; + vm.setPropertyActions = function (actions) { + vm.propertyActions = actions; + }; + + // returns the unique Id for the property to be used as the validation key for server side validation logic + vm.getValidationPath = function () { + + // the elementUdi will be empty when this is not a nested property + var propAlias = vm.propertyAlias ? vm.propertyAlias : vm.property.alias; + vm.elementUdi = ensureUdi(vm.elementUdi); + return serverValidationManager.createPropertyValidationKey(propAlias, vm.elementUdi); + } + + vm.getParentValidationPath = function () { + if (!vm.parentUmbProperty) { + return null; + } + return vm.parentUmbProperty.getValidationPath(); + } + + function onInit() { + vm.controlLabelTitle = null; + if (Umbraco.Sys.ServerVariables.isDebuggingEnabled) { + userService.getCurrentUser().then(function (u) { + if (u.allowedSections.indexOf("settings") !== -1 ? true : false) { + vm.controlLabelTitle = vm.property.alias; + } + }); + } + + vm.elementUdi = ensureUdi(vm.elementUdi); + + if (!vm.parentUmbProperty) { + // not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope + // inheritance is (i.e.infinite editing) + var found = angularHelper.traverseScopeChain($scope, s => s && s.vm && s.vm.constructor.name === "UmbPropertyController"); + vm.parentUmbProperty = found ? found.vm : null; + } + } // if only a guid is passed in, we'll ensure a correct udi structure function ensureUdi(udi) { @@ -13,61 +86,6 @@ angular.module("umbraco.directives") } return udi; } + } - return { - scope: { - property: "=", - elementUdi: "@", - // optional, if set this will be used for the property alias validation path (hack required because NC changes the actual property.alias :/) - propertyAlias: "@", - showInherit: "<", - inheritsFrom: "<" - }, - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/property/umb-property.html', - link: function (scope, element, attr, ctrls) { - - scope.controlLabelTitle = null; - if(Umbraco.Sys.ServerVariables.isDebuggingEnabled) { - userService.getCurrentUser().then(function (u) { - if(u.allowedSections.indexOf("settings") !== -1 ? true : false) { - scope.controlLabelTitle = scope.property.alias; - } - }); - } - - scope.elementUdi = ensureUdi(scope.elementUdi); - - }, - //Define a controller for this directive to expose APIs to other directives - controller: function ($scope) { - - var self = this; - - //set the API properties/methods - - self.property = $scope.property; - self.setPropertyError = function (errorMsg) { - $scope.property.propertyErrorMessage = errorMsg; - }; - - $scope.propertyActions = []; - self.setPropertyActions = function(actions) { - $scope.propertyActions = actions; - }; - - // returns the unique Id for the property to be used as the validation key for server side validation logic - self.getValidationPath = function () { - - // the elementUdi will be empty when this is not a nested property - var propAlias = $scope.propertyAlias ? $scope.propertyAlias : $scope.property.alias; - $scope.elementUdi = ensureUdi($scope.elementUdi); - return serverValidationManager.createPropertyValidationKey(propAlias, $scope.elementUdi); - } - $scope.getValidationPath = self.getValidationPath; - - } - }; - }); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js index 6638ed4e6d..86ea94914a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js @@ -12,48 +12,88 @@ * Another thing this directive does is to ensure that any .control-group that contains form elements that are invalid will * be marked with the 'error' css class. This ensures that labels included in that control group are styled correctly. **/ -function valFormManager(serverValidationManager, $rootScope, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService) { +function valFormManager(serverValidationManager, $rootScope, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService, angularHelper) { var SHOW_VALIDATION_CLASS_NAME = "show-validation"; var SAVING_EVENT_NAME = "formSubmitting"; var SAVED_EVENT_NAME = "formSubmitted"; + function notify(scope) { + scope.$broadcast("valStatusChanged", { form: scope.formCtrl }); + } + + function ValFormManagerController($scope) { + //This exposes an API for direct use with this directive + + // We need this as a way to reference this directive in the scope chain. Since this directive isn't a component and + // because it's an attribute instead of an element, we can't use controllerAs or anything like that. Plus since this is + // an attribute an isolated scope doesn't work so it's a bit weird. By doing this we are able to lookup the parent valFormManager + // in the scope hierarchy even if the DOM hierarchy doesn't match (i.e. in infinite editing) + $scope.valFormManager = this; + + var unsubscribe = []; + var self = this; + + //This is basically the same as a directive subscribing to an event but maybe a little + // nicer since the other directive can use this directive's API instead of a magical event + this.onValidationStatusChanged = function (cb) { + unsubscribe.push($scope.$on("valStatusChanged", function (evt, args) { + cb.apply(self, [evt, args]); + })); + }; + + this.showValidation = $scope.showValidation === true; + + this.notify = function () { + notify($scope); + } + + this.isValid = function () { + return !$scope.formCtrl.$invalid; + } + + //Ensure to remove the event handlers when this instance is destroyted + $scope.$on('$destroy', function () { + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); + } + + /** + * Find's the valFormManager in the scope/DOM hierarchy + * @param {any} scope + * @param {any} ctrls + * @param {any} index + */ + function getAncestorValFormManager(scope, ctrls, index) { + + // first check the normal directive inheritance which relies on DOM inheritance + var found = ctrls[index]; + if (found) { + return found; + } + + // not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope + // inheritance is (i.e.infinite editing) + var found = angularHelper.traverseScopeChain(scope, s => s && s.valFormManager && s.valFormManager.constructor.name === "ValFormManagerController"); + return found ? found.valFormManager : null; + } + return { require: ["form", "^^?valFormManager", "^^?valSubView"], restrict: "A", - controller: function($scope) { - //This exposes an API for direct use with this directive - - var unsubscribe = []; - var self = this; - - //This is basically the same as a directive subscribing to an event but maybe a little - // nicer since the other directive can use this directive's API instead of a magical event - this.onValidationStatusChanged = function (cb) { - unsubscribe.push($scope.$on("valStatusChanged", function(evt, args) { - cb.apply(self, [evt, args]); - })); - }; - - this.showValidation = $scope.showValidation === true; - - //Ensure to remove the event handlers when this instance is destroyted - $scope.$on('$destroy', function () { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - }, + controller: ValFormManagerController, link: function (scope, element, attr, ctrls) { function notifySubView() { - if (subView){ + if (subView) { subView.valStatusChanged({ form: formCtrl, showValidation: scope.showValidation }); } } - var formCtrl = ctrls[0]; - var parentFormMgr = ctrls.length > 0 ? ctrls[1] : null; + var formCtrl = scope.formCtrl = ctrls[0]; + var parentFormMgr = scope.parentFormMgr = getAncestorValFormManager(scope, ctrls, 1); var subView = ctrls.length > 1 ? ctrls[2] : null; var labels = {}; @@ -81,14 +121,14 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location var validatorLengths = _.map(formCtrl.$error, function (val, key) { // if there are child ng-forms, include the $error collections in those as well var innerErrorCount = _.reduce( - _.map(val, v => - _.reduce( - _.map(v.$error, e => e.length), - (m, n) => m + n - ) - ), - (memo, num) => memo + num - ); + _.map(val, v => + _.reduce( + _.map(v.$error, e => e.length), + (m, n) => m + n + ) + ), + (memo, num) => memo + num + ); return val.length + innerErrorCount; }); //sum up all numbers in the resulting array @@ -98,8 +138,9 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location //this is the value we watch to notify of any validation changes on the form return sum; }, function (e) { - scope.$broadcast("valStatusChanged", { form: formCtrl }); + notify(scope); + notifySubView(); //find all invalid elements' .control-group's and apply the error class @@ -128,7 +169,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location var unsubscribe = []; //listen for the forms saving event - unsubscribe.push(scope.$on(SAVING_EVENT_NAME, function(ev, args) { + unsubscribe.push(scope.$on(SAVING_EVENT_NAME, function (ev, args) { element.addClass(SHOW_VALIDATION_CLASS_NAME); scope.showValidation = true; notifySubView(); @@ -137,7 +178,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location })); //listen for the forms saved event - unsubscribe.push(scope.$on(SAVED_EVENT_NAME, function(ev, args) { + unsubscribe.push(scope.$on(SAVED_EVENT_NAME, function (ev, args) { //remove validation class element.removeClass(SHOW_VALIDATION_CLASS_NAME); scope.showValidation = false; @@ -151,7 +192,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location //This handles the 'unsaved changes' dialog which is triggered when a route is attempting to be changed but // the form has pending changes - var locationEvent = $rootScope.$on('$locationChangeStart', function(event, nextLocation, currentLocation) { + var locationEvent = $rootScope.$on('$locationChangeStart', function (event, nextLocation, currentLocation) { var infiniteEditors = editorService.getEditors(); @@ -178,10 +219,10 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location "disableEscKey": true, "submitButtonLabel": labels.stayButton, "closeButtonLabel": labels.discardChangesButton, - submit: function() { + submit: function () { overlayService.close(); }, - close: function() { + close: function () { // close all editors editorService.closeAll(); // allow redirection @@ -190,7 +231,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location var parts = nextPath.split("?"); var query = {}; if (parts.length > 1) { - _.each(parts[1].split("&"), function(q) { + _.each(parts[1].split("&"), function (q) { var keyVal = q.split("="); query[keyVal[0]] = keyVal[1]; }); @@ -215,13 +256,13 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location unsubscribe.push(locationEvent); //Ensure to remove the event handler when this instance is destroyted - scope.$on('$destroy', function() { + scope.$on('$destroy', function () { for (var u in unsubscribe) { unsubscribe[u](); } }); - $timeout(function(){ + $timeout(function () { formCtrl.$setPristine(); }, 1000); 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 27fa67695b..30ba459baf 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 @@ -8,24 +8,26 @@ * We will listen for server side validation changes * and when an error is detected for this property we'll show the error message. * In order for this directive to work, the valFormManager directive must be placed on the containing form. +* We don't set the validity of this validator to false when client side validation fails, only when server side +* validation fails however we do respond to the client side validation changes to display error and adjust UI state. **/ -function valPropertyMsg(serverValidationManager, localizationService) { +function valPropertyMsg(serverValidationManager, localizationService, angularHelper) { return { - require: ['^^form', '^^valFormManager', '^^umbProperty', '?^^umbVariantContent'], + require: ['^^form', '^^valFormManager', '^^umbProperty', '?^^umbVariantContent', '?^^valPropertyMsg'], replace: true, restrict: "E", template: "
{{errorMsg}}
", scope: {}, link: function (scope, element, attrs, ctrl) { - + var unsubscribe = []; var watcher = null; var hasError = false; //create properties on our custom scope so we can use it in our template - scope.errorMsg = ""; - + scope.errorMsg = ""; + //the property form controller api var formCtrl = ctrl[0]; //the valFormManager controller api @@ -33,16 +35,16 @@ function valPropertyMsg(serverValidationManager, localizationService) { //the property controller api var umbPropCtrl = ctrl[2]; //the variants controller api - var umbVariantCtrl = ctrl[3]; - + var umbVariantCtrl = ctrl[3]; + var currentProperty = umbPropCtrl.property; scope.currentProperty = currentProperty; var propertyValidationKey = umbPropCtrl.getValidationPath(); var currentCulture = currentProperty.culture; - var currentSegment = currentProperty.segment; - + var currentSegment = currentProperty.segment; + // validation object won't exist when editor loads outside the content form (ie in settings section when modifying a content type) var isMandatory = currentProperty.validation ? currentProperty.validation.mandatory : undefined; @@ -63,10 +65,10 @@ function valPropertyMsg(serverValidationManager, localizationService) { return; } } - + // if we have reached this part, and there is no culture, then lets fallback to invariant. To get the validation feedback for invariant language. currentCulture = currentCulture || "invariant"; - + // Gets the error message to display function getErrorMsg() { @@ -93,7 +95,14 @@ function valPropertyMsg(serverValidationManager, localizationService) { // 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. function startWatch() { + + // TODO: Can we watch on something other than the value?? This doesn't work for complex editors especially once that have a + // viewmodel/model setup the value that this is watching doesn't actually get updated by a sub-editor in all cases. + // we can probably watch the formCtrl view value? But then we also don't want this to watch complex values that have sub editors anyways + // since that might end up clearing the whole chain of valPropertyMsg when a sub value is changed (in some cases, not with the block editor). + //if there's not already a watch + if (!watcher) { watcher = scope.$watch("currentProperty.value", function (newValue, oldValue) { @@ -107,20 +116,19 @@ function valPropertyMsg(serverValidationManager, localizationService) { if (Utilities.isArray(formCtrl.$error[e])) { errCount++; } - } + } //we are explicitly checking for valServer errors here, since we shouldn't auto clear // based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg // is the only one, then we'll clear. - if (errCount === 0 - || (errCount === 1 && Utilities.isArray(formCtrl.$error.valPropertyMsg)) + if (errCount === 0 + || (errCount === 1 && Utilities.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && Utilities.isArray(formCtrl.$error.valServer))) { - scope.errorMsg = ""; - formCtrl.$setValidity('valPropertyMsg', true); + resetError(); } else if (showValidation && scope.errorMsg === "") { - formCtrl.$setValidity('valPropertyMsg', false); + formCtrl.$setValidity('valPropertyMsg', false, formCtrl); scope.errorMsg = getErrorMsg(); } }, true); @@ -135,6 +143,35 @@ function valPropertyMsg(serverValidationManager, localizationService) { } } + function resetError() { + var hadError = hasError; + hasError = false; + formCtrl.$setValidity('valPropertyMsg', true, formCtrl); + scope.errorMsg = ""; + stopWatch(); + + // if we had an error, then check on the current valFormManager to see if it's + // now valid, if it is it means that the containing form (i.e. the form rendering) + // properties for an element/content type) is now valid which means we can clear + // the parent's valPropertyMsg if there is one. This will only occur with complex editors + // where we have nested umb-property components. + if (hadError) { + scope.$evalAsync(function () { + if (valFormManager.isValid()) { + // TODO: I think we might have to use formCtrl.$$parentForm here since when using inline editing like + // nestedcontent style, there won't be a 'parent' valFormManager, or will there? i'm unsure + var parentValidationKey = umbPropCtrl.getParentValidationPath(); + if (parentValidationKey) { + var parentForm = formCtrl.$$parentForm; + serverValidationManager.removePropertyError(parentValidationKey, currentCulture, "", currentSegment); + } + } + }); + + + } + } + function checkValidationStatus() { if (formCtrl.$invalid) { //first we need to check if the valPropertyMsg validity is invalid @@ -145,12 +182,11 @@ function valPropertyMsg(serverValidationManager, localizationService) { } //if there are any errors in the current property form that are not valPropertyMsg else if (_.without(_.keys(formCtrl.$error), "valPropertyMsg").length > 0) { - + // errors exist, but if the property is NOT mandatory and has no value, the errors should be cleared if (isMandatory !== undefined && isMandatory === false && !currentProperty.value) { - hasError = false; - showValidation = false; - scope.errorMsg = ""; + + resetError(); // if there's no value, the controls can be reset, which clears the error state on formCtrl for (let control of formCtrl.$getControls()) { @@ -167,13 +203,11 @@ function valPropertyMsg(serverValidationManager, localizationService) { } } else { - hasError = false; - scope.errorMsg = ""; + resetError(); } } else { - hasError = false; - scope.errorMsg = ""; + resetError(); } } @@ -203,17 +237,14 @@ function valPropertyMsg(serverValidationManager, localizationService) { startWatch(); } else if (!hasError) { - scope.errorMsg = ""; - stopWatch(); + resetError(); } })); //listen for the forms saved event unsubscribe.push(scope.$on("formSubmitted", function (ev, args) { showValidation = false; - scope.errorMsg = ""; - formCtrl.$setValidity('valPropertyMsg', true); - stopWatch(); + resetError(); })); //listen for server validation changes @@ -224,7 +255,7 @@ function valPropertyMsg(serverValidationManager, localizationService) { // 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. - if (scope.currentProperty) { //this can be null if no property was assigned + if (scope.currentProperty) { //this can be null if no property was assigned, TODO: I don't believe it can? If it was null we'd get errors above function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { hasError = !isValid; @@ -232,14 +263,11 @@ function valPropertyMsg(serverValidationManager, localizationService) { //set the error message to the server message scope.errorMsg = propertyErrors[0].errorMsg ? propertyErrors[0].errorMsg : labels.propertyHasErrors; //flag that the current validator is invalid - formCtrl.$setValidity('valPropertyMsg', false); + formCtrl.$setValidity('valPropertyMsg', false, formCtrl); startWatch(); } else { - scope.errorMsg = ""; - //flag that the current validator is valid - formCtrl.$setValidity('valPropertyMsg', true); - stopWatch(); + resetError(); } } @@ -248,9 +276,8 @@ function valPropertyMsg(serverValidationManager, localizationService) { "", serverValidationManagerCallback, currentSegment - ) + ) ); - } //when the scope is disposed we need to unsubscribe diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js index 701a344838..f002d83360 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js @@ -61,8 +61,8 @@ function valServer(serverValidationManager) { return propertyValidationPath ? propertyValidationPath : currentProperty.alias; } - //Need to watch the value model for it to change, previously we had subscribed to - //modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that + // Need to watch the value model for it to change, previously we had subscribed to + // modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that // doesn't specifically have a 2 way ng binding. 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 @@ -81,6 +81,7 @@ function valServer(serverValidationManager) { if (modelCtrl.$invalid) { modelCtrl.$setValidity('valServer', true); + console.log("valServer cleared (watch)"); //clear the server validation entry serverValidationManager.removePropertyError(getPropertyValidationKey(), currentCulture, fieldName, currentSegment); @@ -101,12 +102,14 @@ function valServer(serverValidationManager) { function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { if (!isValid) { modelCtrl.$setValidity('valServer', false); + console.log("valServer error"); //assign an error msg property to the current validator modelCtrl.errorMsg = propertyErrors[0].errorMsg; startWatch(); } else { modelCtrl.$setValidity('valServer', true); + console.log("valServer cleared"); //reset the error message modelCtrl.errorMsg = ""; stopWatch(); 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 5e65f49794..c6382f1f73 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 @@ -16,10 +16,10 @@ function serverValidationManager($timeout, udiService) { var items = []; /** calls the callback specified with the errors specified, used internally */ - function executeCallback(errorsForCallback, callback, culture, segment) { + function executeCallback(errorsForCallback, callback, culture, segment, isValid) { callback.apply(instance, [ - false, // pass in a value indicating it is invalid + isValid, // pass in a value indicating it is invalid errorsForCallback, // pass in the errors for this item items, // pass in all errors in total culture, // pass the culture that we are listing for. @@ -99,21 +99,21 @@ function serverValidationManager($timeout, udiService) { //its a field error callback var fieldErrors = getFieldErrors(cb.fieldName); if (fieldErrors.length > 0) { - executeCallback(fieldErrors, cb.callback, cb.culture, cb.segment); + executeCallback(fieldErrors, cb.callback, cb.culture, cb.segment, false); } } else if (cb.propertyAlias != null) { //its a property error var propErrors = getPropertyErrors(cb.propertyAlias, cb.culture, cb.segment, cb.fieldName); if (propErrors.length > 0) { - executeCallback(propErrors, cb.callback, cb.culture, cb.segment); + executeCallback(propErrors, cb.callback, cb.culture, cb.segment, false); } } else { //its a variant error var variantErrors = getVariantErrors(cb.culture, cb.segment); if (variantErrors.length > 0) { - executeCallback(variantErrors, cb.callback, cb.culture, cb.segment); + executeCallback(variantErrors, cb.callback, cb.culture, cb.segment, false); } } } @@ -247,7 +247,7 @@ function serverValidationManager($timeout, udiService) { var cbs = getFieldCallbacks(fieldName); //call each callback for this error for (var cb in cbs) { - executeCallback(errorsForCallback, cbs[cb].callback, null, null); + executeCallback(errorsForCallback, cbs[cb].callback, null, null, false); } } @@ -309,14 +309,14 @@ function serverValidationManager($timeout, udiService) { var cbs = getPropertyCallbacks(propertyAlias, culture, fieldName, segment); //call each callback for this error for (var cb in cbs) { - executeCallback(errorsForCallback, cbs[cb].callback, culture, segment); + executeCallback(errorsForCallback, cbs[cb].callback, culture, segment, false); } //execute variant specific callbacks here too when a propery error is added var variantCbs = getVariantCallbacks(culture, segment); //call each callback for this error for (var cb in variantCbs) { - executeCallback(errorsForCallback, variantCbs[cb].callback, culture, segment); + executeCallback(errorsForCallback, variantCbs[cb].callback, culture, segment, false); } } @@ -679,7 +679,18 @@ function serverValidationManager($timeout, udiService) { //remove the item items = _.reject(items, function (item) { - return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + var found = (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + if (found) { + //find all errors for this item + var errorsForCallback = getPropertyErrors(propertyAlias, culture, segment, fieldName); + //we should now call all of the call backs registered for this error + var cbs = getPropertyCallbacks(propertyAlias, culture, fieldName, segment); + //call each callback for this error to tell them it is now valid + for (var cb in cbs) { + executeCallback(errorsForCallback, cbs[cb].callback, culture, segment, true); + } + } + return found; }); }, diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 57d867ccd5..ce20b8dc88 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -151,7 +151,7 @@ h6.-black { } } -.umb-property:last-of-type .umb-control-group { +umb-property:last-of-type .umb-control-group { &::after { margin-top: 0px; height: 0; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html index be8495c30a..b3aa7735a2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html @@ -1,31 +1,32 @@
-
+
-
{{ getValidationPath() }}
+
{{ vm.getValidationPath() }}
-
- - {{inheritsFrom}} +
+ + {{vm.inheritsFrom}} -
diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index 694dac04df..f62014a368 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -208,7 +208,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource if (dto.EditData == null) { if (Debugger.IsAttached) - throw new Exception("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding."); + throw new InvalidOperationException("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding."); Current.Logger.Warn("Missing cmsContentNu edited content for node {NodeId}, consider rebuilding.", dto.Id); } else @@ -235,7 +235,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource if (dto.PubData == null) { if (Debugger.IsAttached) - throw new Exception("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding."); + throw new InvalidOperationException("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding."); Current.Logger.Warn("Missing cmsContentNu published content for node {NodeId}, consider rebuilding.", dto.Id); } else @@ -274,7 +274,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private static ContentNodeKit CreateMediaNodeKit(ContentSourceDto dto) { if (dto.EditData == null) - throw new Exception("No data for media " + dto.Id); + throw new InvalidOperationException("No data for media " + dto.Id); var nested = DeserializeNestedData(dto.EditData);