From 0722adba2f973924b3e22b69a7fcb791e7560e4a Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 10 Jul 2020 08:44:43 +1000 Subject: [PATCH] WIP - with lots of notes --- .../lib/umbraco/Extensions.js | 12 ++ .../validation/valpropertymsg.directive.js | 88 +++++++---- .../validation/valserver.directive.js | 17 +-- .../common/services/angularhelper.service.js | 48 +++--- .../services/contenteditinghelper.service.js | 1 + .../src/common/services/formhelper.service.js | 9 +- .../services/servervalidationmgr.service.js | 127 +++++++++++----- .../blockeditor/blockeditor.controller.js | 3 +- .../blockeditor/blockeditor.html | 5 +- .../components/property/umb-property.html | 1 + .../server-validation-manager.spec.js | 142 ++++++++++-------- 11 files changed, 286 insertions(+), 167 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/lib/umbraco/Extensions.js b/src/Umbraco.Web.UI.Client/lib/umbraco/Extensions.js index 823d3d526d..54fda13a0d 100644 --- a/src/Umbraco.Web.UI.Client/lib/umbraco/Extensions.js +++ b/src/Umbraco.Web.UI.Client/lib/umbraco/Extensions.js @@ -69,6 +69,18 @@ }; } + if (!String.prototype.trimStartSpecial) { + /** trimSpecial extension method for string */ + // Removes all non printable chars from beginning of a string + String.prototype.trimStartSpecial = function () { + var index = 0; + while (this.charCodeAt(index) <= 46) { + index++; + } + return this.substr(index); + }; + } + if (!String.prototype.startsWith) { /** startsWith extension method for string */ String.prototype.startsWith = function (str) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index c586c88d38..353ff2a137 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -158,33 +158,33 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel if (hadError) { scope.$evalAsync(function () { - // TODO: This does not work :( :( :( - // We cannot clear a val-property-msg because another nested child might have server validation errors too. - // I 'think' we might be able to set the UI validation of this val-property-msg based on the child validators as well - // as the server validator so it can 'just' unset itself if all child validators are cleared. Can it be done? + //// TODO: This does not work :( :( :( + //// We cannot clear a val-property-msg because another nested child might have server validation errors too. + //// I 'think' we might be able to set the UI validation of this val-property-msg based on the child validators as well + //// as the server validator so it can 'just' unset itself if all child validators are cleared. Can it be done? - // Here we loop over the umbProperty hierarchy to see if we should clear the val-property-msg server validation key. - // we will clear the key if the parent for is valid, or if the parent form is only invalid due to a single val-property-msg error. - var currUmbProperty = umbPropCtrl; - var parentValidationKey = currUmbProperty.getParentValidationPath(); - while (currUmbProperty && parentValidationKey) { + //// Here we loop over the umbProperty hierarchy to see if we should clear the val-property-msg server validation key. + //// we will clear the key if the parent for is valid, or if the parent form is only invalid due to a single val-property-msg error. + //var currUmbProperty = umbPropCtrl; + //var parentValidationKey = currUmbProperty.getParentValidationPath(); + //while (currUmbProperty && parentValidationKey) { - if (!currUmbProperty.parentForm.$invalid || (_.keys(currUmbProperty.parentForm.$error).length === 1 && currUmbProperty.parentForm.$error.valPropertyMsg)) { - serverValidationManager.removePropertyError(parentValidationKey, currentCulture, "", currentSegment); + // if (!currUmbProperty.parentForm.$invalid || (_.keys(currUmbProperty.parentForm.$error).length === 1 && currUmbProperty.parentForm.$error.valPropertyMsg)) { + // serverValidationManager.removePropertyError(parentValidationKey, currentCulture, "", currentSegment); - // re-assign and loop - if (currUmbProperty !== umbPropCtrl.parentUmbProperty) { - currUmbProperty = umbPropCtrl.parentUmbProperty; - parentValidationKey = currUmbProperty ? currUmbProperty.getParentValidationPath() : null; - } - else { - break; - } - } - else { - break; - } - } + // // re-assign and loop + // if (currUmbProperty !== umbPropCtrl.parentUmbProperty) { + // currUmbProperty = umbPropCtrl.parentUmbProperty; + // parentValidationKey = currUmbProperty ? currUmbProperty.getParentValidationPath() : null; + // } + // else { + // break; + // } + // } + // else { + // break; + // } + //} //// we need to navigate the parentForm here, unfortunately there's no real alternative unless we create our own directive //// of some sort but that would also get messy. This works though since in this case we're always going to be in the property @@ -205,19 +205,43 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel // serverValidationManager.removePropertyError(parentValidationKey, currentCulture, "", currentSegment); // } //} - + }); - - - } + + + } } + //function checkFormValidation(f) { + + // if (!angularHelper.isForm(f)) { + // throw "The object is not an angular Form"; + // } + + // // if there's no value, the controls can be reset, which clears the error state on formCtrl + // for (let control of formCtrl.$getControls()) { + // control.$setValidity(); + // } + + //} + function checkValidationStatus() { if (formCtrl.$invalid) { //first we need to check if the valPropertyMsg validity is invalid if (formCtrl.$error.valPropertyMsg && formCtrl.$error.valPropertyMsg.length > 0) { //since we already have an error we'll just return since this means we've already set the // hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe + + // TODO: This does not work! we cannot just clear if there are no errors in child controls because child controls + // won't even have been loaded yet so this will just instantly clear them + //// At this stage we might have an error assigned because it was assigned based on validation hierarchy from the server, + //// BUT one or ALL of the child server (and client) validations may be cleared at this point. We will know if we have an + //// explicitly assigned error based on the error message assigned, if it is a non-explicit error (meaning that the error + //// was assigned because it has child errors) then the message will just be: labels.propertyHasErrors + //if (scope.errorMsg === labels.propertyHasErrors && _.every(formCtrl.$getControls(), c => c.$valid)) { + // resetError(); + //} + return; } //if there are any errors in the current property form that are not valPropertyMsg @@ -295,7 +319,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel // indicate that a content property is invalid at the property level since developers may not actually implement // the correct field validation in their property editors. - if (scope.currentProperty) { //this can be null if no property was assigned, TODO: I don't believe it can? If it was null we'd get errors above + if (scope.currentProperty) { //this can be null if no property was assigned function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { hasError = !isValid; @@ -311,13 +335,13 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel } } - unsubscribe.push(serverValidationManager.subscribe(propertyValidationKey, + unsubscribe.push(serverValidationManager.subscribe( + propertyValidationKey, currentCulture, "", serverValidationManagerCallback, currentSegment - ) - ); + )); } //when the scope is disposed we need to unsubscribe 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 f002d83360..2846a9d196 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 @@ -55,11 +55,8 @@ function valServer(serverValidationManager) { } } - function getPropertyValidationKey() { - // Get the property validation path if there is one, this is how wiring up any nested/virtual property validation works - var propertyValidationPath = umbPropCtrl ? umbPropCtrl.getValidationPath() : null; - return propertyValidationPath ? propertyValidationPath : currentProperty.alias; - } + // Get the property validation path if there is one, this is how wiring up any nested/virtual property validation works + var propertyValidationPath = umbPropCtrl ? umbPropCtrl.getValidationPath() : currentProperty.alias; // Need to watch the value model for it to change, previously we had subscribed to // modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that @@ -81,10 +78,10 @@ function valServer(serverValidationManager) { if (modelCtrl.$invalid) { modelCtrl.$setValidity('valServer', true); - console.log("valServer cleared (watch)"); + console.log("valServer cleared (watch) " + propertyValidationPath); //clear the server validation entry - serverValidationManager.removePropertyError(getPropertyValidationKey(), currentCulture, fieldName, currentSegment); + serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, fieldName, currentSegment); stopWatch(); } }, true); @@ -102,14 +99,14 @@ function valServer(serverValidationManager) { function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { if (!isValid) { modelCtrl.$setValidity('valServer', false); - console.log("valServer error"); + console.log("valServer error " + propertyValidationPath); //assign an error msg property to the current validator modelCtrl.errorMsg = propertyErrors[0].errorMsg; startWatch(); } else { modelCtrl.$setValidity('valServer', true); - console.log("valServer cleared"); + console.log("valServer cleared " + propertyValidationPath); //reset the error message modelCtrl.errorMsg = ""; stopWatch(); @@ -119,7 +116,7 @@ function valServer(serverValidationManager) { unsubscribe.push(serverValidationManager.subscribe( - getPropertyValidationKey(), + propertyValidationPath, currentCulture, fieldName, serverValidationManagerCallback, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js index 325c6255a5..12247f15b5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js @@ -7,6 +7,9 @@ * Some angular helper/extension methods */ function angularHelper($q) { + + var requiredFormProps = ["$error", "$name", "$dirty", "$pristine", "$valid", "$submitted", "$pending"]; + return { /** @@ -100,6 +103,28 @@ function angularHelper($q) { } }, + + isForm: function (obj) { + + // a method to check that the collection of object prop names contains the property name expected + function allPropertiesExist(objectPropNames) { + //ensure that every required property name exists on the current object + return _.every(requiredFormProps, function (item) { + return _.contains(objectPropNames, item); + }); + } + + //get the keys of the property names for the current object + var props = _.keys(obj); + //if the length isn't correct, try the next prop + if (props.length < requiredFormProps.length) { + return false; + } + + //ensure that every required property name exists on the current scope property + return allPropertiesExist(props); + }, + /** * @ngdoc function * @name getCurrentForm @@ -121,31 +146,10 @@ function angularHelper($q) { // is to inject the $element object and use: $element.inheritedData('$formController'); var form = null; - var requiredFormProps = ["$error", "$name", "$dirty", "$pristine", "$valid", "$submitted", "$pending"]; - - // a method to check that the collection of object prop names contains the property name expected - function propertyExists(objectPropNames) { - //ensure that every required property name exists on the current scope property - return _.every(requiredFormProps, function (item) { - - return _.contains(objectPropNames, item); - }); - } for (var p in scope) { - if (_.isObject(scope[p]) && p !== "this" && p.substr(0, 1) !== "$") { - //get the keys of the property names for the current property - var props = _.keys(scope[p]); - //if the length isn't correct, try the next prop - if (props.length < requiredFormProps.length) { - continue; - } - - //ensure that every required property name exists on the current scope property - var containProperty = propertyExists(props); - - if (containProperty) { + if (this.isForm(scope[p])) { form = scope[p]; break; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index bfcc0d536e..94e04acf6b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -614,6 +614,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt formHelper.handleServerValidation(args.err.data.ModelState); //add model state errors to notifications + // TODO: Need to ignore complex messages if (args.showNotifications) { for (var e in args.err.data.ModelState) { notificationsService.error("Validation", args.err.data.ModelState[e][0]); 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 adca9b30a5..49980a8c41 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 @@ -54,8 +54,13 @@ function formHelper(angularHelper, serverValidationManager, notificationsService } } - //reset the server validations - serverValidationManager.reset(); + //reset the server validations if required (default is true), otherwise notify existing ones of changes + if (!args.keepServerValidation) { + serverValidationManager.reset(); + } + else { + serverValidationManager.notify(); + } return true; }, 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 8999efda64..4cec5a00fe 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 @@ -10,9 +10,29 @@ */ function serverValidationManager($timeout, udiService) { + // TODO: It would be nicer to just SHA1 hash these 'keys' instead of having the keys be compound values + // It would be another lib dependency or we could embed this https://github.com/emn178/js-sha1 + // this would remove the need for the auto-generated 'id' values since the hash would be the actual id value. + + // The array of callback objects, each object 'key' is: + // - propertyAlias + // - culture + // - fieldName + // - segment + // The object also contains: + // - callback (function) + // - id (unique identifier, auto-generated, used internally for unsubscribing the callback) var callbacks = []; - // The array of error messages + // The array of error message objects, each object 'key' is: + // - propertyAlias + // - culture + // - fieldName + // - segment + // The object also contains: + // - errorMsg + // - id (unique identifier, auto-generated, used internally for mapping parent/child property validation messages) + // - parentId (used to map parent/child property validation messages) var items = []; /** calls the callback specified with the errors specified, used internally */ @@ -41,6 +61,13 @@ function serverValidationManager($timeout, udiService) { */ function notify() { $timeout(function () { + + console.log(`VAL-ERROR-COUNT: ${items.length}`); + for (var i = 0; i < items.length; i++) { + var item = items[i]; + console.log(`VAL-ERROR [${item.propertyAlias}] [${item.culture}] [${item.fieldName}] [${item.segment}]`) + } + notifyCallbacks(); }); } @@ -76,7 +103,14 @@ function serverValidationManager($timeout, udiService) { return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); } - + + function getPropertyErrorById(id) { + //find all errors for this id + return _.find(items, function (item) { + return (item.id === id); + }); + } + function getVariantErrors(culture, segment) { if (!culture) { @@ -118,35 +152,47 @@ function serverValidationManager($timeout, udiService) { } /** - * Returns a dictionary of id (of the block) and it's corresponding validation ModelState + * Flattens the complex errror result json into an array of the block's id/parent id and it's corresponding validation ModelState * @param {any} errorMsg + * @param {any} the id of the parentId (if any) */ - function parseComplexEditorError(errorMsg) { + function parseComplexEditorError(errorMsg, parentId) { - var json = JSON.parse(errorMsg); + var json = Utilities.isArray(errorMsg) ? errorMsg : JSON.parse(errorMsg); - var result = {}; + var result = []; - function extractModelState(validation) { + function extractModelState(validation, pid) { if (validation.$id && validation.ModelState) { - result[validation.$id] = validation.ModelState; + var ms = { + id: validation.$id, + elementUdi: udiService.build("element", validation.$id), + parentId: pid, + modelState: validation.ModelState + }; + result.push(ms); + return ms; } + return null; } - function iterateErrorBlocks(blocks) { + function iterateErrorBlocks(blocks, pid) { for (var i = 0; i < blocks.length; i++) { var validation = blocks[i]; - extractModelState(validation); + var ms = extractModelState(validation, pid); + if (!ms) { + continue; + } var nested = _.omit(validation, "$id", "$elementTypeAlias", "ModelState"); for (const [key, value] of Object.entries(nested)) { if (Array.isArray(value)) { - iterateErrorBlocks(value); // recurse + iterateErrorBlocks(value, ms.id); // recurse } } } } - iterateErrorBlocks(json); + iterateErrorBlocks(json, parentId); return result; } @@ -258,7 +304,7 @@ function serverValidationManager($timeout, udiService) { * @description * Adds an error message for the content property */ - function addPropertyError(propertyAlias, culture, fieldName, errorMsg, segment) { + function addPropertyError(propertyAlias, culture, fieldName, errorMsg, segment, parentId) { if (!propertyAlias) { return; @@ -277,14 +323,19 @@ function serverValidationManager($timeout, udiService) { errorMsg = ""; } - // if the error message is json it's a complex editor validation response that we need to parse - if (errorMsg.startsWith("[")) { + var id = String.CreateGuid(); - var idsToErrors = parseComplexEditorError(errorMsg); - for (const [key, value] of Object.entries(idsToErrors)) { - const elementUdi = udiService.build("element", key); - addErrorsForModelState(value, elementUdi); - } + // remove all non printable chars and whitespace from the string + if (Utilities.isString(errorMsg)) { + errorMsg = errorMsg.trimStartSpecial().trim(); + } + + // if the error message is json it's a complex editor validation response that we need to parse + if ((Utilities.isString(errorMsg) && errorMsg.startsWith("[")) || Utilities.isArray(errorMsg)) { + + var idsToErrors = parseComplexEditorError(errorMsg, id); + console.log("idsToErrors = " + JSON.stringify(idsToErrors)); + idsToErrors.forEach(x => addErrorsForModelState(x.modelState, x.elementUdi, x.parentId)); // We need to clear the error message else it will show up as a giant json block against the property errorMsg = ""; @@ -293,6 +344,8 @@ function serverValidationManager($timeout, udiService) { //only add the item if it doesn't exist if (!hasPropertyError(propertyAlias, culture, fieldName, segment)) { items.push({ + id: id, + parentId: parentId, propertyAlias: propertyAlias, culture: culture, segment: segment, @@ -368,10 +421,16 @@ function serverValidationManager($timeout, udiService) { * @methodOf umbraco.services.serverValidationManager * @param {any} modelState * @param {any} elementUdi optional parameter specifying a nested element's UDI for which this property belongs (for complex editors) + * @param {any} parentId optional parameter specifying the parentId validation item object (for complex editors) * @description * This wires up all of the server validation model state so that valServer and valServerField directives work */ - function addErrorsForModelState(modelState, elementUdi) { + function addErrorsForModelState(modelState, elementUdi, parentId) { + + if (!Utilities.isObject(modelState)) { + throw "modelState is not an object"; + } + for (const [key, value] of Object.entries(modelState)) { //This is where things get interesting.... @@ -437,7 +496,7 @@ function serverValidationManager($timeout, udiService) { } // add a generic error for the property - addPropertyError(propertyValidationKey, culture, htmlFieldReference, value && Array.isArray(value) && value.length > 0 ? value[0] : null, segment); + addPropertyError(propertyValidationKey, culture, htmlFieldReference, value && Array.isArray(value) && value.length > 0 ? value[0] : null, segment, parentId); } else { @@ -541,6 +600,8 @@ function serverValidationManager($timeout, udiService) { return; } + console.log(`serverValidationMgr subscribed [${propertyAlias}] [${culture}] [${fieldName}] [${segment}]`); + var id = String.CreateGuid(); //normalize culture to "invariant" @@ -700,23 +761,15 @@ function serverValidationManager($timeout, udiService) { * Gets the error message for the content property */ getPropertyError: function (propertyAlias, culture, fieldName, segment) { - - //normalize culture to "invariant" - if (!culture) { - culture = "invariant"; + var errors = getPropertyErrors(propertyAlias, culture, segment, fieldName); + if (errors.length > 0) { // should only ever contain one + return errors[0]; } - //normalize segment to null - if (!segment) { - segment = null; - } - - var err = _.find(items, function (item) { - //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); - }); - return err; + return undefined; }, - + + getPropertyErrorById: getPropertyErrorById, + /** * @ngdoc function * @name getFieldError diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js index da7ba52536..a62b632d38 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js @@ -56,7 +56,8 @@ angular.module("umbraco") vm.submitAndClose = function () { if (vm.model && vm.model.submit) { - if (formHelper.submitForm({ scope: $scope })) { + // always keep server validations since this will be a nested editor and server validations are global + if (formHelper.submitForm({ scope: $scope, formCtrl: vm.blockForm, keepServerValidation: true })) { vm.model.submit(vm.model); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html index ce157c95f5..01ce137259 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html @@ -1,8 +1,11 @@
- + + +
{{vm.blockForm.$valid}}
+ {{ vm.getValidationPath() }} +
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 009043cae7..c9e2faf089 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 @@ -4,7 +4,8 @@ beforeEach(module('umbraco.services')); beforeEach(inject(function ($injector) { - serverValidationManager = $injector.get('serverValidationManager'); + serverValidationManager = $injector.get('serverValidationManager'); + serverValidationManager.clear(); })); describe('managing field validation errors', function () { @@ -316,6 +317,51 @@ describe('managing complex editor validation errors', function () { + // 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 nonRootLevelComplexValidationMsg = `[ + { + "$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": [ + "" + ] + } + } +]`; + it('create dictionary of id to ModelState', function () { //arrange @@ -365,80 +411,52 @@ ]`; //act - var ids = serverValidationManager.parseComplexEditorError(complexValidationMsg); + var ms = serverValidationManager.parseComplexEditorError(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"); + expect(ms.length).toEqual(3); + expect(ms[0].id).toEqual("34E3A26C-103D-4A05-AB9D-7E14032309C3"); + expect(ms[1].id).toEqual("FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1"); + expect(ms[2].id).toEqual("7170A4DD-2441-4B1B-A8D3-437D75C4CBC9"); }); 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); + var ms = serverValidationManager.parseComplexEditorError(nonRootLevelComplexValidationMsg); //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(ms.length).toEqual(3); + expect(ms[0].id).toEqual("34E3A26C-103D-4A05-AB9D-7E14032309C3"); + var item0ModelState = ms[0].modelState; 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"); + expect(ms[1].id).toEqual("FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1"); + expect(ms[2].id).toEqual("7170A4DD-2441-4B1B-A8D3-437D75C4CBC9"); + + }); + + it('add errors for ModelState with inherited errors', function () { + + //act + let modelState = { + "_Properties.blockFeatures.invariant.null": [ + nonRootLevelComplexValidationMsg + ] + }; + serverValidationManager.addErrorsForModelState(modelState); + + //assert + console.log(JSON.stringify(serverValidationManager.items)); + let propError = serverValidationManager.getPropertyError("umb://element/FBEAEE8F4BC943EE8B81FCA8978850F1/city"); + expect(propError).toBeDefined(); + console.log(JSON.stringify(propError)); + expect(propError.parentId).toBeDefined(); + let parentError = serverValidationManager.getPropertyErrorById(propError.parentId); + expect(parentError).toBeDefined(); + expect(parentError.propertyAlias).toEqual("umb://element/34E3A26C103D4A05AB9D7E14032309C3/addresses"); });