Gets the inherited property validation working from the server side.

This commit is contained in:
Shannon
2020-07-01 12:44:34 +10:00
parent 2fc6ed10fb
commit 7c6cf3710b
4 changed files with 112 additions and 50 deletions
@@ -130,43 +130,60 @@ namespace Umbraco.Tests.Web.Validation
addressBookElementTypeResult2.ValidationResults.Add(bookNamePropertyTypeResult2);
nestedLevel1.ValidationResults.Add(addressBookElementTypeResult2);
var serialized = JsonConvert.SerializeObject(nestedLevel1, Formatting.Indented, new ValidationResultConverter());
// books is the outer most validation result and doesn't have it's own direct ValidationResult errors
var outerError = new ComplexEditorValidationResult();
var id4 = Guid.NewGuid();
var addressBookCollectionElementTypeResult = new ComplexEditorElementTypeValidationResult("addressBookCollection", id4);
var booksPropertyTypeResult= new ComplexEditorPropertyTypeValidationResult("books");
booksPropertyTypeResult.AddValidationResult(nestedLevel1); // books is the outer most validation result
addressBookCollectionElementTypeResult.ValidationResults.Add(booksPropertyTypeResult);
outerError.ValidationResults.Add(addressBookCollectionElementTypeResult);
var serialized = JsonConvert.SerializeObject(outerError, Formatting.Indented, new ValidationResultConverter());
Console.WriteLine(serialized);
var jsonError = JsonConvert.DeserializeObject<JArray>(serialized);
Assert.IsNotNull(jsonError.SelectToken("$[0]"));
Assert.AreEqual(id2.ToString(), jsonError.SelectToken("$[0].$id").Value<string>());
Assert.AreEqual("addressBook", jsonError.SelectToken("$[0].$elementTypeAlias").Value<string>());
Assert.IsNotNull(jsonError.SelectToken("$[0].ModelState"));
var error1 = jsonError.SelectToken("$[0].ModelState['_Properties.addresses.invariant.null.counter']") as JArray;
Assert.AreEqual(id4.ToString(), jsonError.SelectToken("$[0].$id").Value<string>());
Assert.AreEqual("addressBookCollection", jsonError.SelectToken("$[0].$elementTypeAlias").Value<string>());
Assert.AreEqual(string.Empty, jsonError.SelectToken("$[0].ModelState['_Properties.books.invariant.null'][0]").Value<string>());
var error0 = jsonError.SelectToken("$[0].books") as JArray;
Assert.IsNotNull(error0);
Assert.AreEqual(id2.ToString(), error0.SelectToken("$[0].$id").Value<string>());
Assert.AreEqual("addressBook", error0.SelectToken("$[0].$elementTypeAlias").Value<string>());
Assert.IsNotNull(error0.SelectToken("$[0].ModelState"));
Assert.AreEqual(string.Empty, error0.SelectToken("$[0].ModelState['_Properties.addresses.invariant.null'][0]").Value<string>());
var error1 = error0.SelectToken("$[0].ModelState['_Properties.addresses.invariant.null.counter']") as JArray;
Assert.IsNotNull(error1);
Assert.AreEqual(1, error1.Count);
var error2 = jsonError.SelectToken("$[0].ModelState['_Properties.bookName.invariant.null.book']") as JArray;
var error2 = error0.SelectToken("$[0].ModelState['_Properties.bookName.invariant.null.book']") as JArray;
Assert.IsNotNull(error2);
Assert.AreEqual(1, error2.Count);
Assert.AreEqual(id3.ToString(), jsonError.SelectToken("$[1].$id").Value<string>());
Assert.AreEqual("addressBook", jsonError.SelectToken("$[1].$elementTypeAlias").Value<string>());
Assert.IsNotNull(jsonError.SelectToken("$[1].ModelState"));
var error6 = jsonError.SelectToken("$[1].ModelState['_Properties.addresses.invariant.null.counter']") as JArray;
Assert.AreEqual(id3.ToString(), error0.SelectToken("$[1].$id").Value<string>());
Assert.AreEqual("addressBook", error0.SelectToken("$[1].$elementTypeAlias").Value<string>());
Assert.IsNotNull(error0.SelectToken("$[1].ModelState"));
Assert.AreEqual(string.Empty, error0.SelectToken("$[1].ModelState['_Properties.addresses.invariant.null'][0]").Value<string>());
var error6 = error0.SelectToken("$[1].ModelState['_Properties.addresses.invariant.null.counter']") as JArray;
Assert.IsNotNull(error6);
Assert.AreEqual(1, error6.Count);
var error7 = jsonError.SelectToken("$[1].ModelState['_Properties.bookName.invariant.null']") as JArray;
var error7 = error0.SelectToken("$[1].ModelState['_Properties.bookName.invariant.null']") as JArray;
Assert.IsNotNull(error7);
Assert.AreEqual(1, error7.Count);
Assert.IsNotNull(jsonError.SelectToken("$[0].addresses"));
Assert.AreEqual(id1.ToString(), jsonError.SelectToken("$[0].addresses[0].$id").Value<string>());
Assert.AreEqual("addressInfo", jsonError.SelectToken("$[0].addresses[0].$elementTypeAlias").Value<string>());
Assert.IsNotNull(jsonError.SelectToken("$[0].addresses[0].ModelState"));
var error3 = jsonError.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null.country']") as JArray;
Assert.IsNotNull(error0.SelectToken("$[0].addresses"));
Assert.AreEqual(id1.ToString(), error0.SelectToken("$[0].addresses[0].$id").Value<string>());
Assert.AreEqual("addressInfo", error0.SelectToken("$[0].addresses[0].$elementTypeAlias").Value<string>());
Assert.IsNotNull(error0.SelectToken("$[0].addresses[0].ModelState"));
var error3 = error0.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null.country']") as JArray;
Assert.IsNotNull(error3);
Assert.AreEqual(1, error3.Count);
var error4 = jsonError.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null.capital']") as JArray;
var error4 = error0.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null.capital']") as JArray;
Assert.IsNotNull(error4);
Assert.AreEqual(1, error4.Count);
var error5 = jsonError.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null']") as JArray;
var error5 = error0.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null']") as JArray;
Assert.IsNotNull(error5);
Assert.AreEqual(2, error5.Count);
}
@@ -123,52 +123,25 @@ function serverValidationManager($timeout, udiService) {
* Returns a dictionary of id (of the block) and it's corresponding validation ModelState
* @param {any} errorMsg
*/
function parseComplexEditorError(errorMsg, culture, segment) {
//normalize culture to "invariant"
if (!culture) {
culture = "invariant";
}
//normalize segment to "null" (as a string, in this case it's used for creating a key)
if (!segment) {
segment = "null";
}
function parseComplexEditorError(errorMsg) {
var json = JSON.parse(errorMsg);
var result = {};
function extractModelState(validation) {
if (validation.$id && validation.ModelState && Object.keys(validation.ModelState).length > 0) {
if (validation.$id && validation.ModelState) {
result[validation.$id] = validation.ModelState;
}
else {
// we'll still add the id in the dictionary with an empty result, this indicates that this element
// has nested errors but no errors itself
result[validation.$id] = {};
}
return result[validation.$id];
}
function iterateErrorBlocks(blocks) {
for (var i = 0; i < blocks.length; i++) {
var validation = blocks[i];
var modelState = extractModelState(validation);
var hasModelState = Object.keys(modelState).length > 0;
extractModelState(validation);
var nested = _.omit(validation, "$id", "$elementTypeAlias", "ModelState");
for (const [key, value] of Object.entries(nested)) {
if (Array.isArray(value)) {
// The key here is the property type alias of the nested validation.
// If the extracted ModelState is empty it indicates that this element
// has nested errors but no errors itself. In that case we need to manually populate the
// ModelState properties.
if (!hasModelState) {
var propertyKey = "_Properties." + key + "." + culture + "." + segment;
modelState[propertyKey] = null;
}
iterateErrorBlocks(value); // recurse
}
}
@@ -309,7 +282,7 @@ function serverValidationManager($timeout, udiService) {
// 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, culture, segment);
var idsToErrors = parseComplexEditorError(errorMsg);
for (const [key, value] of Object.entries(idsToErrors)) {
const elementUdi = udiService.build("element", key);
addErrorsForModelState(value, elementUdi);
@@ -316,7 +316,7 @@
describe('managing complex editor validation errors', function () {
it('create json paths for complex validation error', function () {
it('create dictionary of id to ModelState', function () {
//arrange
var complexValidationMsg = `[
@@ -377,6 +377,71 @@
});
it('create dictionary of id to ModelState with inherited errors', function () {
// arrange
// this root element doesn't have it's own attached errors, instead it has model state just
// showing that it has errors within it's nested properties. that ModelState is automatically
// added on the server side.
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": [
""
]
}
}
]`;
//act
var ids = serverValidationManager.parseComplexEditorError(complexValidationMsg);
//assert
var keys = Object.keys(ids);
expect(keys.length).toEqual(3);
expect(keys[0]).toEqual("34E3A26C-103D-4A05-AB9D-7E14032309C3");
var item0ModelState = ids["34E3A26C-103D-4A05-AB9D-7E14032309C3"];
expect(Object.keys(item0ModelState).length).toEqual(1);
expect(item0ModelState["_Properties.addresses.invariant.null"].length).toEqual(1);
expect(item0ModelState["_Properties.addresses.invariant.null"][0]).toEqual("");
expect(keys[1]).toEqual("FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1");
expect(keys[2]).toEqual("7170A4DD-2441-4B1B-A8D3-437D75C4CBC9");
});
});
describe('validation error subscriptions', function() {
@@ -80,6 +80,13 @@ namespace Umbraco.Web.PropertyEditors.Validation
// recurse to get the validation result object
var obj = JToken.FromObject(complexResult, camelCaseSerializer);
joElementType.Add(propTypeResult.PropertyTypeAlias, obj);
// For any nested property error we add the model state as empty state for that nested property
// NOTE: Instead of the empty validation message we could put in the translated
// "errors/propertyHasErrors" message, however I think that leaves for less flexibility since it could/should be
// up to the front-end validator to show whatever message it wants (if any) for an error indicating a nested property error.
// Will leave blank.
modelState.AddPropertyValidationError(new ValidationResult(string.Empty), propTypeResult.PropertyTypeAlias);
}
}
else