From 88700e97071b6528ef5e7b510e366c90cff3b4c9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 30 Jun 2020 19:12:21 +1000 Subject: [PATCH] fixes name, refactors a lot of serverValidationManager so it's structure is maintainable, moves more logic there from directives so it can be tested more easily, finally got the first result working. --- ...ter.cs => BlockListEditorDataConverter.cs} | 4 +- .../property/umbproperty.directive.js | 10 +- .../validation/valpropertymsg.directive.js | 6 +- .../validation/valserver.directive.js | 30 +- .../src/common/services/formhelper.service.js | 74 +- .../services/servervalidationmgr.service.js | 720 ++++++++++-------- .../src/common/services/udi.service.js | 8 +- .../nestedcontent/nestedcontent.editor.html | 2 +- .../BlockEditorPropertyEditor.cs | 2 +- .../BlockListPropertyValueConverter.cs | 2 +- 10 files changed, 459 insertions(+), 399 deletions(-) rename src/Umbraco.Core/Models/Blocks/{BlocListEditorDataConverter.cs => BlockListEditorDataConverter.cs} (75%) diff --git a/src/Umbraco.Core/Models/Blocks/BlocListEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs similarity index 75% rename from src/Umbraco.Core/Models/Blocks/BlocListEditorDataConverter.cs rename to src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs index 4cb7dbe1c5..eea2dfafd0 100644 --- a/src/Umbraco.Core/Models/Blocks/BlocListEditorDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs @@ -7,9 +7,9 @@ namespace Umbraco.Core.Models.Blocks /// /// Data converter for the block list property editor /// - public class BlocListEditorDataConverter : BlockEditorDataConverter + public class BlockListEditorDataConverter : BlockEditorDataConverter { - public BlocListEditorDataConverter() : base(Constants.PropertyEditors.Aliases.BlockList) + public BlockListEditorDataConverter() : base(Constants.PropertyEditors.Aliases.BlockList) { } 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 3f37d06d2b..b9e607ecc9 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 @@ -4,7 +4,7 @@ * @restrict E **/ angular.module("umbraco.directives") - .directive('umbProperty', function (userService) { + .directive('umbProperty', function (userService, serverValidationManager, udiService) { return { scope: { property: "=", @@ -28,6 +28,11 @@ angular.module("umbraco.directives") } }); } + + if (scope.elementUdi && !scope.elementUdi.startsWith("umb://")) { + scope.elementUdi = udiService.build("element", scope.elementUdi); + } + }, //Define a controller for this directive to expose APIs to other directives controller: function ($scope) { @@ -48,9 +53,10 @@ angular.module("umbraco.directives") // 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; - return $scope.elementUdi ? ($scope.elementUdi + "/" + propAlias) : propAlias; + return serverValidationManager.createPropertyValidationKey(propAlias, $scope.elementUdi); } $scope.getValidationPath = self.getValidationPath; 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 30d3530efb..059a4c6853 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 @@ -38,6 +38,8 @@ function valPropertyMsg(serverValidationManager, localizationService) { var currentProperty = umbPropCtrl.property; scope.currentProperty = currentProperty; + var propertyValidationKey = umbPropCtrl.getValidationPath(); + var currentCulture = currentProperty.culture; var currentSegment = currentProperty.segment; @@ -71,7 +73,7 @@ function valPropertyMsg(serverValidationManager, localizationService) { //this can be null if no property was assigned if (scope.currentProperty) { //first try to get the error msg from the server collection - var err = serverValidationManager.getPropertyError(scope.currentProperty.alias, null, "", null); + var err = serverValidationManager.getPropertyError(propertyValidationKey, null, "", null); //if there's an error message use it if (err && err.errorMsg) { return err.errorMsg; @@ -240,7 +242,7 @@ function valPropertyMsg(serverValidationManager, localizationService) { } } - unsubscribe.push(serverValidationManager.subscribe(scope.currentProperty.alias, + unsubscribe.push(serverValidationManager.subscribe(propertyValidationKey, currentCulture, "", serverValidationManagerCallback, 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 d3a7035cac..04f1496a30 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 @@ -7,7 +7,7 @@ **/ function valServer(serverValidationManager) { return { - require: ['ngModel', '?^^umbProperty', '?^^umbVariantContent', '?^^umbNestedProperty'], + require: ['ngModel', '?^^umbProperty', '?^^umbVariantContent'], restrict: "A", scope: {}, link: function (scope, element, attr, ctrls) { @@ -21,7 +21,6 @@ function valServer(serverValidationManager) { // optional reference to the varaint-content-controller, needed to avoid validation when the field is invariant on non-default languages. var umbVariantCtrl = ctrls.length > 2 ? ctrls[2] : null; - var umbNestedPropertyCtrl = ctrls.length > 3 ? ctrls[3] : null; var currentProperty = umbPropCtrl.property; var currentCulture = currentProperty.culture; @@ -56,6 +55,14 @@ function valServer(serverValidationManager) { } } + function getPropertyValidationKey() { + // Get the property validation path if there is one, this is how wiring up any nested/virtual property validation works + var propertyValidationPath = umbPropCtrl ? umbPropCtrl.getValidationPath() : null; + // TODO: Is this going to break with nested content because it changes the alias? + // Hrm, don't think so because NC will use the property validation path + 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 // doesn't specifically have a 2 way ng binding. This is required because when we @@ -78,9 +85,7 @@ function valServer(serverValidationManager) { modelCtrl.$setValidity('valServer', true); //clear the server validation entry - // TODO: We'll need to handle this differently since this will need to target the actual 'fieldName' or validation - // path if there is one - serverValidationManager.removePropertyError(currentProperty.alias, currentCulture, fieldName, currentSegment); + serverValidationManager.removePropertyError(getPropertyValidationKey(), currentCulture, fieldName, currentSegment); stopWatch(); } }, true); @@ -110,21 +115,12 @@ function valServer(serverValidationManager) { } } - // TODO: If this is a property/field within a complex editor which means it could be a nested/nested/nested property/field - // TODO: We have a block $id to work with now so that is what we should be looking to use for the 'key' - - var propertyValidationPath = umbNestedPropertyCtrl ? umbNestedPropertyCtrl.getValidationPath() : null; + unsubscribe.push(serverValidationManager.subscribe( - currentProperty.alias, + getPropertyValidationKey(), currentCulture, - // use the propertyValidationPath for the fieldName value if there is one since if there is one it means it's a complex - // editor and as such the 'fieldName' will be empty. The serverValidationManager knows how to handle the jsonpath - // string as the fieldName. - // TODO: This isn't quite true! If there is a fieldName specified, then it will need to be added to the - // validation path. We should pass in the fieldName to umbNestedPropertyCtrl.getValidationPath(); since this could very well be targeting a specific field - - propertyValidationPath ? propertyValidationPath : fieldName, + fieldName, serverValidationManagerCallback, currentSegment) ); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js index db558c569a..adca9b30a5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js @@ -147,79 +147,7 @@ function formHelper(angularHelper, serverValidationManager, notificationsService * @param {object} err The error object returned from the http promise */ handleServerValidation: function (modelState) { - for (var e in modelState) { - - //This is where things get interesting.... - // We need to support validation for all editor types such as both the content and content type editors. - // The Content editor ModelState is quite specific with the way that Properties are validated especially considering - // that each property is a User Developer property editor. - // The way that Content Type Editor ModelState is created is simply based on the ASP.Net validation data-annotations - // system. - // So, to do this there's some special ModelState syntax we need to know about. - // For Content Properties, which are user defined, we know that they will exist with a prefixed - // ModelState of "_Properties.", so if we detect this, then we know it's for a content Property. - - //the alias in model state can be in dot notation which indicates - // * the first part is the content property alias - // * the second part is the field to which the valiation msg is associated with - //There will always be at least 4 parts for content properties since all model errors for properties are prefixed with "_Properties" - //If it is not prefixed with "_Properties" that means the error is for a field of the object directly. - - // TODO: This 4 part dot notation isn't ideal and instead it would probably be nicer to have a json structure as the key (which could be converted - // to base64 if we cannot do that since it's a 'key'). That way the key can be flexible and 'future proof' since I'm sure something in the future - // will change for this. Another idea is to just have a single key for one property type and have the model error a json structure that handles - // everything. This would probably be the 'nicest' way but would require quite a lot of work. We are part way there with how we are doing - // validation for complex editors. - - // Example: "_Properties.headerImage.en-US.mySegment.myField" - // * it's for a property since it has a _Properties prefix - // * it's for the headerImage property type - // * it's for the en-US culture - // * it's for the mySegment segment - // * it's for the myField html field (optional) - - var parts = e.split("."); - - //Check if this is for content properties - specific to content/media/member editors because those are special - // user defined properties with custom controls. - if (parts.length > 1 && parts[0] === "_Properties") { - - var propertyAlias = parts[1]; - - var culture = null; - if (parts.length > 2) { - culture = parts[2]; - //special check in case the string is formatted this way - if (culture === "null") { - culture = null; - } - } - - var segment = null; - if (parts.length > 3) { - segment = parts[3]; - //special check in case the string is formatted this way - if (segment === "null") { - segment = null; - } - } - - var htmlFieldReference = ""; - if (parts.length > 4) { - htmlFieldReference = parts[4] || ""; - } - - // add a generic error for the property - serverValidationManager.addPropertyError(propertyAlias, culture, htmlFieldReference, modelState[e][0], segment); - - } else { - - //Everthing else is just a 'Field'... the field name could contain any level of 'parts' though, for example: - // Groups[0].Properties[2].Alias - serverValidationManager.addFieldError(e, modelState[e][0]); - } - - } + serverValidationManager.addErrorsForModelState(modelState); } }; } 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 92e879ecac..a945c71dff 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 @@ -8,35 +8,55 @@ * 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 serverValidationManager($timeout) { +function serverValidationManager($timeout, udiService) { var callbacks = []; + + // The array of error messages + var items = []; /** calls the callback specified with the errors specified, used internally */ - function executeCallback(self, errorsForCallback, callback, culture, segment) { + function executeCallback(errorsForCallback, callback, culture, segment) { - callback.apply(self, [ + callback.apply(instance, [ 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 + items, // pass in all errors in total culture, // pass the culture that we are listing for. segment // pass the segment that we are listing for. ] ); } - function getFieldErrors(self, fieldName) { + /** + * @ngdoc function + * @name notify + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * This method isn't used very often but can be used if all subscriptions need to be notified again. This can be + * handy if a view needs to be reloaded/rebuild like when switching variants in the content editor. This is also used + * when a new subscription occurs and there is already registered errors like dynamically created/shown editors. + */ + function notify() { + $timeout(function () { + notifyCallbacks(); + }); + } + + function getFieldErrors(fieldName) { if (!Utilities.isString(fieldName)) { throw "fieldName must be a string"; } //find errors for this field name - return _.filter(self.items, function (item) { + return _.filter(items, function (item) { return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName); }); } - function getPropertyErrors(self, propertyAlias, culture, segment, fieldName) { + function getPropertyErrors(propertyAlias, culture, segment, fieldName) { if (!Utilities.isString(propertyAlias)) { throw "propertyAlias must be a string"; } @@ -52,12 +72,12 @@ function serverValidationManager($timeout) { } //find all errors for this property - return _.filter(self.items, function (item) { + return _.filter(items, function (item) { return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); } - function getVariantErrors(self, culture, segment) { + function getVariantErrors(culture, segment) { if (!culture) { culture = "invariant"; @@ -67,32 +87,33 @@ function serverValidationManager($timeout) { } //find all errors for this property - return _.filter(self.items, function (item) { + return _.filter(items, function (item) { return (item.culture === culture && item.segment === segment); }); } - function notifyCallbacks(self) { - for (var cb in callbacks) { - if (callbacks[cb].propertyAlias === null && callbacks[cb].fieldName !== null) { + function notifyCallbacks() { + for (var i = 0; i < callbacks.length; i++) { + var cb = callbacks[i]; + if (cb.propertyAlias === null && cb.fieldName !== null) { //its a field error callback - var fieldErrors = getFieldErrors(self, callbacks[cb].fieldName); + var fieldErrors = getFieldErrors(cb.fieldName); if (fieldErrors.length > 0) { - executeCallback(self, fieldErrors, callbacks[cb].callback, callbacks[cb].culture, callbacks[cb].segment); + executeCallback(fieldErrors, cb.callback, cb.culture, cb.segment); } } - else if (callbacks[cb].propertyAlias != null) { + else if (cb.propertyAlias != null) { //its a property error - var propErrors = getPropertyErrors(self, callbacks[cb].propertyAlias, callbacks[cb].culture, callbacks[cb].segment, callbacks[cb].fieldName); + var propErrors = getPropertyErrors(cb.propertyAlias, cb.culture, cb.segment, cb.fieldName); if (propErrors.length > 0) { - executeCallback(self, propErrors, callbacks[cb].callback, callbacks[cb].culture, callbacks[cb].segment); + executeCallback(propErrors, cb.callback, cb.culture, cb.segment); } } else { //its a variant error - var variantErrors = getVariantErrors(self, callbacks[cb].culture, callbacks[cb].segment); + var variantErrors = getVariantErrors(cb.culture, cb.segment); if (variantErrors.length > 0) { - executeCallback(self, variantErrors, callbacks[cb].callback, callbacks[cb].culture, callbacks[cb].segment); + executeCallback(variantErrors, cb.callback, cb.culture, cb.segment); } } } @@ -132,9 +153,347 @@ function serverValidationManager($timeout) { return result; } - return { + /** + * @ngdoc function + * @name getPropertyCallbacks + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Gets all callbacks that has been registered using the subscribe method for the propertyAlias + fieldName combo. + * This will always return any callbacks registered for just the property (i.e. field name is empty) and for ones with an + * explicit field name set. + */ + function getPropertyCallbacks(propertyAlias, culture, fieldName, segment) { + //normalize culture to "invariant" + if (!culture) { + culture = "invariant"; + } + //normalize segment to null + if (!segment) { + segment = null; + } + + var found = _.filter(callbacks, function (item) { + //returns any callback that have been registered directly against the field and for only the property + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === ""))); + }); + return found; + } + + /** + * @ngdoc function + * @name getFieldCallbacks + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Gets all callbacks that has been registered using the subscribe method for the field. + */ + function getFieldCallbacks(fieldName) { + var found = _.filter(callbacks, function (item) { + //returns any callback that have been registered directly against the field + return (item.propertyAlias === null && item.culture === "invariant" && item.segment === null && item.fieldName === fieldName); + }); + return found; + } + + /** + * @ngdoc function + * @name getVariantCallbacks + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Gets all callbacks that has been registered using the subscribe method for the culture and segment. + */ + function getVariantCallbacks(culture, segment) { + var found = _.filter(callbacks, function (item) { + //returns any callback that have been registered directly against the given culture and given segment. + return (item.culture === culture && item.segment === segment && item.propertyAlias === null && item.fieldName === null); + }); + return found; + } + + /** + * @ngdoc function + * @name addFieldError + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Adds an error message for a native content item field (not a user defined property, for Example, 'Name') + */ + function addFieldError(fieldName, errorMsg) { + if (!fieldName) { + return; + } + + //only add the item if it doesn't exist + if (!hasFieldError(fieldName)) { + items.push({ + propertyAlias: null, + culture: "invariant", + segment: null, + fieldName: fieldName, + errorMsg: errorMsg + }); + } + + //find all errors for this item + var errorsForCallback = getFieldErrors(fieldName); + //we should now call all of the call backs registered for this error + var cbs = getFieldCallbacks(fieldName); + //call each callback for this error + for (var cb in cbs) { + executeCallback(errorsForCallback, cbs[cb].callback, null, null); + } + } + + /** + * @ngdoc function + * @name addPropertyError + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Adds an error message for the content property + */ + function addPropertyError(propertyAlias, culture, fieldName, errorMsg, segment) { + + // TODO: We need to handle the errorMsg in a special way to check if this is a json structure. If it is we know we are dealing with + // a complex editor and in which case we'll need to adjust how everything works. + + if (!propertyAlias) { + return; + } + + //normalize culture to "invariant" + if (!culture) { + culture = "invariant"; + } + //normalize segment to null + if (!segment) { + segment = null; + } + + // if the error message is json it's a complex editor validation response that we need to parse + if (errorMsg.startsWith("[")) { + + var idsToErrors = parseComplexEditorError(errorMsg); + for (const [key, value] of Object.entries(idsToErrors)) { + addErrorsForModelState(value, udiService.build("element", key)); + } + + // TODO: Make this the generic "Property has errors" but need to find the lang key for that + errorMsg = "Hello!"; + } + + //only add the item if it doesn't exist + if (!hasPropertyError(propertyAlias, culture, fieldName, segment)) { + items.push({ + propertyAlias: propertyAlias, + culture: culture, + segment: segment, + fieldName: fieldName, + errorMsg: errorMsg + }); + } + + //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 + for (var cb in cbs) { + executeCallback(errorsForCallback, cbs[cb].callback, culture, segment); + } + + //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); + } + } + + /** + * @ngdoc function + * @name hasPropertyError + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Checks if the content property + culture + field name combo has an error + */ + function hasPropertyError(propertyAlias, culture, fieldName, segment) { + + //normalize culture to null + if (!culture) { + culture = "invariant"; + } + //normalize segment to null + if (!segment) { + segment = null; + } + + var err = _.find(items, function (item) { + //return true if the property alias matches and if an empty field name is specified or the field name matches + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + }); + return err ? true : false; + } + + /** + * @ngdoc function + * @name hasFieldError + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Checks if a content field has an error + */ + function hasFieldError(fieldName) { + var err = _.find(items, function (item) { + //return true if the property alias matches and if an empty field name is specified or the field name matches + return (item.propertyAlias === null && item.culture === "invariant" && item.segment === null && item.fieldName === fieldName); + }); + return err ? true : false; + } + + /** + * @ngdoc function + * @name addErrorsForModelState + * @methodOf umbraco.services.serverValidationManager + * @param {any} modelState + * @param {any} elementUdi optional parameter specifying a nested element's UDI for which this property belongs (for complex editors) + * @description + * This wires up all of the server validation model state so that valServer and valServerField directives work + */ + function addErrorsForModelState(modelState, elementUdi) { + for (var e in modelState) { + + //This is where things get interesting.... + // We need to support validation for all editor types such as both the content and content type editors. + // The Content editor ModelState is quite specific with the way that Properties are validated especially considering + // that each property is a User Developer property editor. + // The way that Content Type Editor ModelState is created is simply based on the ASP.Net validation data-annotations + // system. + // So, to do this there's some special ModelState syntax we need to know about. + // For Content Properties, which are user defined, we know that they will exist with a prefixed + // ModelState of "_Properties.", so if we detect this, then we know it's for a content Property. + + //the alias in model state can be in dot notation which indicates + // * the first part is the content property alias + // * the second part is the field to which the valiation msg is associated with + //There will always be at least 4 parts for content properties since all model errors for properties are prefixed with "_Properties" + //If it is not prefixed with "_Properties" that means the error is for a field of the object directly. + + // TODO: This 4 part dot notation isn't ideal and instead it would probably be nicer to have a json structure as the key (which could be converted + // to base64 if we cannot do that since it's a 'key'). That way the key can be flexible and 'future proof' since I'm sure something in the future + // will change for this. Another idea is to just have a single key for one property type and have the model error a json structure that handles + // everything. This would probably be the 'nicest' way but would require quite a lot of work. We are part way there with how we are doing + // validation for complex editors. + + // Example: "_Properties.headerImage.en-US.mySegment.myField" + // * it's for a property since it has a _Properties prefix + // * it's for the headerImage property type + // * it's for the en-US culture + // * it's for the mySegment segment + // * it's for the myField html field (optional) + + var parts = e.split("."); + + //Check if this is for content properties - specific to content/media/member editors because those are special + // user defined properties with custom controls. + if (parts.length > 1 && parts[0] === "_Properties") { + + // create the validation key, might just be the prop alias but if it's nested will be a unique udi + var propertyAlias = createPropertyValidationKey(parts[1], elementUdi); + + var culture = null; + if (parts.length > 2) { + culture = parts[2]; + //special check in case the string is formatted this way + if (culture === "null") { + culture = null; + } + } + + var segment = null; + if (parts.length > 3) { + segment = parts[3]; + //special check in case the string is formatted this way + if (segment === "null") { + segment = null; + } + } + + var htmlFieldReference = ""; + if (parts.length > 4) { + htmlFieldReference = parts[4] || ""; + } + + // add a generic error for the property + addPropertyError(propertyAlias, culture, htmlFieldReference, modelState[e][0], segment); + + } + else { + + //Everthing else is just a 'Field'... the field name could contain any level of 'parts' though, for example: + // Groups[0].Properties[2].Alias + addFieldError(e, modelState[e][0]); + } + + } + } + + // TODO: Write a test or two for this and probs a bunch of other things here too! + function createPropertyValidationKey(propertyAlias, elementUdi) { + return elementUdi ? (elementUdi + "/" + propertyAlias) : propertyAlias; + } + + /** + * @ngdoc function + * @name reset + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Clears all errors and notifies all callbacks that all server errros are now valid - used when submitting a form + */ + function reset() { + clear(); + for (var cb in callbacks) { + callbacks[cb].callback.apply(instance, [ + true, //pass in a value indicating it is VALID + [], //pass in empty collection + [], + null, + null] + ); + } + } + + /** + * @ngdoc function + * @name clear + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Clears all errors + */ + function clear() { + items = []; + } + + var instance = { + + addErrorsForModelState: addErrorsForModelState, parseComplexEditorError: parseComplexEditorError, + createPropertyValidationKey: createPropertyValidationKey, /** * @ngdoc function @@ -143,42 +502,27 @@ function serverValidationManager($timeout) { * @function * * @description - * This method needs to be called once all field and property errors are wired up. + * This method can be called once all field and property errors are wired up. * * In some scenarios where the error collection needs to be persisted over a route change * (i.e. when a content item (or any item) is created and the route redirects to the editor) * the controller should call this method once the data is bound to the scope * so that any persisted validation errors are re-bound to their controls. Once they are re-binded this then clears the validation * colleciton so that if another route change occurs, the previously persisted validation errors are not re-bound to the new item. + * + * In the case of content with complex editors, variants and different views, those editors don't call this method and instead + * manage the server validation manually by calling notify when necessary and clear/reset when necessary. */ notifyAndClearAllSubscriptions: function() { - var self = this; - $timeout(function () { - notifyCallbacks(self); + notifyCallbacks(); //now that they are all executed, we're gonna clear all of the errors we have - self.clear(); + clear(); }); }, - /** - * @ngdoc function - * @name notify - * @methodOf umbraco.services.serverValidationManager - * @function - * - * @description - * This method isn't used very often but can be used if all subscriptions need to be notified again. This can be - * handy if a view needs to be reloaded/rebuild like when switching variants in the content editor. - */ - notify: function() { - var self = this; - - $timeout(function () { - notifyCallbacks(self); - }); - }, + notify: notify, /** * @ngdoc function @@ -209,14 +553,8 @@ function serverValidationManager($timeout) { segment = null; } - // TODO: Check if the fieldName is a jsonpath, we will know this if it starts with $. - // in which case we need to handle this a little differently. - if (fieldName && fieldName.startsWith("$.")) { - // TODO: Or... Do we even need to deal with it differently? Maybe with some luck - // we can just store that path and use it. Lets see how this goes. - } - if (propertyAlias === null) { + callbacks.push({ propertyAlias: null, culture: culture, @@ -227,8 +565,7 @@ function serverValidationManager($timeout) { }); } else if (propertyAlias !== undefined) { - //normalize culture to null - + callbacks.push({ propertyAlias: propertyAlias, culture: culture, @@ -246,6 +583,11 @@ function serverValidationManager($timeout) { }); } + // Now notify the registrations for this callback if we've previously been notified and we're not cleared. + // This will happen for dynamically shown editors, like complex editors that load in sub element types. + // TODO: We need to see what the repercussions of this are in other editors! + notify(); + //return a function to unsubscribe this subscription by uniqueId return unsubscribeId; }, @@ -286,52 +628,8 @@ function serverValidationManager($timeout) { } }, - - /** - * @ngdoc function - * @name getPropertyCallbacks - * @methodOf umbraco.services.serverValidationManager - * @function - * - * @description - * Gets all callbacks that has been registered using the subscribe method for the propertyAlias + fieldName combo. - * This will always return any callbacks registered for just the property (i.e. field name is empty) and for ones with an - * explicit field name set. - */ - getPropertyCallbacks: function (propertyAlias, culture, fieldName, segment) { - - //normalize culture to "invariant" - if (!culture) { - culture = "invariant"; - } - //normalize segment to null - if (!segment) { - segment = null; - } - - var found = _.filter(callbacks, function (item) { - //returns any callback that have been registered directly against the field and for only the property - return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === ""))); - }); - return found; - }, - - /** - * @ngdoc function - * @name getFieldCallbacks - * @methodOf umbraco.services.serverValidationManager - * @function - * - * @description - * Gets all callbacks that has been registered using the subscribe method for the field. - */ - getFieldCallbacks: function (fieldName) { - var found = _.filter(callbacks, function (item) { - //returns any callback that have been registered directly against the field - return (item.propertyAlias === null && item.culture === "invariant" && item.segment === null && item.fieldName === fieldName); - }); - return found; - }, + getPropertyCallbacks: getPropertyCallbacks, + getFieldCallbacks: getFieldCallbacks, /** * @ngdoc function @@ -350,121 +648,9 @@ function serverValidationManager($timeout) { return found; }, - /** - * @ngdoc function - * @name getVariantCallbacks - * @methodOf umbraco.services.serverValidationManager - * @function - * - * @description - * Gets all callbacks that has been registered using the subscribe method for the culture and segment. - */ - getVariantCallbacks: function (culture, segment) { - var found = _.filter(callbacks, function (item) { - //returns any callback that have been registered directly against the given culture and given segment. - return (item.culture === culture && item.segment === segment && item.propertyAlias === null && item.fieldName === null); - }); - return found; - }, - - /** - * @ngdoc function - * @name addFieldError - * @methodOf umbraco.services.serverValidationManager - * @function - * - * @description - * Adds an error message for a native content item field (not a user defined property, for Example, 'Name') - */ - addFieldError: function(fieldName, errorMsg) { - if (!fieldName) { - return; - } - - //only add the item if it doesn't exist - if (!this.hasFieldError(fieldName)) { - this.items.push({ - propertyAlias: null, - culture: "invariant", - segment: null, - fieldName: fieldName, - errorMsg: errorMsg - }); - } - - //find all errors for this item - 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) { - executeCallback(this, errorsForCallback, cbs[cb].callback, null, null); - } - }, - - /** - * @ngdoc function - * @name addPropertyError - * @methodOf umbraco.services.serverValidationManager - * @function - * - * @description - * Adds an error message for the content property - */ - addPropertyError: function (propertyAlias, culture, fieldName, errorMsg, segment) { - - // TODO: We need to handle the errorMsg in a special way to check if this is a json structure. If it is we know we are dealing with - // a complex editor and in which case we'll need to adjust how everything works. - - if (!propertyAlias) { - return; - } - - //normalize culture to "invariant" - if (!culture) { - culture = "invariant"; - } - //normalize segment to null - if (!segment) { - segment = null; - } - - // if the error message is json it's a complex editor validation response that we need to parse - if (errorMsg.startsWith("[")) { - - var idsToErrors = parseComplexEditorError(errorMsg); - - // TODO: Make this the generic "Property has errors" but need to find the lang key for that - errorMsg = "Hello!"; - } - - //only add the item if it doesn't exist - if (!this.hasPropertyError(propertyAlias, culture, fieldName, segment)) { - this.items.push({ - propertyAlias: propertyAlias, - culture: culture, - segment: segment, - fieldName: fieldName, - errorMsg: errorMsg - }); - } - - //find all errors for this item - var errorsForCallback = getPropertyErrors(this, propertyAlias, culture, segment, fieldName); - //we should now call all of the call backs registered for this error - var cbs = this.getPropertyCallbacks(propertyAlias, culture, fieldName, segment); - //call each callback for this error - for (var cb in cbs) { - executeCallback(this, errorsForCallback, cbs[cb].callback, culture, segment); - } - - //execute variant specific callbacks here too when a propery error is added - var variantCbs = this.getVariantCallbacks(culture, segment); - //call each callback for this error - for (var cb in variantCbs) { - executeCallback(this, errorsForCallback, variantCbs[cb].callback, culture, segment); - } - }, + getVariantCallbacks: getVariantCallbacks, + addFieldError: addFieldError, + addPropertyError: addPropertyError, /** * @ngdoc function @@ -491,45 +677,13 @@ function serverValidationManager($timeout) { } //remove the item - this.items = _.reject(this.items, function (item) { + items = _.reject(items, function (item) { return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); }, - /** - * @ngdoc function - * @name reset - * @methodOf umbraco.services.serverValidationManager - * @function - * - * @description - * Clears all errors and notifies all callbacks that all server errros are now valid - used when submitting a form - */ - reset: function () { - this.clear(); - for (var cb in callbacks) { - callbacks[cb].callback.apply(this, [ - true, //pass in a value indicating it is VALID - [], //pass in empty collection - [], - null, - null] - ); - } - }, - - /** - * @ngdoc function - * @name clear - * @methodOf umbraco.services.serverValidationManager - * @function - * - * @description - * Clears all errors - */ - clear: function() { - this.items = []; - }, + reset: reset, + clear: clear, /** * @ngdoc function @@ -551,7 +705,7 @@ function serverValidationManager($timeout) { segment = null; } - var err = _.find(this.items, function (item) { + var err = _.find(items, function (item) { //return true if the property alias matches and if an empty field name is specified or the field name matches return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); @@ -568,56 +722,15 @@ function serverValidationManager($timeout) { * Gets the error message for a content field */ getFieldError: function (fieldName) { - var err = _.find(this.items, function (item) { + var err = _.find(items, function (item) { //return true if the property alias matches and if an empty field name is specified or the field name matches return (item.propertyAlias === null && item.culture === "invariant" && item.segment === null && item.fieldName === fieldName); }); return err; }, - /** - * @ngdoc function - * @name hasPropertyError - * @methodOf umbraco.services.serverValidationManager - * @function - * - * @description - * Checks if the content property + culture + field name combo has an error - */ - hasPropertyError: function (propertyAlias, culture, fieldName, segment) { - - //normalize culture to null - if (!culture) { - culture = "invariant"; - } - //normalize segment to null - if (!segment) { - segment = null; - } - - var err = _.find(this.items, function (item) { - //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); - }); - return err ? true : false; - }, - - /** - * @ngdoc function - * @name hasFieldError - * @methodOf umbraco.services.serverValidationManager - * @function - * - * @description - * Checks if a content field has an error - */ - hasFieldError: function (fieldName) { - var err = _.find(this.items, function (item) { - //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === null && item.culture === "invariant" && item.segment === null && item.fieldName === fieldName); - }); - return err ? true : false; - }, + hasPropertyError: hasPropertyError, + hasFieldError: hasFieldError, /** * @ngdoc function @@ -635,7 +748,7 @@ function serverValidationManager($timeout) { culture = "invariant"; } - var err = _.find(this.items, function (item) { + var err = _.find(items, function (item) { return (item.culture === culture && item.segment === null); }); return err ? true : false; @@ -661,14 +774,25 @@ function serverValidationManager($timeout) { segment = null; } - var err = _.find(this.items, function (item) { + var err = _.find(items, function (item) { return (item.culture === culture && item.segment === segment); }); return err ? true : false; - }, - /** The array of error messages */ - items: [] + } + }; + + // Used to return the 'items' array as a reference/getter + Object.defineProperty(instance, "items", { + get: function () { + return items; + }, + set: function (value) { + throw "Cannot set the items array"; + } + }); + + return instance; } angular.module('umbraco.services').factory('serverValidationManager', serverValidationManager); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js b/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js index 8c178533bd..980d5ddc72 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js @@ -16,13 +16,17 @@ * @function * * @description - * Generates a Udi string. + * Generates a Udi string with a new ID * * @param {string} entityType The entityType as a string. * @returns {string} The generated UDI */ create: function(entityType) { - return "umb://" + entityType + "/" + (String.CreateGuid().replace(/-/g, "")); + return this.create(entityType, String.CreateGuid()); + }, + + build: function (entityType, guid) { + return "umb://" + entityType + "/" + (guid.replace(/-/g, "")); } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html index a6085cd8c3..968dc63265 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html @@ -3,7 +3,7 @@ diff --git a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs index 5aad81ab84..d2d144272c 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs @@ -54,7 +54,7 @@ namespace Umbraco.Web.PropertyEditors { _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; - _blockEditorValues = new BlockEditorValues(new BlocListEditorDataConverter(), contentTypeService); + _blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService); Validators.Add(new BlockEditorValidator(_blockEditorValues, propertyEditors, dataTypeService, textService)); } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index f75a400993..a72ed4385b 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -61,7 +61,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var value = (string)inter; if (string.IsNullOrWhiteSpace(value)) return model; - var converter = new BlocListEditorDataConverter(); + var converter = new BlockListEditorDataConverter(); var converted = converter.Convert(value); if (converted.Blocks.Count == 0) return model;