diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 8929922320..78a1ff4ccf 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1055,6 +1055,7 @@ namespace Umbraco.Core.Services uow.Commit(); //Special case for the Upload DataType + //TODO: Should we handle this with events? var uploadDataTypeId = new Guid(Constants.PropertyEditors.UploadField); if (content.Properties.Any(x => x.PropertyType.DataTypeId == uploadDataTypeId)) { diff --git a/src/Umbraco.Web.UI.Client/src/views/media/edit.html b/src/Umbraco.Web.UI.Client/src/views/media/edit.html index 8ff21f8015..dc4fe83b0c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/edit.html @@ -6,23 +6,12 @@
+ + + Save -
- Publish - - - - - -
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/mediaedit.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/mediaedit.controller.js index 3fa3120372..76f9eaa52e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/mediaedit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/mediaedit.controller.js @@ -1,4 +1,4 @@ -function mediaEditController($scope, $routeParams, mediaResource, notificationsService) { +function mediaEditController($scope, $routeParams, mediaResource, notificationsService, angularHelper, serverValidationManager, contentEditingHelper) { if ($routeParams.create) { @@ -13,6 +13,13 @@ function mediaEditController($scope, $routeParams, mediaResource, notificationsS .then(function (data) { $scope.contentLoaded = true; $scope.content = data; + + //in one particular special case, after we've created a new item we redirect back to the edit + // route but there might be server validation errors in the collection which we need to display + // after the redirect, so we will bind all subscriptions which will show the server validation errors + // if there are any and then clear them so the collection no longer persists them. + serverValidationManager.executeAndClearAllSubscriptions(); + }); } @@ -28,22 +35,27 @@ function mediaEditController($scope, $routeParams, mediaResource, notificationsS } }; - //TODO: Clean this up and share this code with the content editor - $scope.saveAndPublish = function (cnt) { - mediaResource.saveMedia(cnt, $routeParams.create, $scope.files) - .then(function (data) { - $scope.content = data; - notificationsService.success("Published", "Media has been saved and published"); - }); - }; + //ensure there is a form object assigned. + var currentForm = angularHelper.getRequiredCurrentForm($scope); + + $scope.save = function (cnt) { + + $scope.$broadcast("saving", { scope: $scope }); + + //don't continue if the form is invalid + if (currentForm.$invalid) return; + + serverValidationManager.reset(); - //TODO: Clean this up and share this code with the content editor - $scope.save = function (cnt) { mediaResource.saveMedia(cnt, $routeParams.create, $scope.files) .then(function (data) { - $scope.content = data; - notificationsService.success("Saved", "Media has been saved"); - }); + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + newContent: data + }); + }, function (err) { + contentEditingHelper.handleSaveError(err, $scope); + }); }; } diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 8923314534..b5eda0d3c8 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -777,7 +777,7 @@ To manage your website, simply open the umbraco back office and start adding con Sent For Approval Changes have been sent for approval Media saved - + Media saved without any errors Member saved Stylesheet Property Saved Stylesheet saved diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index e56017416b..0cdd9893bf 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -763,7 +763,7 @@ To manage your website, simply open the umbraco back office and start adding con Sent For Approval Changes have been sent for approval Media saved - + Media saved without any errors Member saved Stylesheet Property Saved Stylesheet saved diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index be8cff37e7..55b3edc9fc 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -22,11 +22,94 @@ using umbraco; namespace Umbraco.Web.Editors { + public abstract class ContentControllerBase : UmbracoAuthorizedJsonController + { + /// + /// Constructor + /// + protected ContentControllerBase() + : this(UmbracoContext.Current) + { + } + + /// + /// Constructor + /// + /// + protected ContentControllerBase(UmbracoContext umbracoContext) + : base(umbracoContext) + { + } + + protected void HandleContentNotFound(int id) + { + ModelState.AddModelError("id", string.Format("content with id: {0} was not found", id)); + var errorResponse = Request.CreateErrorResponse( + HttpStatusCode.NotFound, + ModelState); + throw new HttpResponseException(errorResponse); + } + + protected void UpdateName(ContentItemSave contentItem) + where TPersisted : IContentBase + { + //Don't update the name if it is empty + if (!contentItem.Name.IsNullOrWhiteSpace()) + { + contentItem.PersistedContent.Name = contentItem.Name; + } + } + + protected void MapPropertyValues(ContentItemSave contentItem) + where TPersisted : IContentBase + { + //Map the property values + foreach (var p in contentItem.ContentDto.Properties) + { + //get the dbo property + var dboProperty = contentItem.PersistedContent.Properties[p.Alias]; + + //create the property data to send to the property editor + var d = new Dictionary(); + //add the files if any + var files = contentItem.UploadedFiles.Where(x => x.PropertyId == p.Id).ToArray(); + if (files.Any()) + { + d.Add("files", files); + } + var data = new ContentPropertyData(p.Value, d); + + //get the deserialized value from the property editor + if (p.PropertyEditor == null) + { + LogHelper.Warn("No property editor found for property " + p.Alias); + } + else + { + dboProperty.Value = p.PropertyEditor.ValueEditor.DeserializeValue(data, dboProperty.Value); + } + } + } + + protected void HandleInvalidModelState(ContentItemDisplayBase display) + where TPersisted : IContentBase + where T : ContentPropertyBasic + { + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 + if (!ModelState.IsValid) + { + display.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Forbidden, display)); + } + } + + } + /// /// The API controller used for editing content /// [PluginController("UmbracoApi")] - public class ContentController : UmbracoAuthorizedJsonController + public class ContentController : ContentControllerBase { private readonly ContentModelMapper _contentModelMapper; @@ -66,11 +149,7 @@ namespace Umbraco.Web.Editors var foundContent = Services.ContentService.GetById(id); if (foundContent == null) { - ModelState.AddModelError("id", string.Format("content with id: {0} was not found", id)); - var errorResponse = Request.CreateErrorResponse( - HttpStatusCode.NotFound, - ModelState); - throw new HttpResponseException(errorResponse); + HandleContentNotFound(id); } return _contentModelMapper.ToContentItemDisplay(foundContent); } @@ -107,43 +186,14 @@ namespace Umbraco.Web.Editors // * and validated // * any file attachments have been saved to their temporary location for us to use // * we have a reference to the DTO object and the persisted object - - //Don't update the name if it is empty - if (!contentItem.Name.IsNullOrWhiteSpace()) - { - contentItem.PersistedContent.Name = contentItem.Name; - } + + UpdateName(contentItem); //TODO: We need to support 'send to publish' //TODO: We'll need to save the new template, publishat, etc... values here - //Map the property values - foreach (var p in contentItem.ContentDto.Properties) - { - //get the dbo property - var dboProperty = contentItem.PersistedContent.Properties[p.Alias]; - - //create the property data to send to the property editor - var d = new Dictionary(); - //add the files if any - var files = contentItem.UploadedFiles.Where(x => x.PropertyId == p.Id).ToArray(); - if (files.Any()) - { - d.Add("files", files); - } - var data = new ContentPropertyData(p.Value, d); - - //get the deserialized value from the property editor - if (p.PropertyEditor == null) - { - LogHelper.Warn("No property editor found for property " + p.Alias); - } - else - { - dboProperty.Value = p.PropertyEditor.ValueEditor.DeserializeValue(data, dboProperty.Value); - } - } + MapPropertyValues(contentItem); //We need to manually check the validation results here because: // * We still need to save the entity even if there are validation value errors @@ -191,12 +241,9 @@ namespace Umbraco.Web.Editors //return the updated model var display = _contentModelMapper.ToContentItemDisplay(contentItem.PersistedContent); + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 - if (!ModelState.IsValid) - { - display.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Forbidden, display)); - } + HandleInvalidModelState(display); //put the correct msgs in switch (contentItem.Action) diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 5e9dc789e7..7ea0df7f7e 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -4,6 +4,7 @@ using System.Net; using System.Net.Http; using System.Web.Http; using System.Web.Http.ModelBinding; +using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; @@ -14,6 +15,7 @@ using Umbraco.Web.WebApi; using System.Linq; using Umbraco.Web.WebApi.Binders; using Umbraco.Web.WebApi.Filters; +using umbraco; namespace Umbraco.Web.Editors { @@ -30,7 +32,7 @@ namespace Umbraco.Web.Editors //} [PluginController("UmbracoApi")] - public class MediaController : UmbracoAuthorizedJsonController + public class MediaController : ContentControllerBase { private readonly MediaModelMapper _mediaModelMapper; @@ -67,7 +69,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var emptyContent = new Umbraco.Core.Models.Media("Empty", parentId, contentType); + var emptyContent = new Core.Models.Media("Empty", parentId, contentType); return _mediaModelMapper.ToMediaItemDisplay(emptyContent); } @@ -81,11 +83,7 @@ namespace Umbraco.Web.Editors var foundContent = Services.MediaService.GetById(id); if (foundContent == null) { - ModelState.AddModelError("id", string.Format("media with id: {0} was not found", id)); - var errorResponse = Request.CreateErrorResponse( - HttpStatusCode.NotFound, - ModelState); - throw new HttpResponseException(errorResponse); + HandleContentNotFound(id); } return _mediaModelMapper.ToMediaItemDisplay(foundContent); } @@ -123,35 +121,26 @@ namespace Umbraco.Web.Editors // * any file attachments have been saved to their temporary location for us to use // * we have a reference to the DTO object and the persisted object - //Now, we just need to save the data + UpdateName(contentItem); - contentItem.PersistedContent.Name = contentItem.Name; - //TODO: We'll need to save the new template, publishat, etc... values here + MapPropertyValues(contentItem); - //Save the property values (for properties that have a valid editor ... not legacy) - foreach (var p in contentItem.ContentDto.Properties.Where(x => x.PropertyEditor != null)) + //We need to manually check the validation results here because: + // * We still need to save the entity even if there are validation value errors + // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) + // then we cannot continue saving, we can only display errors + // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display + // a message indicating this + if (!ModelState.IsValid) { - //get the dbo property - var dboProperty = contentItem.PersistedContent.Properties[p.Alias]; - - //create the property data to send to the property editor - var d = new Dictionary(); - //add the files if any - var files = contentItem.UploadedFiles.Where(x => x.PropertyId == p.Id).ToArray(); - if (files.Any()) + if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) + && (contentItem.Action == ContentSaveAction.SaveNew)) { - d.Add("files", files); - } - var data = new ContentPropertyData(p.Value, d); - - //get the deserialized value from the property editor - if (p.PropertyEditor == null) - { - LogHelper.Warn("No property editor found for property " + p.Alias); - } - else - { - dboProperty.Value = p.PropertyEditor.ValueEditor.DeserializeValue(data, dboProperty.Value); + //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! + // add the modelstate to the outgoing object and throw a 403 + var forDisplay = _mediaModelMapper.ToMediaItemDisplay(contentItem.PersistedContent); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Forbidden, forDisplay)); } } @@ -159,7 +148,21 @@ namespace Umbraco.Web.Editors Services.MediaService.Save(contentItem.PersistedContent); //return the updated model - return _mediaModelMapper.ToMediaItemDisplay(contentItem.PersistedContent); + var display = _mediaModelMapper.ToMediaItemDisplay(contentItem.PersistedContent); + + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 + HandleInvalidModelState(display); + + //put the correct msgs in + switch (contentItem.Action) + { + case ContentSaveAction.Save: + case ContentSaveAction.SaveNew: + display.AddSuccessNotification(ui.Text("speechBubbles", "editMediaSaved"), ui.Text("speechBubbles", "editMediaSavedText")); + break; + } + + return display; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs index 4e4f403760..e08540b242 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.Serialization; @@ -13,33 +12,11 @@ namespace Umbraco.Web.Models.ContentEditing /// A model representing a content item to be displayed in the back office /// [DataContract(Name = "content", Namespace = "")] - public class ContentItemDisplay : TabbedContentItem, INotificationModel + public class ContentItemDisplay : ContentItemDisplayBase { - public ContentItemDisplay() - { - Notifications = new List(); - } - [DataMember(Name = "publishDate")] public DateTime? PublishDate { get; set; } - - /// - /// This is used for validation of a content item. - /// - /// - /// 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. - /// - [DataMember(Name = "modelState")] - public IDictionary Errors { get; set; } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } - + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs new file mode 100644 index 0000000000..c696aeb675 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using Umbraco.Core.Models; + +namespace Umbraco.Web.Models.ContentEditing +{ + public abstract class ContentItemDisplayBase : TabbedContentItem, INotificationModel, IErrorModel + where T : ContentPropertyBasic + where TPersisted : IContentBase + { + protected ContentItemDisplayBase() + { + Notifications = new List(); + } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } + + /// + /// This is used for validation of a content item. + /// + /// + /// 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. + /// + [DataMember(Name = "modelState")] + public IDictionary Errors { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/IErrorModel.cs b/src/Umbraco.Web/Models/ContentEditing/IErrorModel.cs new file mode 100644 index 0000000000..b56ae528b4 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/IErrorModel.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Umbraco.Web.Models.ContentEditing +{ + public interface IErrorModel + { + /// + /// This is used for validation of a content item. + /// + /// + /// 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. + /// + IDictionary Errors { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs index 956194034b..d72fd84c33 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs @@ -8,7 +8,7 @@ namespace Umbraco.Web.Models.ContentEditing /// A model representing a content item to be displayed in the back office /// [DataContract(Name = "content", Namespace = "")] - public class MediaItemDisplay : TabbedContentItem + public class MediaItemDisplay : ContentItemDisplayBase { } diff --git a/src/Umbraco.Web/Models/ContentEditing/TabbedContentItem.cs b/src/Umbraco.Web/Models/ContentEditing/TabbedContentItem.cs index 6e1411f62e..b18118d8c2 100644 --- a/src/Umbraco.Web/Models/ContentEditing/TabbedContentItem.cs +++ b/src/Umbraco.Web/Models/ContentEditing/TabbedContentItem.cs @@ -8,7 +8,8 @@ using Umbraco.Core.Models; namespace Umbraco.Web.Models.ContentEditing { public abstract class TabbedContentItem : ContentItemBasic - where T : ContentPropertyBasic where TPersisted : IContentBase + where T : ContentPropertyBasic + where TPersisted : IContentBase { protected TabbedContentItem() { diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 40a697e1a9..6332d13afe 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -305,8 +305,10 @@ + +