diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbnestedproperty.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbnestedproperty.component.js new file mode 100644 index 0000000000..239259b3fa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbnestedproperty.component.js @@ -0,0 +1,49 @@ +(function () { + "use strict"; + + + /** + * @ngdoc component + * @name Umbraco.umbBlockListBlockContent + * @function + * + * @description + * The component for a style-inheriting block of the block list property editor. + */ + angular + .module("umbraco") + .component("umbNestedProperty", { + transclude: true, + template: '
', + controller: NestedPropertyController, + controllerAs: 'vm', + bindings: { + propertyTypeAlias: "@", + elementTypeIndex: "@" + }, + require: { + umbNestedProperty: "?^^umbNestedProperty" + } + }); + + function NestedPropertyController($scope) { + var vm = this; + vm.$onInit = function () { + + }; + + // returns a jsonpath for where this property is located in a hierarchy + // this will call into all hierarchical parents + vm.getValidationPath = function () { + + var path = vm.umbNestedProperty ? vm.umbNestedProperty.getValidationPath() : "$"; + if (vm.propertyTypeAlias && vm.elementTypeIndex) { + path += ".[nestedValidation].[" + vm.elementTypeIndex + "].[" + vm.propertyTypeAlias + "]"; + return path; + } + return null; + } + } + + +})(); 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 ad62bcd3db..d83b066a89 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 @@ -15,7 +15,7 @@ angular.module("umbraco.directives") restrict: 'E', replace: true, templateUrl: 'views/components/property/umb-property.html', - link: function (scope) { + link: function (scope, element, attr, ctrls) { scope.controlLabelTitle = null; if(Umbraco.Sys.ServerVariables.isDebuggingEnabled) { @@ -43,6 +43,8 @@ angular.module("umbraco.directives") $scope.propertyActions = actions; }; + + } }; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js index 72dba3ca2f..8a19bcc400 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js @@ -63,7 +63,8 @@ templateUrl: Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "/views/propertyeditors/nestedcontent/nestedcontent.editor.html", scope: { ngModel: '=', - tabAlias: '=' + tabAlias: '=', + itemIndex: '@' }, link: link }; 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 3fa9220f7b..cea4cbfde6 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'], + require: ['ngModel', '?^^umbProperty', '?^^umbVariantContent', '?^^umbNestedProperty'], restrict: "A", scope: {}, link: function (scope, element, attr, ctrls) { @@ -21,6 +21,7 @@ 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; @@ -75,7 +76,10 @@ function valServer(serverValidationManager) { if (modelCtrl.$invalid) { 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); stopWatch(); } @@ -105,9 +109,35 @@ function valServer(serverValidationManager) { stopWatch(); } } - unsubscribe.push(serverValidationManager.subscribe(currentProperty.alias, + + // TODO: If this is a property/field within a complex editor which means it could be a nested/nested/nested property/field + // we need to figure out a way to get it's "Path" (or jsonpath) which can be represented by something like: + // $.[nestedValidation].[0].[prop1].[nestedValidation].[0].[prop2] + // Or ... if we have names instead of indexes (which is seems like we do) + // $.nestedValidation.[type1].[prop1].[nestedValidation].[type2].[prop2] + // This would mean: + // - the first row/item in a complex editor + // - within the property 'prop1' + // - the first row/item in a complex editor + // - within the property 'prop2' + // So how can we figure out this path? The only way is really by looking up our current hierarchy of items + // TODO: OK, so we thought we had it with umb-property being able to know the content type BUT this doesn't work + // because the validation results could have a many rows for the same content type, we need to have the index available + // so the firest example above works much better. + // ... OK ... looks like we have an index to work with, but we'll need to update the block editor to support this too. + + var propertyValidationPath = umbNestedPropertyCtrl ? umbNestedPropertyCtrl.getValidationPath() : null; + + unsubscribe.push(serverValidationManager.subscribe( + currentProperty.alias, currentCulture, - fieldName, + // 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, 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 90fbd76ec9..68e2e8a78e 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 @@ -162,6 +162,12 @@ function formHelper(angularHelper, serverValidationManager, notificationsService //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 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 718e44d66e..04b4d2ad9a 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 @@ -36,7 +36,6 @@ function serverValidationManager($timeout) { }); } - function getPropertyErrors(self, propertyAlias, culture, segment, fieldName) { if (!Utilities.isString(propertyAlias)) { throw "propertyAlias must be a string"; @@ -98,7 +97,30 @@ function serverValidationManager($timeout) { } } } - + + function parseComplexEditorError(errorMsg) { + var json = JSON.parse(errorMsg); + + var nestedValidation = json["nestedValidation"]; + if (!nestedValidation) { + throw "Invalid JSON structure for complex property, missing 'nestedValidation'"; + } + + // each key represents an element type, the key is it's alias + var keys = Object.keys(nestedValidation); + + // TODO: Could we use an individual instance of serverValidationManager for each element type? It could/should work the way + // it does today since it currently manages all callbacks for all simple properties on a content item based on a content type. + // Hrmmm... only thing is then how to dispose/cleanup of these instances? + + // TODO: ... actually, because we are registering a JSONPath into the 'fieldName' for when complex editors subscribe, perhaps + // the only thing we need to do is build up all of the different JSONPath's and their errors here based on this object and then + // execute callbacks for each? So I think we need to make a function recursively return all possible keys! ... we can even have tests + // for that :) + + + } + return { /** @@ -173,7 +195,14 @@ function serverValidationManager($timeout) { if (!segment) { 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, @@ -370,6 +399,10 @@ function serverValidationManager($timeout) { * 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; } @@ -383,31 +416,39 @@ function serverValidationManager($timeout) { segment = null; } - //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); + // if the error message is json it's a complex editor validation response that we need to parse + if (errorMsg.startsWith("{")) { + parseComplexEditorError(errorMsg); } + else { + + //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); + } - //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); } }, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 6a2c012e25..fd6620060e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -20,10 +20,13 @@