Gets unique keys working per nested property

This commit is contained in:
Shannon
2020-06-30 10:49:31 +10:00
parent 8eb467affa
commit 459904ffdd
13 changed files with 117 additions and 149 deletions
@@ -1,59 +0,0 @@
(function () {
"use strict";
/**
* @ngdoc component
* @name umbraco.umbNestedProperty
* @function
*
* @description
* Used to have nested property editors within complex editors in order to generate jsonpath for them to be used in validation
*/
angular
.module("umbraco")
.component("umbNestedProperty", {
transclude: true,
template: '<div><pre>{{vm.getValidationPath()}}</pre><div ng-transclude></div></div>',
controller: NestedPropertyController,
controllerAs: 'vm',
bindings: {
propertyTypeAlias: "@",
elementTypeIndex: "<",
findInScopeChain: "<" // TODO: Use/enable this
},
require: {
umbNestedProperty: "?^^"
}
});
function NestedPropertyController($scope, angularHelper) {
var vm = this;
vm.$onInit = function () {
if (!vm.propertyTypeAlias) {
throw "no propertyTypeAlias specified for umbNestedProperty";
}
};
vm.$postLink = function () {
// if directive inheritance (DOM) doesn't find one, then check scope inheritance
if (!vm.umbNestedProperty/* && findInScopeChain*/) {
var found = angularHelper.traverseScopeChain($scope, s => s.vm && s.vm.constructor.name == "NestedPropertyController");
if (found) {
vm.umbNestedProperty = found.vm;
}
}
}
// 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() : "$";
path += ".[nestedValidation].[" + vm.elementTypeIndex + "].[" + vm.propertyTypeAlias + "]";
return path;
}
}
})();
@@ -7,7 +7,10 @@ angular.module("umbraco.directives")
.directive('umbProperty', function (userService) {
return {
scope: {
property: "=",
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: "<"
},
@@ -43,7 +46,13 @@ angular.module("umbraco.directives")
$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;
return $scope.elementUdi ? ($scope.elementUdi + "/" + propAlias) : propAlias;
}
$scope.getValidationPath = self.getValidationPath;
}
};
@@ -64,8 +64,7 @@
templateUrl: Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "/views/propertyeditors/nestedcontent/nestedcontent.editor.html",
scope: {
ngModel: '=',
tabAlias: '=',
itemIndex: '='
tabAlias: '='
},
link: link
};
@@ -110,21 +110,8 @@ 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
// 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.
// 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;
@@ -98,48 +98,44 @@ function serverValidationManager($timeout) {
}
}
/**
* Returns a dictionary of id (of the block) and it's corresponding validation ModelState
* @param {any} errorMsg
*/
function parseComplexEditorError(errorMsg) {
var json = JSON.parse(errorMsg);
var nestedValidation = json["nestedValidation"];
if (!nestedValidation) {
throw "Invalid JSON structure for complex property, missing 'nestedValidation'";
var result = {};
function extractModelState(validation) {
if (validation.$id && validation.ModelState) {
result[validation.$id] = validation.ModelState;
}
}
nestedValidation.forEach((item, index) => {
var elementType = Object.keys(item)[0];
var key = "nestedValidation.[" + index + "].";
});
for (var i = 0; i < nestedValidation.length; i++) {
function iterateErrorBlocks(blocks) {
for (var i = 0; i < blocks.length; i++) {
var validation = blocks[i];
extractModelState(validation);
var nested = _.omit(validation, "$id", "$elementTypeAlias", "ModelState");
for (const [key, value] of Object.entries(nested)) {
if (Array.isArray(value)) {
iterateErrorBlocks(value); // recurse
}
}
}
}
// each key represents an element type, the key is it's alias
var keys = Object.keys(nestedValidation);
var asdf = keys;
// 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 :)
iterateErrorBlocks(json);
return result;
}
return {
parseComplexEditorError: parseComplexEditorError,
/**
* @ngdoc function
* @name notifyAndClearAllSubscriptions
@@ -434,12 +430,11 @@ function serverValidationManager($timeout) {
}
// if the error message is json it's a complex editor validation response that we need to parse
if (errorMsg.startsWith("{")) {
if (errorMsg.startsWith("[")) {
var parsed = parseComplexEditorError(errorMsg);
var idsToErrors = parseComplexEditorError(errorMsg);
// reset to nothing because we will not display the json message but we still want to inform the
// root properties of the error.
// TODO: Make this the generic "Property has errors" but need to find the lang key for that
errorMsg = "Hello!";
}
@@ -1 +1 @@
<umb-element-editor-content model="model.settings" item-index="model.index"></umb-element-editor-content>
<umb-element-editor-content model="model.settings"></umb-element-editor-content>
@@ -1,8 +1,8 @@
<div class="form-horizontal">
<ng-form name="elementTypeContentForm">
<div class="umb-group-panel"
data-element="group-{{group.alias}}"
ng-repeat="group in vm.model.variants[0].tabs track by group.label">
data-element="group-{{group.alias}}"
ng-repeat="group in vm.model.variants[0].tabs track by group.label">
<div class="umb-group-panel__header">
<div id="group-{{group.id}}">{{ group.label }}</div>
@@ -13,28 +13,23 @@
<umb-property data-element="property-{{property.alias}}"
ng-repeat="property in group.properties track by property.alias"
property="property"
element-udi="{{vm.model.udi}}"
show-inherit="vm.model.variants.length > 1 && !property.culture && !activeVariant.language.isDefault"
inherits-from="defaultVariant.language.name">
<umb-nested-property property-type-alias="{{property.alias}}"
element-type-index="vm.itemIndex">
<div ng-class="{'o-40 cursor-not-allowed': vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue}">
<umb-property-editor model="property"
preview="vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue">
</umb-property-editor>
</div>
</umb-nested-property>
<div ng-class="{'o-40 cursor-not-allowed': vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue}">
<umb-property-editor model="property"
preview="vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue">
</umb-property-editor>
</div>
</umb-property>
</div>
</div>
<umb-empty-state
ng-if="vm.model.tabs.length === 0"
position="center">
<umb-empty-state ng-if="vm.model.tabs.length === 0"
position="center">
<localize key="content_noProperties"></localize>
</umb-empty-state>
@@ -10,10 +10,7 @@
controller: ElementEditorContentComponentController,
controllerAs: 'vm',
bindings: {
model: '=',
// As this component is used for creating nested editors based on an element type, we need to know the index of this nested
// editor so that validation works. For example, if this is used in the block editor, this is the index of the block being rendered.
itemIndex: '<'
model: '='
}
});
@@ -2,6 +2,8 @@
<ng-form name="propertyForm">
<div class="control-group umb-control-group" ng-class="{hidelabel:property.hideLabel, 'umb-control-group__listview': property.alias === '_umb_containerView'}">
<pre>{{ getValidationPath() }}</pre>
<val-property-msg></val-property-msg>
<div class="umb-el-wrap">
@@ -5,7 +5,6 @@
<span>{{block.label}}</span>
</button>
<div class="blockelement-inlineblock-editor__inner" ng-class="{'--singleGroup':block.content.variants[0].tabs.length === 1}" ng-if="block.active === true">
<!-- NOTE: The 'index' is passed to this via it's scope which is set on the outer component: umbBlockListBlockContent -->
<umb-element-editor-content model="block.content" item-index="index"></umb-element-editor-content>
<umb-element-editor-content model="block.content"></umb-element-editor-content>
</div>
</div>
@@ -1,14 +1,13 @@
<div class="umb-pane">
<div ng-repeat="property in tab.properties" class="umb-nested-content-property-container">
<umb-property property="property"
<umb-property property="property"
property-alias="{{property.propertyAlias}}"
element-udi="{{'umb://element/' + model.key}}"
ng-class="{'umb-nested-content--not-supported': property.notSupported, 'umb-nested-content--mandatory': property.ncMandatory}"
data-element="property-{{property.alias}}">
<umb-nested-property property-type-alias="{{property.propertyAlias}}"
element-type-index="itemIndex">
<umb-property-editor model="property"></umb-property-editor>
</umb-nested-property>
<umb-property-editor model="property"></umb-property-editor>
</umb-property>
@@ -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" item-index="$index" tab-alias="ncTabAlias" />
<umb-nested-content-editor ng-model="node" tab-alias="ncTabAlias" />
</div>
</div>
@@ -316,19 +316,64 @@
describe('managing complex editor validation errors', function () {
it('can retrieve validation errors for the property', function () {
it('create json paths for complex validation error', function () {
//arrange
var complexValidationMsg = '{"nestedValidation":[{"textPage":{"title":[{"errorMessage":"WRONG!","memberNames":["innerFieldId"]}]}}]}';
serverValidationManager.addPropertyError("myProperty", null, null, complexValidationMsg, null);
var complexValidationMsg = `[
{
"$elementTypeAlias": "addressBook",
"$id": "34E3A26C-103D-4A05-AB9D-7E14032309C3",
"addresses":
[
{
"$elementTypeAlias": "addressInfo",
"$id": "FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1",
"ModelState":
{
"_Properties.city.invariant.null.country": [
"City is not in Australia"
],
"_Properties.city.invariant.null.capital": [
"Not a capital city"
]
}
},
{
"$elementTypeAlias": "addressInfo",
"$id": "7170A4DD-2441-4B1B-A8D3-437D75C4CBC9",
"ModelState":
{
"_Properties.city.invariant.null.country": [
"City is not in Australia"
],
"_Properties.city.invariant.null.capital": [
"Not a capital city"
]
}
}
],
"ModelState":
{
"_Properties.addresses.invariant.null.counter": [
"Must have at least 3 addresses"
],
"_Properties.bookName.invariant.null.book": [
"Invalid address book name"
]
}
}
]`;
//act
var err1 = serverValidationManager.getPropertyError("myProperty", null, null, null);
var ids = serverValidationManager.parseComplexEditorError(complexValidationMsg);
//assert
expect(err1).not.toBeUndefined();
expect(err1.propertyAlias).toEqual("myProperty");
expect(err1.errorMsg).toEqual(complexValidationMsg);
//assert
var keys = Object.keys(ids);
expect(keys.length).toEqual(3);
expect(keys[0]).toEqual("34E3A26C-103D-4A05-AB9D-7E14032309C3");
expect(keys[1]).toEqual("FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1");
expect(keys[2]).toEqual("7170A4DD-2441-4B1B-A8D3-437D75C4CBC9");
});