diff --git a/src/Umbraco.Core/PropertyEditors/RequiredValueValidator.cs b/src/Umbraco.Core/PropertyEditors/RequiredValueValidator.cs index 7990998d9e..e40ae002d6 100644 --- a/src/Umbraco.Core/PropertyEditors/RequiredValueValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/RequiredValueValidator.cs @@ -15,11 +15,11 @@ namespace Umbraco.Core.PropertyEditors if (value == null) { - yield return new ValidationResult("Value cannot be null"); + yield return new ValidationResult("Value cannot be null", new[] { "value" }); } if (value.IsNullOrWhiteSpace()) { - yield return new ValidationResult("Value cannot be empty"); + yield return new ValidationResult("Value cannot be empty", new[] { "value" }); } } } 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 bf480be7a3..db9e05dc22 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 @@ -38,14 +38,10 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser * @description * re-binds all changed property values to the origContent object from the newContent object and returns an array of changed properties. */ - reBindChangedProperties: function (origContent, newContent) { + reBindChangedProperties: function (allOrigProps, allNewProps) { var changed = []; - //get a list of properties since they are contained in tabs - var allOrigProps = this.getAllProps(origContent); - var allNewProps = this.getAllProps(newContent); - function getNewProp(alias) { if (alias.startsWith("_umb_")) { return null; @@ -78,10 +74,8 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser * It's worth noting that when a 403 occurs, the data is still saved just never published, though this depends on if the entity is a new * entity and whether or not the data fulfils the absolute basic requirements like having a mandatory Name. */ - handleValidationErrors: function (content, modelState) { - //get a list of properties since they are contained in tabs - var allProps = this.getAllProps(content); - + handleValidationErrors: function (allProps, modelState) { + //find the content property for the current error, for use in the loop below function findContentProp(props, propAlias) { return _.find(props, function (item) { @@ -132,21 +126,36 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser * @description * A function to handle what happens when we have validation issues from the server side */ - handleSaveError: function (err, scope) { + handleSaveError: function (args) { + + if (!args.err) { + throw "args.err cannot be null"; + } + if (!args.allNewProps && !angular.isArray(args.allNewProps)) { + throw "args.allNewProps must be a valid array"; + } + if (!args.allOrigProps && !angular.isArray(args.allOrigProps)) { + throw "args.allOrigProps must be a valid array"; + } + //When the status is a 403 status, we have validation errors. //Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something). //Or, some strange server error - if (err.status === 403) { + if (args.err.status === 403) { //now we need to look through all the validation errors - if (err.data && (err.data.modelState)) { + if (args.err.data && (args.err.data.ModelState)) { + + this.handleValidationErrors(args.allNewProps, args.err.data.ModelState); - this.handleValidationErrors(err.data, err.data.modelState); - - if (!this.redirectToCreatedContent(err.data.id, err.data.modelState)) { + if (!this.redirectToCreatedContent(args.err.data.id, args.err.data.ModelState)) { //we are not redirecting because this is not new content, it is existing content. In this case // we need to detect what properties have changed and re-bind them with the server data. Then we need // to re-bind any server validation errors after the digest takes place. - this.reBindChangedProperties(scope.content, err.data); + + if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { + args.rebindCallback(); + } + serverValidationManager.executeAndClearAllSubscriptions(); } @@ -154,11 +163,11 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser return true; } else { - dialogService.ysodDialog(err); + dialogService.ysodDialog(args.err); } } else { - dialogService.ysodDialog(err); + dialogService.ysodDialog(args.err); } return false; @@ -166,7 +175,7 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser /** * @ngdoc function - * @name umbraco.services.contentEditingHelper#handleSaveError + * @name umbraco.services.contentEditingHelper#handleSuccessfulSave * @methodOf umbraco.services.contentEditingHelper * @function * diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index a260ce904e..9b52da35ad 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -85,7 +85,7 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ * In both of the above, the successCallback must accept these parameters: data, status, headers, config * If using the errorCallback it must accept these parameters: data, status, headers, config * The success callback must return the data which will be resolved by the deferred object. - * The error callback must return an object containing: {errorMsg: errorMessage, data: originalData } + * The error callback must return an object containing: {errorMsg: errorMessage, data: originalData, status: status } */ resourcePromise: function (httpPromise, opts) { var deferred = $q.defer(); @@ -101,7 +101,8 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ return { //NOTE: the default error message here should never be used based on the above docs! errorMsg: (angular.isString(opts) ? opts : 'An error occurred!'), - data: data + data: data, + status: status }; } @@ -137,7 +138,8 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ //return an error object including the error message for UI deferred.reject({ errorMsg: result.errorMsg, - data: result.data + data: result.data, + status: result.status }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js index 8358dea7b8..7e290796dc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js @@ -61,11 +61,22 @@ function ContentEditController($scope, $routeParams, $location, contentResource, contentEditingHelper.handleSuccessfulSave({ scope: $scope, newContent: data, - rebindCallback: contentEditingHelper.reBindChangedProperties(scope.content, data) + rebindCallback: contentEditingHelper.reBindChangedProperties( + contentEditingHelper.getAllProps($scope.content), + contentEditingHelper.getAllProps(data)) }); - }, function (err) { - contentEditingHelper.handleSaveError(err, $scope); + }, function (err) { + + var allNewProps = contentEditingHelper.getAllProps(err.data); + var allOrigProps = contentEditingHelper.getAllProps($scope.content); + + contentEditingHelper.handleSaveError({ + err: err, + allNewProps: allNewProps, + allOrigProps: contentEditingHelper.getAllProps($scope.content), + rebindCallback: contentEditingHelper.reBindChangedProperties(allOrigProps, allNewProps) + }); }); }; @@ -89,7 +100,11 @@ function ContentEditController($scope, $routeParams, $location, contentResource, }); }, function (err) { - contentEditingHelper.handleSaveError(err, $scope); + contentEditingHelper.handleSaveError({ + err: err, + allNewProps: contentEditingHelper.getAllProps(err.data), + allOrigProps: contentEditingHelper.getAllProps($scope.content) + }); }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/datatype/datatype.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/datatype/datatype.edit.controller.js index 544a9133b8..72e52d835c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatype/datatype.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/datatype/datatype.edit.controller.js @@ -102,7 +102,17 @@ function DataTypeEditController($scope, $routeParams, $location, dataTypeResourc }); }, function (err) { - contentEditingHelper.handleSaveError(err, $scope); + + //NOTE: in the case of data type values we are setting the orig/new props + // to be the same thing since that only really matters for content/media. + contentEditingHelper.handleSaveError({ + err: err, + allNewProps: $scope.preValues, + allOrigProps: $scope.preValues, + rebindCallback: function () { + createPreValueProps(err.data.preValues); + } + }); }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js index aa16731fde..088d0ee4a5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js @@ -59,11 +59,22 @@ function mediaEditController($scope, $routeParams, mediaResource, notificationsS contentEditingHelper.handleSuccessfulSave({ scope: $scope, newContent: data, - rebindCallback: contentEditingHelper.reBindChangedProperties(scope.content, data) + rebindCallback: contentEditingHelper.reBindChangedProperties( + contentEditingHelper.getAllProps($scope.content), + contentEditingHelper.getAllProps(data)) }); }, function (err) { - contentEditingHelper.handleSaveError(err, $scope); + + var allNewProps = contentEditingHelper.getAllProps(err.data); + var allOrigProps = contentEditingHelper.getAllProps($scope.content); + + contentEditingHelper.handleSaveError({ + err: err, + allNewProps: allNewProps, + allOrigProps: contentEditingHelper.getAllProps($scope.content), + rebindCallback: contentEditingHelper.reBindChangedProperties(allOrigProps, allNewProps) + }); }); }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/requiredfield.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/requiredfield.html index 79b44ced00..5012926855 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/requiredfield.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/requiredfield.html @@ -1,8 +1,8 @@ 
+ val-server="value"/> Required - + {{propertyForm.requiredField.errorMsg}}
diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js index 1778cd34c2..3aa47a7eb5 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js @@ -22,10 +22,14 @@ describe('contentEditingHelper tests', function () { data: content, status: 403 }; - err.data.modelState = {}; + err.data.ModelState = {}; //act - var handled = contentEditingHelper.handleSaveError(err, {content: content}); + var handled = contentEditingHelper.handleSaveError({ + err: err, + allNewProps: contentEditingHelper.getAllProps(content), + allOrigProps: contentEditingHelper.getAllProps(content) + }); //assert expect(handled).toBe(true); @@ -39,7 +43,11 @@ describe('contentEditingHelper tests', function () { }; //act - var handled = contentEditingHelper.handleSaveError(err, null); + var handled = contentEditingHelper.handleSaveError({ + err: err, + allNewProps: [], + allOrigProps: [] + }); //assert expect(handled).toBe(false); @@ -55,7 +63,11 @@ describe('contentEditingHelper tests', function () { }; //act - var handled = contentEditingHelper.handleSaveError(err, { content: content }); + var handled = contentEditingHelper.handleSaveError({ + err: err, + allNewProps: contentEditingHelper.getAllProps(content), + allOrigProps: contentEditingHelper.getAllProps(content) + }); //assert expect(handled).toBe(false); @@ -69,9 +81,10 @@ describe('contentEditingHelper tests', function () { //arrange var content = mocksUtils.getMockContent(1234); + var allProps = contentEditingHelper.getAllProps(content); //act - contentEditingHelper.handleValidationErrors(content, { Name: ["Required"] }); + contentEditingHelper.handleValidationErrors(allProps, { Name: ["Required"] }); //assert expect(serverValidationManager.items.length).toBe(1); @@ -84,9 +97,10 @@ describe('contentEditingHelper tests', function () { //arrange var content = mocksUtils.getMockContent(1234); + var allProps = contentEditingHelper.getAllProps(content); //act - contentEditingHelper.handleValidationErrors(content, { "Property.bodyText": ["Required"] }); + contentEditingHelper.handleValidationErrors(allProps, { "Property.bodyText": ["Required"] }); //assert expect(serverValidationManager.items.length).toBe(1); @@ -99,9 +113,10 @@ describe('contentEditingHelper tests', function () { //arrange var content = mocksUtils.getMockContent(1234); + var allProps = contentEditingHelper.getAllProps(content); //act - contentEditingHelper.handleValidationErrors(content, { "Property.bodyText.value": ["Required"] }); + contentEditingHelper.handleValidationErrors(allProps, { "Property.bodyText.value": ["Required"] }); //assert expect(serverValidationManager.items.length).toBe(1); @@ -114,10 +129,11 @@ describe('contentEditingHelper tests', function () { //arrange var content = mocksUtils.getMockContent(1234); + var allProps = contentEditingHelper.getAllProps(content); //act contentEditingHelper.handleValidationErrors( - content, + allProps, { "Name": ["Required"], "UpdateDate": ["Invalid date"], @@ -202,7 +218,9 @@ describe('contentEditingHelper tests', function () { newContent.tabs[1].properties[2].value = "origValue4"; //act - var changed = contentEditingHelper.reBindChangedProperties(origContent, newContent); + var changed = contentEditingHelper.reBindChangedProperties( + contentEditingHelper.getAllProps(origContent), + contentEditingHelper.getAllProps(newContent)); //assert expect(changed.length).toBe(2); diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs index 676c694793..dfdd08757b 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs @@ -32,8 +32,10 @@ namespace Umbraco.Web.Models.ContentEditing /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the /// updated model. + /// + /// NOTE: The ProperCase is important because when we return ModeState normally it will always be proper case. /// - [DataMember(Name = "modelState")] + [DataMember(Name = "ModelState")] public IDictionary Errors { get; set; } } }