From 59f9af6eb853b4b8894c573000c1769ced9dc460 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Apr 2018 01:38:35 +1000 Subject: [PATCH] Gets publish dialog mostly working with server side validation messages wired up to the dialog --- src/Umbraco.Core/Models/IContent.cs | 2 +- .../components/content/edit.controller.js | 23 +++--- .../overlays/umboverlay.directive.js | 25 +++--- .../services/contenteditinghelper.service.js | 13 +-- .../services/umbdataformatter.service.js | 22 +++-- .../overlays/publish/publish.controller.js | 52 ++---------- .../common/overlays/publish/publish.html | 34 ++++---- .../src/views/languages/edit.controller.js | 12 +-- .../umbraco/config/lang/en_us.xml | 4 +- src/Umbraco.Web/Editors/ContentController.cs | 81 ++++++++----------- .../Models/ContentEditing/ContentItemSave.cs | 10 ++- .../Models/ContentEditing/ContentVariation.cs | 14 ++-- .../ContentEditing/ContentVariationPublish.cs | 18 +++++ .../Models/ContentEditing/MoveOrCopy.cs | 1 - .../Models/Mapping/VariationResolver.cs | 3 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + .../WebApi/Binders/ContentItemBinder.cs | 9 ++- .../WebServices/BulkPublishController.cs | 2 +- 18 files changed, 157 insertions(+), 169 deletions(-) create mode 100644 src/Umbraco.Web/Models/ContentEditing/ContentVariationPublish.cs diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index d551421c00..ff270458bb 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -116,7 +116,7 @@ namespace Umbraco.Core.Models /// A value indicating whether the values could be published. /// /// The document must then be published via the content service. - /// Values are not published if they are not valie. + /// Values are not published if they are not valid. /// bool PublishValues(int? languageId = null, string segment = null); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 1e168973ad..4865ffa88a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -197,11 +197,10 @@ // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish function performSave(args) { - var deferred = $q.defer(); - + $scope.page.buttonGroupState = "busy"; - contentEditingHelper.contentEditorPerformSave({ + return contentEditingHelper.contentEditorPerformSave({ statusMessage: args.statusMessage, saveMethod: args.saveMethod, scope: $scope, @@ -214,7 +213,6 @@ $scope.page.buttonGroupState = "success"; - deferred.resolve(data); }, function (err) { //error if (err) { @@ -222,11 +220,8 @@ } $scope.page.buttonGroupState = "error"; - - deferred.reject(err); + return $q.reject(err); }); - - return deferred.promise; } function resetLastListPageNumber(content) { @@ -304,15 +299,19 @@ // return performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing...", action: "publish" }); var dialog = { - title: "Ready to Publish?", + title: "Ready to Publish?", //TODO: localize view: "publish", + variants: $scope.editors[0].content.variants, submitButtonLabel: "Publish", submit: function(model) { model.submitButtonState = "busy"; - console.log(model.selection); - // TODO: call bulk publishing method - performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing...", action: "publish" }).then(function(){ + + //we need to return this promise so that the dialog can handle the result and wire up the validation response + return performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing...", action: "publish" }).then(function(){ overlayService.close(); + }, function(err) { + model.submitButtonState = "error"; + return $q.reject(err); }); }, close: function(oldModel) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js index 98124887bc..25d324408d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js @@ -412,7 +412,7 @@ Opens an overlay to show a custom YSOD.
(function() { 'use strict'; - function OverlayDirective($timeout, formHelper, overlayHelper, localizationService) { + function OverlayDirective($timeout, formHelper, overlayHelper, localizationService, $q) { function link(scope, el, attr, ctrl) { @@ -638,14 +638,21 @@ Opens an overlay to show a custom YSOD.
scope.submitForm = function(model) { if(scope.model.submit) { if (formHelper.submitForm({scope: scope})) { - formHelper.resetForm({ scope: scope }); - - if(scope.model.confirmSubmit && scope.model.confirmSubmit.enable && !scope.directive.enableConfirmButton) { - scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton); - } else { - unregisterOverlay(); - scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton); - } + + if (scope.model.confirmSubmit && scope.model.confirmSubmit.enable && !scope.directive.enableConfirmButton) { + //wrap in a when since we don't know if this is a promise or not + $q.when(scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton)).then( + function() { + formHelper.resetForm({ scope: scope }); + }, angular.noop); + } else { + unregisterOverlay(); + //wrap in a when since we don't know if this is a promise or not + $q.when(scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton)).then( + function() { + formHelper.resetForm({ scope: scope }); + }, angular.noop); + } } } 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 4c5cb2abb1..0a586be540 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 @@ -54,13 +54,11 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica //we will use the default one for content if not specified var rebindCallback = args.rebindCallback === undefined ? self.reBindChangedProperties : args.rebindCallback; - var deferred = $q.defer(); - if (!args.scope.busy && formHelper.submitForm({ scope: args.scope, statusMessage: args.statusMessage, action: args.action })) { args.scope.busy = true; - args.saveMethod(args.content, $routeParams.create, fileManager.getFiles()) + return args.saveMethod(args.content, $routeParams.create, fileManager.getFiles()) .then(function (data) { formHelper.resetForm({ scope: args.scope, notifications: data.notifications }); @@ -74,7 +72,6 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica }); args.scope.busy = false; - deferred.resolve(data); }, function (err) { self.handleSaveError({ @@ -91,14 +88,13 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } } args.scope.busy = false; - deferred.reject(err); + return $q.reject(err); }); } else { - deferred.reject(); + return angularHelper.rejectedPromise(); } - - return deferred.promise; + }, /** Used by the content editor and media editor to add an info tab to the tabs array (normally known as the properties tab) */ @@ -508,7 +504,6 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * @description * A function to handle what happens when we have validation issues from the server side * - * TODO: Move to formHelper, so all this is in one place */ handleSaveError: function (args) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index a6b77ce65f..439720e461 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -324,15 +324,21 @@ //this is basically the same as for media but we need to explicitly add some extra properties var saveModel = this.formatMediaPostData(displayModel, action); - //get the selected variant - var currVariant = _.find(displayModel.variants, - function(v) { - return v.current === true; + //get the selected variant and build the additional published variants + saveModel.publishVariations = []; + _.each(displayModel.variants, + function (d) { + //set the selected variant if this is current + if (d.current === true) { + saveModel.languageId = d.language.id; + } + if (d.publish === true) { + saveModel.publishVariations.push({ + languageId: d.language.id, + segment: d.segment + }); + } }); - if (currVariant) { - saveModel.languageId = currVariant.language.id; - } - var propExpireDate = displayModel.removeDate; var propReleaseDate = displayModel.releaseDate; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.controller.js index 45a2856066..0c07bad316 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.controller.js @@ -4,54 +4,14 @@ function PublishController($scope, $timeout) { var vm = this; - vm.variants = []; - vm.loading = true; + vm.variants = $scope.model.variants; function onInit() { - - vm.loading = true; - - $timeout(function(){ - - vm.variants = [ - { - "cultureDisplayName": "English (United States)", - "culture": "en-US", - "state": "Published (pending changes)", - "selected": false, - "validationError": false, - "validationErrorMessage": "" - }, - { - "cultureDisplayName": "Spanish (Spain)", - "culture": "es-ES", - "state": "Draft", - "selected": false, - "validationError": false, - "validationErrorMessage": "" - }, - { - "cultureDisplayName": "French (France)", - "culture": "fr-FR", - "state": "Published (pending changes)", - "selected": false, - "validationError": true, - "validationErrorMessage": "Lorem ipsum dolor sit amet..." - }, - { - "cultureDisplayName": "German (Germany)", - "culture": "de-DE", - "state": "Draft", - "selected": false, - "validationError": false, - "validationErrorMessage": "" - } - ]; - - vm.loading = false; - - }, 1000); - + _.each(vm.variants, + function (v) { + v.compositeId = v.language.id + "_" + (v.segment ? v.segment : ""); + v.htmlId = "publish_variant_" + v.compositeId; + }); } onInit(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.html index 5f67c90270..70cfe83a53 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.html @@ -1,4 +1,4 @@ -
+

What languages would you like to publish?

@@ -10,20 +10,24 @@
- + + +
-
\ No newline at end of file +
diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index ce6df0d351..7ac9c340f5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -73,10 +73,15 @@ function save() { - if (formHelper.submitForm({ scope: $scope, statusMessage: "Saving..." })) { + if (formHelper.submitForm({ scope: $scope })) { vm.page.saveButtonState = "busy"; languageResource.save(vm.language).then(function (lang) { + + formHelper.resetForm({ + scope: $scope + }); + vm.language = lang; vm.page.saveButtonState = "success"; notificationsService.success(localizationService.localize("speechBubbles_languageSaved")); @@ -86,10 +91,7 @@ }, function (err) { vm.page.saveButtonState = "error"; - contentEditingHelper.handleSaveError({ - redirectOnFailure: false, - err: err - }); + formHelper.handleError(err); //show any notifications if (angular.isArray(err.data.notifications)) { 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 011a07012a..04b417124b 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1418,8 +1418,8 @@ To manage your website, simply open the Umbraco back office and start adding con An error occurred while unlocking the user Member was exported to file An error occurred while exporting the member - Validation failed - Publishing was interrupted because validation failed for required content language '%0%' + Validation failed for mandatory language '%0%' + Validation failed for language '%0%' Uses CSS syntax ex: h1, .redHeader, .blueTex diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 171ff9603b..1e6d0867d4 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -29,6 +29,7 @@ using Umbraco.Web.Models; using Umbraco.Web._Legacy.Actions; using Constants = Umbraco.Core.Constants; using ContentVariation = Umbraco.Core.Models.ContentVariation; +using Language = Umbraco.Web.Models.ContentEditing.Language; namespace Umbraco.Web.Editors { @@ -556,48 +557,7 @@ namespace Umbraco.Web.Editors } return contentItemDisplay; } - - /// - /// This will check content variants other than the current one for validation errors - /// - /// - /// - /// - /// - /// When publishing a content variant, we need to ensure that: - /// a) the current variant passes validation rules - /// b) the default variant and any mandatory variants pass validation rules - /// - private bool ValidateContentVariants(IContent content, ContentItemDto dto, ContentItemDisplay display) - { - var variants = display.Variants.ToList(); - if (variants.Count == 1) return true; // no need to validate others - - var validator = new ContentItemValidationHelper(); - - var currVariant = display.Variants.First(x => x.IsCurrent); - - var isValid = true; - - //for each variant, we'll need to re-map to a display object to validate it's properties - foreach (var contentVariation in variants.Where(x => x.Mandatory && x.Language.Id != currVariant.Language.Id)) - { - var mapped = MapToBasic(content, contentVariation.Language.Id); - var modelState = new ModelStateDictionary(); //new dictionary since we don't want validation results populating the current model state (at least for now) - var variantValid = validator.ValidatePropertyData(mapped, dto, modelState); - if (!variantValid) - { - display.AddErrorNotification( - Services.TextService.Localize("speechBubbles/contentVariantValidationErrorHeader"), - Services.TextService.Localize("speechBubbles/contentLanguageValidationErrorText", new[]{contentVariation.Language.Name})); - - isValid = false; - } - } - - return isValid; - } - + private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func saveMethod) { //If we've reached here it means: @@ -657,18 +617,41 @@ namespace Umbraco.Web.Editors else { //publish the item and check if it worked, if not we will show a diff msg below - contentItem.PersistedContent.PublishValues(contentItem.LanguageId); + contentItem.PersistedContent.PublishValues(contentItem.LanguageId); //we are not checking for a return value here because we've alraedy pre-validated the property values + + //check if we are publishing other variants and validate them + var allLangs = Services.LocalizationService.GetAllLanguages().ToList(); + var variantsToValidate = contentItem.PublishVariations.Where(x => x.LanguageId != contentItem.LanguageId).ToList(); + foreach (var publishVariation in variantsToValidate) + { + if (!contentItem.PersistedContent.PublishValues(publishVariation.LanguageId)) + { + var errMsg = Services.TextService.Localize("speechBubbles/contentLangValidationError", new[]{allLangs.First(x => x.Id == publishVariation.LanguageId).CultureName}); + ModelState.AddModelError("publish_variant_" + publishVariation.LanguageId + "_", errMsg); + } + } + + //validate any mandatory variants that are not in the list + var mandatoryLangs = Mapper.Map, IEnumerable>(allLangs) + .Where(x => variantsToValidate.All(v => v.LanguageId != x.Id)) //don't include variants above + .Where(x => x.Id != contentItem.LanguageId) //don't include the current variant + .Where(x => x.Mandatory); + foreach (var lang in mandatoryLangs) + { + if (contentItem.PersistedContent.Validate(lang.Id).Length > 0) + { + var errMsg = Services.TextService.Localize("speechBubbles/contentReqLangValidationError", new[]{allLangs.First(x => x.Id == lang.Id).CultureName}); + ModelState.AddModelError("publish_variant_" + lang.Id + "_", errMsg); + } + } + publishStatus = Services.ContentService.SaveAndPublish(contentItem.PersistedContent, Security.CurrentUser.Id); wasCancelled = publishStatus.Result == PublishResultType.FailedCancelledByEvent; } - //return the updated model + //get the updated model var display = MapToDisplay(contentItem.PersistedContent, contentItem.LanguageId); - - //validate the mandatory variants - if (!ValidateContentVariants(contentItem.PersistedContent, contentItem.ContentDto, display)) - throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); - + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 HandleInvalidModelState(display); diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs index d8ab8149dc..ca410100ec 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Runtime.Serialization; using Newtonsoft.Json; using Umbraco.Core.Models; @@ -15,7 +16,7 @@ namespace Umbraco.Web.Models.ContentEditing /// The language Id for the content variation being saved /// [DataMember(Name = "languageId")] - public int? LanguageId { get; set; } + public int? LanguageId { get; set; } //TODO: Change this to ContentVariationPublish, but this will all change anyways when we can edit all variants at once /// /// The template alias to save @@ -28,6 +29,11 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "expireDate")] public DateTime? ExpireDate { get; set; } - + + /// + /// Indicates that these variations should also be published + /// + [DataMember(Name = "publishVariations")] + public IEnumerable PublishVariations { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentVariation.cs b/src/Umbraco.Web/Models/ContentEditing/ContentVariation.cs index 9079395ca2..83fb3b03f3 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentVariation.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentVariation.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Core.Models; @@ -10,18 +11,19 @@ namespace Umbraco.Web.Models.ContentEditing [DataContract(Name = "contentVariant", Namespace = "")] public class ContentVariation { + [DataMember(Name = "language", IsRequired = true)] + [Required] + public Language Language { get; set; } + + [DataMember(Name = "segment")] + public string Segment { get; set; } + /// /// The content name of the variant /// [DataMember(Name = "name")] public string Name { get; set; } - [DataMember(Name = "language")] - public Language Language { get; set; } - - [DataMember(Name = "segment")] - public string Segment { get; set; } - [DataMember(Name = "state")] public string PublishedState { get; set; } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentVariationPublish.cs b/src/Umbraco.Web/Models/ContentEditing/ContentVariationPublish.cs new file mode 100644 index 0000000000..aefc487e7c --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/ContentVariationPublish.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Used to indicate which additional variants to publish when a content item is published + /// + public class ContentVariationPublish + { + [DataMember(Name = "languageId", IsRequired = true)] + [Required] + public int LanguageId { get; set; } + + [DataMember(Name = "segment")] + public string Segment { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/MoveOrCopy.cs b/src/Umbraco.Web/Models/ContentEditing/MoveOrCopy.cs index a387876a45..dcd4a4f037 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MoveOrCopy.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MoveOrCopy.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.Serialization; diff --git a/src/Umbraco.Web/Models/Mapping/VariationResolver.cs b/src/Umbraco.Web/Models/Mapping/VariationResolver.cs index dd229f47e3..1ced557b91 100644 --- a/src/Umbraco.Web/Models/Mapping/VariationResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/VariationResolver.cs @@ -32,7 +32,8 @@ namespace Umbraco.Web.Models.Mapping //fixme these all need to the variant values but we need to wait for the db/service changes Name = source.Name , Exists = source.HasVariation(x.Id), //TODO: This needs to be wired up with new APIs when they are ready - PublishedState = source.PublishedState.ToString() + PublishedState = source.PublishedState.ToString(), + //Segment = ?? We'll need to populate this one day when we support segments }).ToList(); var langId = context.GetLanguageId(); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 405cd8f577..14603da9eb 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -230,6 +230,7 @@ + diff --git a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs b/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs index 77eb7d7f8a..0a399c24c5 100644 --- a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs @@ -27,9 +27,14 @@ namespace Umbraco.Web.WebApi.Binders protected override ContentItemDto MapFromPersisted(ContentItemSave model) { - return ContextMapper.Map>(model.PersistedContent, new Dictionary + return MapFromPersisted(model.PersistedContent, model.LanguageId); + } + + internal static ContentItemDto MapFromPersisted(IContent content, int? languageId) + { + return ContextMapper.Map>(content, new Dictionary { - [ContextMapper.LanguageKey] = model.LanguageId + [ContextMapper.LanguageKey] = languageId }); } } diff --git a/src/Umbraco.Web/WebServices/BulkPublishController.cs b/src/Umbraco.Web/WebServices/BulkPublishController.cs index c2f888527e..daea5cc58f 100644 --- a/src/Umbraco.Web/WebServices/BulkPublishController.cs +++ b/src/Umbraco.Web/WebServices/BulkPublishController.cs @@ -29,7 +29,7 @@ namespace Umbraco.Web.WebServices if (publishDescendants == false) { - content.PublishValues(); // fixme variants? + content.PublishValues(); // fixme variants? validation - when this returns null? var result = Services.ContentService.SaveAndPublish(content, Security.CurrentUser.Id); return Json(new {