diff --git a/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs b/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs index f385fbcba6..fbb2ce9c80 100644 --- a/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs +++ b/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs @@ -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(serialized); Assert.IsNotNull(jsonError.SelectToken("$[0]")); - Assert.AreEqual(id2.ToString(), jsonError.SelectToken("$[0].$id").Value()); - Assert.AreEqual("addressBook", jsonError.SelectToken("$[0].$elementTypeAlias").Value()); - 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()); + Assert.AreEqual("addressBookCollection", jsonError.SelectToken("$[0].$elementTypeAlias").Value()); + Assert.AreEqual(string.Empty, jsonError.SelectToken("$[0].ModelState['_Properties.books.invariant.null'][0]").Value()); + + var error0 = jsonError.SelectToken("$[0].books") as JArray; + Assert.IsNotNull(error0); + Assert.AreEqual(id2.ToString(), error0.SelectToken("$[0].$id").Value()); + Assert.AreEqual("addressBook", error0.SelectToken("$[0].$elementTypeAlias").Value()); + Assert.IsNotNull(error0.SelectToken("$[0].ModelState")); + Assert.AreEqual(string.Empty, error0.SelectToken("$[0].ModelState['_Properties.addresses.invariant.null'][0]").Value()); + 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()); - Assert.AreEqual("addressBook", jsonError.SelectToken("$[1].$elementTypeAlias").Value()); - 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()); + Assert.AreEqual("addressBook", error0.SelectToken("$[1].$elementTypeAlias").Value()); + Assert.IsNotNull(error0.SelectToken("$[1].ModelState")); + Assert.AreEqual(string.Empty, error0.SelectToken("$[1].ModelState['_Properties.addresses.invariant.null'][0]").Value()); + 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()); - Assert.AreEqual("addressInfo", jsonError.SelectToken("$[0].addresses[0].$elementTypeAlias").Value()); - 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()); + Assert.AreEqual("addressInfo", error0.SelectToken("$[0].addresses[0].$elementTypeAlias").Value()); + 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); } 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 02bccc8dd2..5e65f49794 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 @@ -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); diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js index be30f2b92d..009043cae7 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js @@ -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() { diff --git a/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs b/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs index af915569c0..40fc5cc36c 100644 --- a/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs @@ -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