Gets publish dialog mostly working with server side validation messages wired up to the dialog

This commit is contained in:
Shannon
2018-04-10 01:38:35 +10:00
parent e61089f927
commit 59f9af6eb8
18 changed files with 157 additions and 169 deletions

View File

@@ -116,7 +116,7 @@ namespace Umbraco.Core.Models
/// <returns>A value indicating whether the values could be published.</returns>
/// <remarks>
/// <para>The document must then be published via the content service.</para>
/// <para>Values are not published if they are not valie.</para>
/// <para>Values are not published if they are not valid.</para>
/// </remarks>
bool PublishValues(int? languageId = null, string segment = null);

View File

@@ -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) {

View File

@@ -412,7 +412,7 @@ Opens an overlay to show a custom YSOD. </br>
(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. </br>
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);
}
}
}

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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();

View File

@@ -1,4 +1,4 @@
<div ng-controller="Umbraco.Overlays.PublishController as vm">
<div ng-controller="Umbraco.Overlays.PublishController as vm" >
<div style="margin-bottom: 15px;">
<p>What languages would you like to publish?</p>
@@ -10,20 +10,24 @@
<div class="umb-list umb-list--condensed" ng-if="!vm.loading">
<div class="umb-list-item" ng-repeat="variant in vm.variants">
<label class="flex">
<input
type="checkbox"
checklist-model="model.selection"
checklist-value="variant.culture"
ng-disabled="variant.validationError"
style="margin-right: 8px;" />
<div>
<div ng-class="{'bold': variant.selected}" style="margin-bottom: 2px;">{{ variant.cultureDisplayName }}</div>
<div class="umb-permission__description" ng-if="!variant.validationError">{{ variant.state }}</div>
<div class="umb-permission__description" style="color: #F02E28;" ng-if="variant.validationError">{{ variant.validationErrorMessage }}</div>
</div>
</label>
<ng-form name="publishVariantSelectorForm">
<label class="flex" for="{{variant.htmlId}}">
<input
id="{{variant.htmlId}}"
name="publishVariantSelector"
type="checkbox"
ng-model="variant.publish"
ng-disabled="variant.validationError"
style="margin-right: 8px;"
val-server-field="{{variant.htmlId}}"/>
<div>
<div ng-class="{'bold': variant.published}" style="margin-bottom: 2px;">{{ variant.language.name }}</div>
<div class="umb-permission__description" ng-if="!variant.validationError">{{ variant.state }}</div>
<div class="umb-permission__description" style="color: #F02E28;" val-msg-for="publishVariantSelector" val-toggle-msg="valServerField"></div>
</div>
</label>
</ng-form>
</div>
</div>
</div>
</div>

View File

@@ -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)) {

View File

@@ -1418,8 +1418,8 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="unlockUserError">An error occurred while unlocking the user</key>
<key alias="memberExportedSuccess">Member was exported to file</key>
<key alias="memberExportedError">An error occurred while exporting the member</key>
<key alias="contentVariantValidationErrorHeader">Validation failed</key>
<key alias="contentLanguageValidationErrorText">Publishing was interrupted because validation failed for required content language '%0%'</key>
<key alias="contentReqLangValidationError">Validation failed for mandatory language '%0%'</key>
<key alias="contentLangValidationError">Validation failed for language '%0%'</key>
</area>
<area alias="stylesheet">
<key alias="aliasHelp">Uses CSS syntax ex: h1, .redHeader, .blueTex</key>

View File

@@ -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;
}
/// <summary>
/// This will check content variants other than the current one for validation errors
/// </summary>
/// <param name="content"></param>
/// <param name="dto"></param>
/// <param name="display"></param>
/// <remarks>
/// 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
/// </remarks>
private bool ValidateContentVariants(IContent content, ContentItemDto<IContent> dto, ContentItemDisplay display)
{
var variants = display.Variants.ToList();
if (variants.Count == 1) return true; // no need to validate others
var validator = new ContentItemValidationHelper<IContent, ContentItemSave>();
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<IContent, OperationResult> 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<ILanguage>, IEnumerable<Language>>(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);

View File

@@ -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
/// </summary>
[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
/// <summary>
/// The template alias to save
@@ -28,6 +29,11 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "expireDate")]
public DateTime? ExpireDate { get; set; }
/// <summary>
/// Indicates that these variations should also be published
/// </summary>
[DataMember(Name = "publishVariations")]
public IEnumerable<ContentVariationPublish> PublishVariations { get; set; }
}
}

View File

@@ -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; }
/// <summary>
/// The content name of the variant
/// </summary>
[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; }

View File

@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
namespace Umbraco.Web.Models.ContentEditing
{
/// <summary>
/// Used to indicate which additional variants to publish when a content item is published
/// </summary>
public class ContentVariationPublish
{
[DataMember(Name = "languageId", IsRequired = true)]
[Required]
public int LanguageId { get; set; }
[DataMember(Name = "segment")]
public string Segment { get; set; }
}
}

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.Serialization;

View File

@@ -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();

View File

@@ -230,6 +230,7 @@
<Compile Include="Media\ThumbnailProviders\ThumbnailProviderCollection.cs" />
<Compile Include="Media\ThumbnailProviders\ThumbnailProviderCollectionBuilder.cs" />
<Compile Include="Models\ContentEditing\ContentVariation.cs" />
<Compile Include="Models\ContentEditing\ContentVariationPublish.cs" />
<Compile Include="Models\ContentEditing\EditorNavigation.cs" />
<Compile Include="Models\ContentEditing\GetAvailableCompositionsFilter.cs" />
<Compile Include="Editors\IEditorValidator.cs" />

View File

@@ -27,9 +27,14 @@ namespace Umbraco.Web.WebApi.Binders
protected override ContentItemDto<IContent> MapFromPersisted(ContentItemSave model)
{
return ContextMapper.Map<IContent, ContentItemDto<IContent>>(model.PersistedContent, new Dictionary<string, object>
return MapFromPersisted(model.PersistedContent, model.LanguageId);
}
internal static ContentItemDto<IContent> MapFromPersisted(IContent content, int? languageId)
{
return ContextMapper.Map<IContent, ContentItemDto<IContent>>(content, new Dictionary<string, object>
{
[ContextMapper.LanguageKey] = model.LanguageId
[ContextMapper.LanguageKey] = languageId
});
}
}

View File

@@ -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
{