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 99cff8a309..264c8f140d 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
@@ -397,6 +397,7 @@
if (formHelper.submitForm({ scope: $scope, action: "save" })) {
var dialog = {
+ parentScope: $scope,
view: "views/content/overlays/save.html",
variants: $scope.content.variants, //set a model property for the dialog
skipFormValidation: true, //when submitting the overlay form, skip any client side validation
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 5ab335429d..9d98dce3af 100644
--- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml
+++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml
@@ -1396,12 +1396,15 @@ To manage your website, simply open the Umbraco back office and start adding con
Datatype saved
Dictionary item saved
Publishing failed because the parent page isn't published
+ %0% variant published
Content published
and visible on the website
+ %0% variant saved
Content saved
Remember to publish to make changes visible
Sent For Approval
Changes have been sent for approval
+ %0% variant changes have been sent for approval
Media saved
Media saved without any errors
Member saved
diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs
index df80879037..5041c411df 100644
--- a/src/Umbraco.Web/Editors/ContentController.cs
+++ b/src/Umbraco.Web/Editors/ContentController.cs
@@ -51,12 +51,14 @@ namespace Umbraco.Web.Editors
{
private readonly IPublishedSnapshotService _publishedSnapshotService;
private readonly PropertyEditorCollection _propertyEditors;
+ private readonly Lazy> _allLangs;
public ContentController(IPublishedSnapshotService publishedSnapshotService, PropertyEditorCollection propertyEditors)
{
if (publishedSnapshotService == null) throw new ArgumentNullException(nameof(publishedSnapshotService));
_publishedSnapshotService = publishedSnapshotService;
_propertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors));
+ _allLangs = new Lazy>(() => Services.LocalizationService.GetAllLanguages().ToDictionary(x => x.IsoCode, x => x, StringComparer.InvariantCultureIgnoreCase));
}
///
@@ -639,74 +641,91 @@ namespace Umbraco.Web.Editors
//initialize this to successful
var publishStatus = new PublishResult(null, contentItem.PersistedContent);
- var wasCancelled = false;
+ bool wasCancelled;
- if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew)
- {
- //save the item
- var saveResult = saveMethod(contentItem.PersistedContent);
+ //used to track successful notifications
+ var notifications = new SimpleNotificationModel();
- wasCancelled = saveResult.Success == false && saveResult.Result == OperationResultType.FailedCancelledByEvent;
- }
- else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew)
+ switch (contentItem.Action)
{
- var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id);
- wasCancelled = sendResult == false;
- }
- else
- {
- PublishInternal(contentItem, ref publishStatus, ref wasCancelled);
+ case ContentSaveAction.Save:
+ case ContentSaveAction.SaveNew:
+ var saveResult = saveMethod(contentItem.PersistedContent);
+ wasCancelled = saveResult.Success == false && saveResult.Result == OperationResultType.FailedCancelledByEvent;
+ if (saveResult.Success)
+ {
+ if (variantCount > 1)
+ {
+ var cultureErrors = ModelState.GetCulturesWithPropertyErrors();
+ foreach (var c in contentItem.Variants.Where(x => x.Save && !cultureErrors.Contains(x.Culture)).Select(x => x.Culture).ToArray())
+ {
+ notifications.AddSuccessNotification(
+ Services.TextService.Localize("speechBubbles/editVariantSavedHeader", new[] {_allLangs.Value[c].CultureName}),
+ Services.TextService.Localize("speechBubbles/editContentSavedText"));
+ }
+ }
+ else if (ModelState.IsValid)
+ {
+ notifications.AddSuccessNotification(
+ Services.TextService.Localize("speechBubbles/editContentSavedHeader"),
+ Services.TextService.Localize("speechBubbles/editContentSavedText"));
+ }
+ }
+ break;
+ case ContentSaveAction.SendPublish:
+ case ContentSaveAction.SendPublishNew:
+ var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id);
+ wasCancelled = sendResult == false;
+ if (sendResult)
+ {
+ if (variantCount > 1)
+ {
+ var cultureErrors = ModelState.GetCulturesWithPropertyErrors();
+ foreach (var c in contentItem.Variants.Where(x => x.Save && !cultureErrors.Contains(x.Culture)).Select(x => x.Culture).ToArray())
+ {
+ notifications.AddSuccessNotification(
+ Services.TextService.Localize("speechBubbles/editContentSendToPublish"),
+ Services.TextService.Localize("speechBubbles/editVariantSendToPublishText", new[] { _allLangs.Value[c].CultureName }));
+ }
+ }
+ else if (ModelState.IsValid)
+ {
+ notifications.AddSuccessNotification(
+ Services.TextService.Localize("speechBubbles/editContentSendToPublish"),
+ Services.TextService.Localize("speechBubbles/editContentSendToPublishText"));
+ }
+ }
+ break;
+ case ContentSaveAction.Publish:
+ case ContentSaveAction.PublishNew:
+ PublishInternal(contentItem, ref publishStatus, out wasCancelled, out var successfulCultures);
+ AddMessageForPublishStatus(publishStatus, notifications, successfulCultures);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
}
//get the updated model
var display = MapToDisplay(contentItem.PersistedContent);
+ //merge the tracked success messages with the outgoing model
+ display.Notifications.AddRange(notifications.Notifications);
+
//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)
+ if (wasCancelled)
{
- case ContentSaveAction.Save:
- case ContentSaveAction.SaveNew:
- if (wasCancelled == false)
- {
- display.AddSuccessNotification(
- Services.TextService.Localize("speechBubbles/editContentSavedHeader"),
- Services.TextService.Localize("speechBubbles/editContentSavedText"));
- }
- else
- {
- AddCancelMessage(display);
- }
- break;
- case ContentSaveAction.SendPublish:
- case ContentSaveAction.SendPublishNew:
- if (wasCancelled == false)
- {
- display.AddSuccessNotification(
- Services.TextService.Localize("speechBubbles/editContentSendToPublish"),
- Services.TextService.Localize("speechBubbles/editContentSendToPublishText"));
- }
- else
- {
- AddCancelMessage(display);
- }
- break;
- case ContentSaveAction.Publish:
- case ContentSaveAction.PublishNew:
- ShowMessageForPublishStatus(publishStatus, display);
- break;
+ AddCancelMessage(display);
+ if (IsCreatingAction(contentItem.Action))
+ {
+ //If the item is new and the operation was cancelled, we need to return a different
+ // status code so the UI can handle it since it won't be able to redirect since there
+ // is no Id to redirect to!
+ throw new HttpResponseException(Request.CreateValidationErrorResponse(display));
+ }
}
-
- //If the item is new and the operation was cancelled, we need to return a different
- // status code so the UI can handle it since it won't be able to redirect since there
- // is no Id to redirect to!
- if (wasCancelled && IsCreatingAction(contentItem.Action))
- {
- throw new HttpResponseException(Request.CreateValidationErrorResponse(display));
- }
-
+
display.PersistedContent = contentItem.PersistedContent;
return display;
@@ -718,10 +737,13 @@ namespace Umbraco.Web.Editors
///
///
///
+ ///
+ /// if the content is variant this will return an array of cultures that will be published (passed validation rules)
+ ///
///
/// If this is a culture variant than we need to do some validation, if it's not we'll publish as normal
///
- private void PublishInternal(ContentItemSave contentItem, ref PublishResult publishStatus, ref bool wasCancelled)
+ private void PublishInternal(ContentItemSave contentItem, ref PublishResult publishStatus, out bool wasCancelled, out string[] successfulCultures)
{
if (publishStatus == null) throw new ArgumentNullException(nameof(publishStatus));
@@ -730,17 +752,15 @@ namespace Umbraco.Web.Editors
//its invariant, proceed normally
publishStatus = Services.ContentService.SaveAndPublish(contentItem.PersistedContent, userId: Security.CurrentUser.Id);
wasCancelled = publishStatus.Result == PublishResultType.FailedCancelledByEvent;
+ successfulCultures = Array.Empty();
}
else
{
//All variants in this collection should have a culture if we get here! but we'll double check and filter here
var cultureVariants = contentItem.Variants.Where(x => !x.Culture.IsNullOrWhiteSpace()).ToList();
- //check if we are publishing other variants and validate them
- var allLangs = Services.LocalizationService.GetAllLanguages().ToDictionary(x => x.IsoCode, x => x, StringComparer.InvariantCultureIgnoreCase);
-
//validate if we can publish based on the mandatory language requirements
- var canPublish = ValidatePublishingMandatoryLanguages(contentItem, allLangs, cultureVariants);
+ var canPublish = ValidatePublishingMandatoryLanguages(contentItem, cultureVariants);
//Now check if there are validation errors on each variant.
//If validation errors are detected on a variant and it's state is set to 'publish', then we
@@ -756,7 +776,7 @@ namespace Umbraco.Web.Editors
if (canPublish)
{
//try to publish all the values on the model
- canPublish = PublishCulture(contentItem.PersistedContent, cultureVariants, allLangs);
+ canPublish = PublishCulture(contentItem.PersistedContent, cultureVariants);
}
if (canPublish)
@@ -764,6 +784,7 @@ namespace Umbraco.Web.Editors
//proceed to publish if all validation still succeeds
publishStatus = Services.ContentService.SavePublishing(contentItem.PersistedContent, Security.CurrentUser.Id);
wasCancelled = publishStatus.Result == PublishResultType.FailedCancelledByEvent;
+ successfulCultures = contentItem.Variants.Where(x => x.Publish).Select(x => x.Culture).ToArray();
}
else
{
@@ -771,6 +792,7 @@ namespace Umbraco.Web.Editors
var saveResult = Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id);
publishStatus = new PublishResult(PublishResultType.FailedCannotPublish, null, contentItem.PersistedContent);
wasCancelled = saveResult.Result == OperationResultType.FailedCancelledByEvent;
+ successfulCultures = Array.Empty();
}
}
}
@@ -779,15 +801,14 @@ namespace Umbraco.Web.Editors
/// Validate if publishing is possible based on the mandatory language requirements
///
///
- ///
///
///
- private bool ValidatePublishingMandatoryLanguages(ContentItemSave contentItem, IDictionary allLangs, IReadOnlyCollection cultureVariants)
+ private bool ValidatePublishingMandatoryLanguages(ContentItemSave contentItem, IReadOnlyCollection cultureVariants)
{
var canPublish = true;
//validate any mandatory variants that are not in the list
- var mandatoryLangs = Mapper.Map, IEnumerable>(allLangs.Values).Where(x => x.Mandatory);
+ var mandatoryLangs = Mapper.Map, IEnumerable>(_allLangs.Value.Values).Where(x => x.Mandatory);
foreach (var lang in mandatoryLangs)
{
@@ -800,7 +821,7 @@ namespace Umbraco.Web.Editors
if (isPublished || isPublishing) continue;
//cannot continue publishing since a required language that is not currently being published isn't published
- AddCultureValidationError(lang.IsoCode, allLangs, "speechBubbles/contentReqCulturePublishError");
+ AddCultureValidationError(lang.IsoCode, "speechBubbles/contentReqCulturePublishError");
canPublish = false;
}
@@ -812,9 +833,8 @@ namespace Umbraco.Web.Editors
///
///
///
- ///
///
- private bool PublishCulture(IContent persistentContent, IEnumerable cultureVariants, IDictionary allLangs)
+ private bool PublishCulture(IContent persistentContent, IEnumerable cultureVariants)
{
foreach(var variant in cultureVariants.Where(x => x.Publish))
{
@@ -822,7 +842,7 @@ namespace Umbraco.Web.Editors
var valid = persistentContent.PublishCulture(variant.Culture);
if (!valid)
{
- AddCultureValidationError(variant.Culture, allLangs, "speechBubbles/contentCultureValidationError");
+ AddCultureValidationError(variant.Culture, "speechBubbles/contentCultureValidationError");
return false;
}
}
@@ -834,16 +854,15 @@ namespace Umbraco.Web.Editors
/// Adds a generic culture error for use in displaying the culture validation error in the save/publish dialogs
///
///
- ///
///
- private void AddCultureValidationError(string culture, IDictionary allLangs, string localizationKey)
+ private void AddCultureValidationError(string culture, string localizationKey)
{
var key = "_content_variant_" + culture + "_";
if (ModelState.ContainsKey(key)) return;
- var errMsg = Services.TextService.Localize(localizationKey, new[] { allLangs[culture].CultureName });
+ var errMsg = Services.TextService.Localize(localizationKey, new[] { _allLangs.Value[culture].CultureName });
ModelState.AddModelError(key, errMsg);
}
-
+
///
/// Publishes a document with a given ID
///
@@ -868,7 +887,7 @@ namespace Umbraco.Web.Editors
if (publishResult.Success == false)
{
var notificationModel = new SimpleNotificationModel();
- ShowMessageForPublishStatus(publishResult, notificationModel);
+ AddMessageForPublishStatus(publishResult, notificationModel);
return Request.CreateValidationErrorResponse(notificationModel);
}
@@ -1225,11 +1244,9 @@ namespace Umbraco.Web.Editors
//Add any culture specific errors here
var cultureErrors = ModelState.GetCulturesWithPropertyErrors();
- var allLangs = Services.LocalizationService.GetAllLanguages().ToDictionary(x => x.IsoCode, x => x, StringComparer.InvariantCultureIgnoreCase);
-
foreach (var cultureError in cultureErrors)
{
- AddCultureValidationError(cultureError, allLangs, "speechBubbles/contentCultureValidationError");
+ AddCultureValidationError(cultureError, "speechBubbles/contentCultureValidationError");
}
}
@@ -1370,15 +1387,35 @@ namespace Umbraco.Web.Editors
return toMove;
}
- private void ShowMessageForPublishStatus(PublishResult status, INotificationModel display)
+ ///
+ /// Adds notification messages to the outbound display model for a given published status
+ ///
+ ///
+ ///
+ ///
+ /// This is null when dealing with invariant content, else it's the cultures that were succesfully published
+ ///
+ private void AddMessageForPublishStatus(PublishResult status, INotificationModel display, string[] successfulCultures = null)
{
switch (status.Result)
{
case PublishResultType.Success:
case PublishResultType.SuccessAlready:
- display.AddSuccessNotification(
+ if (successfulCultures == null)
+ {
+ display.AddSuccessNotification(
Services.TextService.Localize("speechBubbles/editContentPublishedHeader"),
Services.TextService.Localize("speechBubbles/editContentPublishedText"));
+ }
+ else
+ {
+ foreach (var c in successfulCultures)
+ {
+ display.AddSuccessNotification(
+ Services.TextService.Localize("speechBubbles/editVariantContentPublishedHeader", new[]{ _allLangs.Value[c].CultureName}),
+ Services.TextService.Localize("speechBubbles/editContentPublishedText"));
+ }
+ }
break;
case PublishResultType.FailedPathNotPublished:
display.AddWarningNotification(
@@ -1390,12 +1427,14 @@ namespace Umbraco.Web.Editors
AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent");
break;
case PublishResultType.FailedAwaitingRelease:
+ //TODO: We'll need to deal with variants here eventually
display.AddWarningNotification(
Services.TextService.Localize("publish"),
Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease",
new[] { $"{status.Content.Name} ({status.Content.Id})" }).Trim());
break;
case PublishResultType.FailedHasExpired:
+ //TODO: We'll need to deal with variants here eventually
display.AddWarningNotification(
Services.TextService.Localize("publish"),
Services.TextService.Localize("publish/contentPublishedFailedExpired",
@@ -1505,7 +1544,6 @@ namespace Umbraco.Web.Editors
/// Used to map an instance to a and ensuring a language is present if required
///
///
- ///
///
private ContentItemDisplay MapToDisplay(IContent content)
{