adding umbNestedProperty to build up a json path for a targeted nested property type.

This commit is contained in:
Shannon
2020-06-24 11:51:48 +10:00
parent 29aaabcc6e
commit b99c170f86
11 changed files with 188 additions and 47 deletions
@@ -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: '<div ng-transclude></div>',
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;
}
}
})();
@@ -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;
};
}
};
});
@@ -63,7 +63,8 @@
templateUrl: Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "/views/propertyeditors/nestedcontent/nestedcontent.editor.html",
scope: {
ngModel: '=',
tabAlias: '='
tabAlias: '=',
itemIndex: '@'
},
link: link
};
@@ -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)
);
@@ -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
@@ -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);
}
},
@@ -20,10 +20,13 @@
<div class="umb-block-list__block" ng-class="{'--open':block.isOpen}">
<umb-block-list-scoped-block-content ng-if="block.config.stylesheet" class="umb-block-list__block--content blockelement__draggable-element" view="{{block.view}}" stylesheet="/{{::block.config.stylesheet}}" api="vm.blockEditorApi" block="block" index="$index">
</umb-block-list-scoped-block-content>
<umb-block-list-block-content ng-if="!block.config.stylesheet" class="umb-block-list__block--content" view="{{block.view}}" api="vm.blockEditorApi" block="block" index="$index">
</umb-block-list-block-content>
<umb-nested-property property-type-alias="{{property.propertyAlias}}"
element-type-index="{{$index}}">
<umb-block-list-scoped-block-content ng-if="block.config.stylesheet" class="umb-block-list__block--content blockelement__draggable-element" view="{{block.view}}" stylesheet="/{{::block.config.stylesheet}}" api="vm.blockEditorApi" block="block" index="$index">
</umb-block-list-scoped-block-content>
<umb-block-list-block-content ng-if="!block.config.stylesheet" class="umb-block-list__block--content" view="{{block.view}}" api="vm.blockEditorApi" block="block" index="$index">
</umb-block-list-block-content>
</umb-nested-property>
<div class="umb-block-list__block--actions">
<button type="button" class="btn-reset umb-outline action --settings" localize="title" title="actions_editSettings" ng-click="vm.blockEditorApi.openSettingsForBlock(block);" ng-if="block.showSettings === true">
@@ -54,10 +54,6 @@
labels.content_createEmpty = data[1];
});
vm.$onInit = function() {
inlineEditing = vm.model.config.useInlineEditingAsDefault;
@@ -586,8 +586,14 @@
for (var p = 0; p < tab.properties.length; p++) {
var prop = tab.properties[p];
// store the original alias before we change below, see notes
prop.propertyAlias = prop.alias;
// NOTE: This is super ugly, the reason it is like this is because it controls the label/html id in the umb-property component at a higher level.
// not pretty :/ but we can't change this now since it would require a bunch of plumbing to be able to change the id's higher up.
prop.alias = model.alias + "___" + prop.alias;
// TODO: Do we need to deal with this separately?
// Force validation to occur server side as this is the
// only way we can have consistency between mandatory and
// regex validation messages. Not ideal, but it works.
@@ -1,13 +1,20 @@
<div class="umb-pane">
<div ng-repeat="property in tab.properties" class="umb-nested-content-property-container">
<div ng-repeat="property in tab.properties" class="umb-nested-content-property-container">
<umb-property property="property" ng-class="{'umb-nested-content--not-supported': property.notSupported, 'umb-nested-content--mandatory': property.ncMandatory}" data-element="property-{{property.alias}}">
<umb-property-editor model="property"></umb-property-editor>
</umb-property>
<umb-property property="property"
ng-class="{'umb-nested-content--not-supported': property.notSupported, 'umb-nested-content--mandatory': property.ncMandatory}"
data-element="property-{{property.alias}}">
<div ng-if="property.notSupported" class="umb-nested-content-overlay"></div>
<umb-nested-property property-type-alias="{{property.propertyAlias}}"
element-type-index="{{itemIndex}}">
<umb-property-editor model="property"></umb-property-editor>
</umb-nested-property>
<p ng-if="property.notSupported">{{property.notSupportedMessage}}</p>
</umb-property>
</div>
<div ng-if="property.notSupported" class="umb-nested-content-overlay"></div>
<p ng-if="property.notSupported">{{property.notSupportedMessage}}</p>
</div>
</div>
@@ -28,7 +28,7 @@
</div>
<div class="umb-nested-content__content" ng-if="vm.currentNode.key === node.key && !vm.sorting">
<umb-nested-content-editor ng-model="node" tab-alias="ncTabAlias" />
<umb-nested-content-editor ng-model="node" item-index="{{$index}}" tab-alias="ncTabAlias" />
</div>
</div>