From 9227517a50031f116273582f73cf70167fb65d37 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 12 Feb 2025 12:30:27 +0100 Subject: [PATCH] Split force for publish descendants into separate options for publish unpublish and re-publish unedited (13) (#18249) * Split force for publish descendents into separate options for publish unpublish and re-publish unedited. * Added integration task verifying updated behaviour. * Variant integration test. * Update test data controller. * Remove usued function parameters. * Refactor to enum. * Fixed flags enum. * Variable name refactor. * Applied changes from code review. * Refactored method name. * Aligned js boolean checks. --- .../EmbeddedResources/Lang/en.xml | 1 + .../EmbeddedResources/Lang/en_us.xml | 1 + .../ContentEditing/ContentSaveAction.cs | 60 ++++-- .../Models/PublishBranchFilter.cs | 28 +++ src/Umbraco.Core/Services/ContentService.cs | 47 ++--- src/Umbraco.Core/Services/IContentService.cs | 40 ++++ .../Controllers/ContentController.cs | 70 +++++-- .../Filters/ContentSaveValidationAttribute.cs | 10 + .../components/content/edit.controller.js | 2 +- .../src/common/resources/content.resource.js | 10 +- .../overlays/publishdescendants.controller.js | 14 +- .../content/overlays/publishdescendants.html | 24 +++ .../UmbracoTestDataController.cs | 2 +- .../Services/ContentServiceTests.cs | 4 +- .../Services/ContentEventsTests.cs | 4 +- .../ContentServicePublishBranchTests.cs | 191 ++++++++++++++++-- .../Services/ContentServiceTagsTests.cs | 2 +- 17 files changed, 417 insertions(+), 93 deletions(-) create mode 100644 src/Umbraco.Core/Models/PublishBranchFilter.cs diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index 5f48939766..cf79f426b7 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -309,6 +309,7 @@ Remove this text box Content root Include unpublished content items. + Publish unchanged items. This value is hidden. If you need access to view this value please contact your website administrator. diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index f86b082513..bd387b4a07 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -308,6 +308,7 @@ Remove this text box Content root Include unpublished content items. + Publish unchanged items. This value is hidden. If you need access to view this value please contact your website administrator. diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs b/src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs index 889b03db6d..929ee7c097 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs @@ -1,69 +1,101 @@ namespace Umbraco.Cms.Core.Models.ContentEditing; /// -/// The action associated with saving a content item +/// The action associated with saving a content item. /// public enum ContentSaveAction { /// - /// Saves the content item, no publish + /// Saves the content item, no publish. /// Save = 0, /// - /// Creates a new content item + /// Creates a new content item. /// SaveNew = 1, /// - /// Saves and publishes the content item + /// Saves and publishes the content item. /// Publish = 2, /// - /// Creates and publishes a new content item + /// Creates and publishes a new content item. /// PublishNew = 3, /// - /// Saves and sends publish notification + /// Saves and sends publish notification. /// SendPublish = 4, /// - /// Creates and sends publish notification + /// Creates and sends publish notification. /// SendPublishNew = 5, /// - /// Saves and schedules publishing + /// Saves and schedules publishing. /// Schedule = 6, /// - /// Creates and schedules publishing + /// Creates and schedules publishing. /// ScheduleNew = 7, /// - /// Saves and publishes the content item including all descendants that have a published version + /// Saves and publishes the content item including all descendants that have a published version. /// PublishWithDescendants = 8, /// - /// Creates and publishes the content item including all descendants that have a published version + /// Creates and publishes the new content item including all descendants that have a published version. /// PublishWithDescendantsNew = 9, /// /// Saves and publishes the content item including all descendants regardless of whether they have a published version - /// or not + /// or not. /// + [Obsolete("This option is no longer used as the 'force' aspect has been extended into options for publishing unpublished and re-publishing changed content. Please use one of those options instead.")] PublishWithDescendantsForce = 10, /// - /// Creates and publishes the content item including all descendants regardless of whether they have a published - /// version or not + /// Creates and publishes the new content item including all descendants regardless of whether they have a published + /// version or not. /// + [Obsolete("This option is no longer used as the 'force' aspect has been extended into options for publishing unpublished and re-publishing changed content. Please use one of those options instead.")] PublishWithDescendantsForceNew = 11, + + /// + /// Saves and publishes the content item including all descendants including publishing previously unpublished content. + /// + PublishWithDescendantsIncludeUnpublished = 12, + + /// + /// Saves and publishes the new content item including all descendants including publishing previously unpublished content. + /// + PublishWithDescendantsIncludeUnpublishedNew = 13, + + /// + /// Saves and publishes the content item including all descendants irrespective of whether there are any pending changes. + /// + PublishWithDescendantsForceRepublish = 14, + + /// + /// Saves and publishes the new content item including all descendants including publishing previously unpublished content. + /// + PublishWithDescendantsForceRepublishNew = 15, + + /// + /// Saves and publishes the content item including all descendants including publishing previously unpublished content and irrespective of whether there are any pending changes. + /// + PublishWithDescendantsIncludeUnpublishedAndForceRepublish = 16, + + /// + /// Saves and publishes the new content item including all descendants including publishing previously unpublished content and irrespective of whether there are any pending changes. + /// + PublishWithDescendantsIncludeUnpublishedAndForceRepublishNew = 17, } diff --git a/src/Umbraco.Core/Models/PublishBranchFilter.cs b/src/Umbraco.Core/Models/PublishBranchFilter.cs new file mode 100644 index 0000000000..e47a07f677 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishBranchFilter.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Cms.Core.Models; + +/// +/// Describes the options available with publishing a content branch for force publishing. +/// +[Flags] +public enum PublishBranchFilter +{ + /// + /// The default behavior is to publish only the published content that has changed. + /// + Default = 0, + + /// + /// For publishing a branch, publish all changed content, including content that is not published. + /// + IncludeUnpublished = 1, + + /// + /// For publishing a branch, force republishing of all published content, including content that has not changed. + /// + ForceRepublish = 2, + + /// + /// For publishing a branch, publish all content, including content that is not published and content that has not changed. + /// + All = IncludeUnpublished | ForceRepublish, +} diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 5ff81a2165..45176629a2 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1963,17 +1963,14 @@ public class ContentService : RepositoryService, IContentService } // utility 'ShouldPublish' func used by SaveAndPublishBranch - private HashSet? SaveAndPublishBranch_ShouldPublish(ref HashSet? cultures, string c, bool published, bool edited, bool isRoot, bool force) + private HashSet? SaveAndPublishBranch_ShouldPublish(ref HashSet? cultures, string c, bool published, bool edited, bool isRoot, PublishBranchFilter publishBranchFilter) { // if published, republish if (published) { - if (cultures == null) - { - cultures = new HashSet(); // empty means 'already published' - } + cultures ??= []; // empty means 'already published' - if (edited) + if (edited || publishBranchFilter.HasFlag(PublishBranchFilter.ForceRepublish)) { cultures.Add(c); // means 'republish this culture' } @@ -1982,15 +1979,12 @@ public class ContentService : RepositoryService, IContentService } // if not published, publish if force/root else do nothing - if (!force && !isRoot) + if (!publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished) && !isRoot) { return cultures; // null means 'nothing to do' } - if (cultures == null) - { - cultures = new HashSet(); - } + cultures ??= []; cultures.Add(c); // means 'publish this culture' return cultures; @@ -1998,6 +1992,10 @@ public class ContentService : RepositoryService, IContentService /// public IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = Constants.Security.SuperUserId) + => SaveAndPublishBranch(content, force ? PublishBranchFilter.IncludeUnpublished : PublishBranchFilter.Default, culture, userId); + + /// + public IEnumerable SaveAndPublishBranch(IContent content, PublishBranchFilter publishBranchFilter, string culture = "*", int userId = Constants.Security.SuperUserId) { // note: EditedValue and PublishedValue are objects here, so it is important to .Equals() // and not to == them, else we would be comparing references, and that is a bad thing @@ -2016,13 +2014,13 @@ public class ContentService : RepositoryService, IContentService // invariant content type if (!c.ContentType.VariesByCulture()) { - return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force); + return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, publishBranchFilter); } // variant content type, specific culture if (culture != "*") { - return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force); + return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, publishBranchFilter); } // variant content type, all cultures @@ -2032,23 +2030,27 @@ public class ContentService : RepositoryService, IContentService // others will have to 'republish this culture' foreach (var x in c.AvailableCultures) { - SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force); + SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, publishBranchFilter); } return culturesToPublish; } - // if not published, publish if force/root else do nothing - return force || isRoot + // if not published, publish if forcing unpublished/root else do nothing + return publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished) || isRoot ? new HashSet { "*" } // "*" means 'publish all' : null; // null means 'nothing to do' } - return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId); + return SaveAndPublishBranch(content, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId); } /// public IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId) + => SaveAndPublishBranch(content, force ? PublishBranchFilter.IncludeUnpublished : PublishBranchFilter.Default, cultures, userId); + + /// + public IEnumerable SaveAndPublishBranch(IContent content, PublishBranchFilter publishBranchFilter, string[] cultures, int userId = Constants.Security.SuperUserId) { // note: EditedValue and PublishedValue are objects here, so it is important to .Equals() // and not to == them, else we would be comparing references, and that is a bad thing @@ -2064,7 +2066,7 @@ public class ContentService : RepositoryService, IContentService // invariant content type if (!c.ContentType.VariesByCulture()) { - return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force); + return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, publishBranchFilter); } // variant content type, specific cultures @@ -2074,24 +2076,23 @@ public class ContentService : RepositoryService, IContentService // others will have to 'republish this culture' foreach (var x in cultures) { - SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force); + SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, publishBranchFilter); } return culturesToPublish; } - // if not published, publish if force/root else do nothing - return force || isRoot + // if not published, publish if forcing unpublished/root else do nothing + return publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished) || isRoot ? new HashSet(cultures) // means 'publish specified cultures' : null; // null means 'nothing to do' } - return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId); + return SaveAndPublishBranch(content, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId); } internal IEnumerable SaveAndPublishBranch( IContent document, - bool force, Func?> shouldPublish, Func, IReadOnlyCollection, bool> publishCultures, int userId = Constants.Security.SuperUserId) diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 1733a74142..c7b33589b3 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -431,6 +431,7 @@ public interface IContentService : IContentServiceBase /// published. The root of the branch is always published, regardless of . /// /// + [Obsolete("This method is not longer used as the 'force' parameter has been extended into options for publishing unpublished and re-publishing changed content. Please use the overload containing the parameter for those options instead.")] IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = Constants.Security.SuperUserId); /// @@ -447,8 +448,47 @@ public interface IContentService : IContentServiceBase /// published. The root of the branch is always published, regardless of . /// /// + [Obsolete("This method is not longer used as the 'force' parameter has been extended into options for publishing unpublished and re-publishing changed content. Please use the overload containing the parameter for those options instead.")] IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId); + /// + /// Saves and publishes a document branch. + /// + /// The root document. + /// A value indicating options for force publishing unpublished or re-publishing unchanged content. + /// A culture, or "*" for all cultures. + /// The identifier of the user performing the operation. + /// + /// + /// Unless specified, all cultures are re-published. Otherwise, one culture can be specified. To act on more + /// than one culture, see the other overloads of this method. + /// + /// + /// The root of the branch is always published, regardless of . + /// + /// + IEnumerable SaveAndPublishBranch(IContent content, PublishBranchFilter publishBranchFilter, string culture = "*", int userId = Constants.Security.SuperUserId) +#pragma warning disable CS0618 // Type or member is obsolete + => SaveAndPublishBranch(content, publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished), culture, userId); +#pragma warning restore CS0618 // Type or member is obsolete + + /// + /// Saves and publishes a document branch. + /// + /// The root document. + /// A value indicating options for force publishing unpublished or re-publishing unchanged content. + /// The cultures to publish. + /// The identifier of the user performing the operation. + /// + /// + /// The root of the branch is always published, regardless of . + /// + /// + IEnumerable SaveAndPublishBranch(IContent content, PublishBranchFilter publishBranchFilter, string[] cultures, int userId = Constants.Security.SuperUserId) +#pragma warning disable CS0618 // Type or member is obsolete + => SaveAndPublishBranch(content, publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished), cultures, userId); +#pragma warning restore CS0618 // Type or member is obsolete + ///// ///// Saves and publishes a document branch. ///// diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index c676cee2ca..ef935b6b59 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -999,18 +999,29 @@ public class ContentController : ContentControllerBase // if there's only one variant and the model state is not valid we cannot publish so change it to save if (variantCount == 1) { + switch (contentItem.Action) { case ContentSaveAction.Publish: case ContentSaveAction.PublishWithDescendants: +#pragma warning disable CS0618 // Type or member is obsolete case ContentSaveAction.PublishWithDescendantsForce: +#pragma warning restore CS0618 // Type or member is obsolete + case ContentSaveAction.PublishWithDescendantsIncludeUnpublished: + case ContentSaveAction.PublishWithDescendantsForceRepublish: + case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublish: case ContentSaveAction.SendPublish: case ContentSaveAction.Schedule: contentItem.Action = ContentSaveAction.Save; break; case ContentSaveAction.PublishNew: case ContentSaveAction.PublishWithDescendantsNew: +#pragma warning disable CS0618 // Type or member is obsolete case ContentSaveAction.PublishWithDescendantsForceNew: +#pragma warning restore CS0618 // Type or member is obsolete + case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedNew: + case ContentSaveAction.PublishWithDescendantsForceRepublishNew: + case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublishNew: case ContentSaveAction.SendPublishNew: case ContentSaveAction.ScheduleNew: contentItem.Action = ContentSaveAction.SaveNew; @@ -1144,6 +1155,16 @@ public class ContentController : ContentControllerBase break; case ContentSaveAction.PublishWithDescendants: case ContentSaveAction.PublishWithDescendantsNew: +#pragma warning disable CS0618 // Type or member is obsolete + case ContentSaveAction.PublishWithDescendantsForce: + case ContentSaveAction.PublishWithDescendantsForceNew: +#pragma warning restore CS0618 // Type or member is obsolete + case ContentSaveAction.PublishWithDescendantsIncludeUnpublished: + case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedNew: + case ContentSaveAction.PublishWithDescendantsForceRepublish: + case ContentSaveAction.PublishWithDescendantsForceRepublishNew: + case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublish: + case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublishNew: { if (!await ValidatePublishBranchPermissionsAsync(contentItem)) { @@ -1154,7 +1175,7 @@ public class ContentController : ContentControllerBase break; } - var publishStatus = PublishBranchInternal(contentItem, false, cultureForInvariantErrors, out wasCancelled, out var successfulCultures).ToList(); + var publishStatus = PublishBranchInternal(contentItem, BuildPublishBranchFilter(contentItem.Action), cultureForInvariantErrors, out wasCancelled, out var successfulCultures).ToList(); var addedDomainWarnings = AddDomainWarnings(publishStatus, successfulCultures, globalNotifications, defaultCulture); AddPublishStatusNotifications(publishStatus, globalNotifications, notifications, successfulCultures); if (addedDomainWarnings is false) @@ -1163,22 +1184,6 @@ public class ContentController : ContentControllerBase } } break; - case ContentSaveAction.PublishWithDescendantsForce: - case ContentSaveAction.PublishWithDescendantsForceNew: - { - if (!await ValidatePublishBranchPermissionsAsync(contentItem)) - { - globalNotifications.AddErrorNotification( - _localizedTextService.Localize(null, "publish"), - _localizedTextService.Localize("publish", "invalidPublishBranchPermissions")); - wasCancelled = false; - break; - } - - var publishStatus = PublishBranchInternal(contentItem, true, cultureForInvariantErrors, out wasCancelled, out var successfulCultures).ToList(); - AddPublishStatusNotifications(publishStatus, globalNotifications, notifications, successfulCultures); - } - break; default: throw new ArgumentOutOfRangeException(); } @@ -1228,6 +1233,31 @@ public class ContentController : ContentControllerBase return display; } + private static PublishBranchFilter BuildPublishBranchFilter(ContentSaveAction contentSaveAction) + { + var includeUnpublished = contentSaveAction == ContentSaveAction.PublishWithDescendantsIncludeUnpublished + || contentSaveAction == ContentSaveAction.PublishWithDescendantsIncludeUnpublishedNew + || contentSaveAction == ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublish + || contentSaveAction == ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublishNew; + var forceRepublish = contentSaveAction == ContentSaveAction.PublishWithDescendantsForceRepublish + || contentSaveAction == ContentSaveAction.PublishWithDescendantsForceRepublishNew + || contentSaveAction == ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublish + || contentSaveAction == ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublishNew; + + PublishBranchFilter publishBranchFilter = PublishBranchFilter.Default; + if (includeUnpublished) + { + publishBranchFilter |= PublishBranchFilter.IncludeUnpublished; + } + + if (forceRepublish) + { + publishBranchFilter |= PublishBranchFilter.ForceRepublish; + } + + return publishBranchFilter; + } + private void AddPublishStatusNotifications( IReadOnlyCollection publishStatus, SimpleNotificationModel globalNotifications, @@ -1668,12 +1698,12 @@ public class ContentController : ContentControllerBase return authorizationResult.Succeeded; } - private IEnumerable PublishBranchInternal(ContentItemSave contentItem, bool force, string? cultureForInvariantErrors, out bool wasCancelled, out string[]? successfulCultures) + private IEnumerable PublishBranchInternal(ContentItemSave contentItem, PublishBranchFilter publishBranchFilter, string? cultureForInvariantErrors, out bool wasCancelled, out string[]? successfulCultures) { if (!contentItem.PersistedContent?.ContentType.VariesByCulture() ?? false) { //its invariant, proceed normally - IEnumerable publishStatus = _contentService.SaveAndPublishBranch(contentItem.PersistedContent!, force, userId: _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); + IEnumerable publishStatus = _contentService.SaveAndPublishBranch(contentItem.PersistedContent!, publishBranchFilter, userId: _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); // TODO: Deal with multiple cancellations wasCancelled = publishStatus.Any(x => x.Result == PublishResultType.FailedPublishCancelledByEvent); successfulCultures = null; //must be null! this implies invariant @@ -1709,7 +1739,7 @@ public class ContentController : ContentControllerBase { //proceed to publish if all validation still succeeds IEnumerable publishStatus = _contentService.SaveAndPublishBranch( - contentItem.PersistedContent!, force, culturesToPublish.WhereNotNull().ToArray(), _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); + contentItem.PersistedContent!, publishBranchFilter, culturesToPublish.WhereNotNull().ToArray(), _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); // TODO: Deal with multiple cancellations wasCancelled = publishStatus.Any(x => x.Result == PublishResultType.FailedPublishCancelledByEvent); successfulCultures = contentItem.Variants.Where(x => x.Publish).Select(x => x.Culture).WhereNotNull() diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs index c75bbd5a80..45948b5960 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs @@ -182,7 +182,12 @@ public sealed class ContentSaveValidationAttribute : TypeFilterAttribute break; case ContentSaveAction.Publish: case ContentSaveAction.PublishWithDescendants: +#pragma warning disable CS0618 // Type or member is obsolete case ContentSaveAction.PublishWithDescendantsForce: +#pragma warning restore CS0618 // Type or member is obsolete + case ContentSaveAction.PublishWithDescendantsIncludeUnpublished: + case ContentSaveAction.PublishWithDescendantsForceRepublish: + case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublish: permissionToCheck.Add(ActionPublish.ActionLetter); contentToCheck = contentItem.PersistedContent; contentIdToCheck = contentToCheck?.Id ?? default; @@ -232,7 +237,12 @@ public sealed class ContentSaveValidationAttribute : TypeFilterAttribute break; case ContentSaveAction.PublishNew: case ContentSaveAction.PublishWithDescendantsNew: +#pragma warning disable CS0618 // Type or member is obsolete case ContentSaveAction.PublishWithDescendantsForceNew: +#pragma warning restore CS0618 // Type or member is obsolete + case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedNew: + case ContentSaveAction.PublishWithDescendantsForceRepublishNew: + case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublishNew: //Publish new requires both ActionNew AND ActionPublish // TODO: Shouldn't publish also require ActionUpdate since it will definitely perform an update to publish but maybe that's just implied 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 72c5f3fec1..6d5503301b 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 @@ -963,7 +963,7 @@ //we need to return this promise so that the dialog can handle the result and wire up the validation response return performSave({ saveMethod: function (content, create, files, showNotifications) { - return contentResource.publishWithDescendants(content, create, model.includeUnpublished, files, showNotifications); + return contentResource.publishWithDescendants(content, create, model.includeUnpublished, model.forceRepublish, files, showNotifications); }, action: "publishDescendants", showNotifications: false, diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index b3218b2c7f..a86675916f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -1003,14 +1003,18 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the saved content item. * */ - publishWithDescendants: function (content, isNew, force, files, showNotifications) { + publishWithDescendants: function (content, isNew, includeUnpublished, forceRepublish, files, showNotifications) { var endpoint = umbRequestHelper.getApiUrl( "contentApiBaseUrl", "PostSave"); var action = "publishWithDescendants"; - if (force === true) { - action += "Force"; + if (includeUnpublished === true && forceRepublish === true) { + action += "IncludeUnpublishedAndForceRepublish"; + } else if (includeUnpublished === true) { + action += "IncludeUnpublished"; + } else if (forceRepublish === true) { + action += "ForceRepublish"; } return saveContentItem(content, action + (isNew ? "New" : ""), files, endpoint, showNotifications); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js index 1b4c16b28f..40e7e2d4ba 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js @@ -5,10 +5,12 @@ var vm = this; vm.includeUnpublished = $scope.model.includeUnpublished || false; + vm.forceRepublish = $scope.model.forceRepublish || false; vm.publishAll = false; vm.changeSelection = changeSelection; vm.toggleIncludeUnpublished = toggleIncludeUnpublished; + vm.toggleForceRepublish = toggleForceRepublish; vm.changePublishAllSelection = changePublishAllSelection; function onInit() { @@ -28,9 +30,9 @@ vm.labels.includeUnpublished = value; }); } - if (!vm.labels.includeUnpublished) { - localizationService.localize("content_includeUnpublished").then(value => { - vm.labels.includeUnpublished = value; + if (!vm.labels.forceRepublish) { + localizationService.localize("content_forceRepublish").then(value => { + vm.labels.forceRepublish = value; }); } @@ -69,10 +71,14 @@ function toggleIncludeUnpublished() { vm.includeUnpublished = !vm.includeUnpublished; - // make sure this value is pushed back to the scope $scope.model.includeUnpublished = vm.includeUnpublished; } + function toggleForceRepublish() { + vm.forceRepublish = !vm.forceRepublish; + $scope.model.forceRepublish = vm.forceRepublish; + } + /** Returns true if publishing is possible based on if there are un-published mandatory languages */ function canPublish() { var selected = []; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html index cbd78fb2d5..a4d337ac71 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html @@ -17,6 +17,18 @@ +
+ + +
+
@@ -36,6 +48,18 @@
+
+ + +
+
diff --git a/tests/Umbraco.TestData/UmbracoTestDataController.cs b/tests/Umbraco.TestData/UmbracoTestDataController.cs index 2e44764030..56d196f179 100644 --- a/tests/Umbraco.TestData/UmbracoTestDataController.cs +++ b/tests/Umbraco.TestData/UmbracoTestDataController.cs @@ -86,7 +86,7 @@ public class UmbracoTestDataController : SurfaceController var imageIds = CreateMediaTree(company, faker, count, depth).ToList(); var contentIds = CreateContentTree(company, faker, count, depth, imageIds, out var root).ToList(); - Services.ContentService.SaveAndPublishBranch(root, true); + Services.ContentService.SaveAndPublishBranch(root, PublishBranchFilter.IncludeUnpublished); scope.Complete(); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index a624d6d885..55e98daa3a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs @@ -1303,7 +1303,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent // publish parent & its branch // only those that are not already published // only invariant/neutral values - var parentPublished = ContentService.SaveAndPublishBranch(parent, true); + var parentPublished = ContentService.SaveAndPublishBranch(parent, PublishBranchFilter.IncludeUnpublished); foreach (var result in parentPublished) { @@ -1522,7 +1522,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent ContentService.Save(content); // Act - var published = ContentService.SaveAndPublishBranch(content, true); + var published = ContentService.SaveAndPublishBranch(content, PublishBranchFilter.IncludeUnpublished); // Assert Assert.That(published.All(x => x.Success), Is.False); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs index fda265c705..b0a241a9f5 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs @@ -706,7 +706,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // branch is: ResetEvents(); - ContentService.SaveAndPublishBranch(content1, force: false); // force = false, don't publish unpublished items + ContentService.SaveAndPublishBranch(content1, PublishBranchFilter.Default); // PublishBranchFilter.Default: don't publish unpublished items foreach (EventInstance e in _events) { @@ -743,7 +743,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services ContentService.Unpublish(content1); ResetEvents(); - ContentService.SaveAndPublishBranch(content1, force: true); // force = true, also publish unpublished items + ContentService.SaveAndPublishBranch(content1, PublishBranchFilter.IncludeUnpublished); // PublishBranchFilter.IncludeUnpublished: also publish unpublished items foreach (EventInstance e in _events) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs index 48cc197be6..e9e673d501 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -16,8 +14,7 @@ using Umbraco.Cms.Tests.Integration.Testing; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; [TestFixture] -[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, PublishedRepositoryEvents = true, - WithApplication = true)] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, PublishedRepositoryEvents = true, WithApplication = true)] public class ContentServicePublishBranchTests : UmbracoIntegrationTest { private IContentService ContentService => GetRequiredService(); @@ -47,9 +44,9 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest // ii1 !published !edited // ii2 !published !edited - // !force = publishes those that are actually published, and have changes + // PublishBranchFilter.None = publishes those that are actually published, and have changes // here: root (root is always published) - var r = SaveAndPublishInvariantBranch(iRoot, false, method).ToArray(); + var r = SaveAndPublishInvariantBranch(iRoot, PublishBranchFilter.Default, method).ToArray(); // not forcing, ii1 and ii2 not published yet: only root got published AssertPublishResults(r, x => x.Content.Name, "iroot"); @@ -83,9 +80,9 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest // ii21 (published) !edited // ii22 !published !edited - // !force = publishes those that are actually published, and have changes + // PublishBranchFilter.None = publishes those that are actually published, and have changes // here: nothing - r = SaveAndPublishInvariantBranch(iRoot, false, method).ToArray(); + r = SaveAndPublishInvariantBranch(iRoot, PublishBranchFilter.Default, method).ToArray(); // not forcing, ii12 and ii2, ii21, ii22 not published yet: only root, ii1, ii11 got published AssertPublishResults(r, x => x.Content.Name, "iroot", "ii1", "ii11"); @@ -110,11 +107,11 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest // ii21 (published) !edited // ii22 !published !edited - // !force = publishes those that are actually published, and have changes + // PublishBranchFilter.None = publishes those that are actually published, and have changes // here: iroot and ii11 // not forcing, ii12 and ii2, ii21, ii22 not published yet: only root, ii1, ii11 got published - r = SaveAndPublishInvariantBranch(iRoot, false, method).ToArray(); + r = SaveAndPublishInvariantBranch(iRoot, PublishBranchFilter.Default, method).ToArray(); AssertPublishResults(r, x => x.Content.Name, "iroot", "ii1", "ii11"); AssertPublishResults( r, @@ -123,9 +120,9 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest PublishResultType.SuccessPublishAlready, PublishResultType.SuccessPublish); - // force = publishes everything that has changes + // PublishBranchFilter.IncludeUnpublished = publishes everything that has changes // here: ii12, ii2, ii22 - ii21 was published already but masked - r = SaveAndPublishInvariantBranch(iRoot, true, method).ToArray(); + r = SaveAndPublishInvariantBranch(iRoot, PublishBranchFilter.IncludeUnpublished, method).ToArray(); AssertPublishResults( r, x => x.Content.Name, @@ -182,7 +179,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest iv1.SetValue("vp", "UPDATED-iv1.de", "de"); ContentService.Save(iv1); - var r = ContentService.SaveAndPublishBranch(vRoot, false) + var r = ContentService.SaveAndPublishBranch(vRoot, PublishBranchFilter.Default) .ToArray(); // no culture specified so "*" is used, so all cultures Assert.AreEqual(PublishResultType.SuccessPublishAlready, r[0].Result); Assert.AreEqual(PublishResultType.SuccessPublishCulture, r[1].Result); @@ -219,7 +216,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest iv1.SetValue("vp", "UPDATED-iv1.de", "de"); var saveResult = ContentService.Save(iv1); - var r = ContentService.SaveAndPublishBranch(vRoot, false, "de").ToArray(); + var r = ContentService.SaveAndPublishBranch(vRoot, PublishBranchFilter.Default, "de").ToArray(); Assert.AreEqual(PublishResultType.SuccessPublishAlready, r[0].Result); Assert.AreEqual(PublishResultType.SuccessPublishCulture, r[1].Result); } @@ -263,9 +260,9 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest // iv1 !published !edited // iv2 !published !edited - // !force = publishes those that are actually published, and have changes + // PublishBranchFilter.None = publishes those that are actually published, and have changes // here: nothing - var r = ContentService.SaveAndPublishBranch(vRoot, false).ToArray(); // no culture specified = all cultures + var r = ContentService.SaveAndPublishBranch(vRoot, PublishBranchFilter.Default).ToArray(); // no culture specified = all cultures // not forcing, iv1 and iv2 not published yet: only root got published AssertPublishResults(r, x => x.Content.Name, "vroot.de"); @@ -298,7 +295,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest Assert.IsTrue(iv1.IsCulturePublished("ru")); Assert.IsFalse(iv1.IsCulturePublished("es")); - r = ContentService.SaveAndPublishBranch(vRoot, false, "de").ToArray(); + r = ContentService.SaveAndPublishBranch(vRoot, PublishBranchFilter.Default, "de").ToArray(); // not forcing, iv2 not published yet: only root and iv1 got published AssertPublishResults(r, x => x.Content.Name, "vroot.de", "iv1.de"); @@ -375,7 +372,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest { Can_Publish_Mixed_Branch(out var iRoot, out var ii1, out var iv11); - var r = ContentService.SaveAndPublishBranch(iRoot, false, "de").ToArray(); + var r = ContentService.SaveAndPublishBranch(iRoot, PublishBranchFilter.Default, "de").ToArray(); AssertPublishResults(r, x => x.Content.Name, "iroot", "ii1", "iv11.de"); AssertPublishResults( r, @@ -401,7 +398,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest { Can_Publish_Mixed_Branch(out var iRoot, out var ii1, out var iv11); - var r = ContentService.SaveAndPublishBranch(iRoot, false, new[] { "de", "ru" }).ToArray(); + var r = ContentService.SaveAndPublishBranch(iRoot, PublishBranchFilter.Default, ["de", "ru"]).ToArray(); AssertPublishResults(r, x => x.Content.Name, "iroot", "ii1", "iv11.de"); AssertPublishResults( r, @@ -422,6 +419,156 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest Assert.AreEqual("changed.ru", iv11.GetValue("vp", "ru", published: true)); } + [TestCase(PublishBranchFilter.Default)] + [TestCase(PublishBranchFilter.IncludeUnpublished)] + [TestCase(PublishBranchFilter.ForceRepublish)] + [TestCase(PublishBranchFilter.All)] + public void Can_Publish_Invariant_Branch_With_Force_Options(PublishBranchFilter publishBranchFilter) + { + CreateTypes(out var iContentType, out _); + + // Create content (published root, published child, unpublished child, changed child). + IContent iRoot = new Content("iroot", -1, iContentType); + iRoot.SetValue("ip", "iroot"); + ContentService.SaveAndPublish(iRoot); + + IContent ii1 = new Content("ii1", iRoot, iContentType); + ii1.SetValue("ip", "vii1"); + ContentService.SaveAndPublish(ii1); + + IContent ii2 = new Content("ii2", iRoot, iContentType); + ii2.SetValue("ip", "vii2"); + ContentService.Save(ii2); + + IContent ii3 = new Content("ii3", iRoot, iContentType); + ii3.SetValue("ip", "vii3"); + ContentService.SaveAndPublish(ii3); + ii3.SetValue("ip", "vii3a"); + ContentService.Save(ii3); + + var result = ContentService.SaveAndPublishBranch(iRoot, publishBranchFilter).ToArray(); + + var expectedContentNames = GetExpectedContentNamesForForceOptions(publishBranchFilter); + var expectedPublishResultTypes = GetExpectedPublishResultTypesForForceOptions(publishBranchFilter); + AssertPublishResults(result, x => x.Content.Name, expectedContentNames); + AssertPublishResults( + result, + x => x.Result, + expectedPublishResultTypes); + } + + [TestCase("*", PublishBranchFilter.Default)] + [TestCase("*", PublishBranchFilter.IncludeUnpublished)] + [TestCase("*", PublishBranchFilter.ForceRepublish)] + [TestCase("*", PublishBranchFilter.All)] + [TestCase("de", PublishBranchFilter.Default)] + [TestCase("de", PublishBranchFilter.IncludeUnpublished)] + [TestCase("de", PublishBranchFilter.ForceRepublish)] + [TestCase("de", PublishBranchFilter.All)] + public void Can_Publish_Variant_Branch_With_Force_Options(string culture, PublishBranchFilter publishBranchFilter) + { + CreateTypes(out _, out var vContentType); + + // Create content (published root, published child, unpublished child, changed child). + IContent vRoot = new Content("vroot", -1, vContentType); + vRoot.SetCultureName("vroot.de", "de"); + vRoot.SetCultureName("vroot.ru", "ru"); + vRoot.SetValue("ip", "vroot"); + vRoot.SetValue("vp", "vroot.de", "de"); + vRoot.SetValue("vp", "vroot.ru", "ru"); + ContentService.SaveAndPublish(vRoot); + + IContent iv1 = new Content("iv1", vRoot, vContentType, "de"); + iv1.SetCultureName("iv1.de", "de"); + iv1.SetCultureName("iv1.ru", "ru"); + iv1.SetValue("ip", "iv1"); + iv1.SetValue("vp", "iv1.de", "de"); + iv1.SetValue("vp", "iv1.ru", "ru"); + ContentService.SaveAndPublish(iv1); + + IContent iv2 = new Content("iv2", vRoot, vContentType, "de"); + iv2.SetCultureName("iv2.de", "de"); + iv2.SetCultureName("iv2.ru", "ru"); + iv2.SetValue("ip", "iv2"); + iv2.SetValue("vp", "iv2.de", "de"); + iv2.SetValue("vp", "iv2.ru", "ru"); + ContentService.Save(iv2); + + // When testing with a specific culture, publish the other one, so we can test that + // the specified unpublished culture is handled correctly. + if (culture != "*") + { + ContentService.SaveAndPublish(iv2, "ru"); + } + + IContent iv3 = new Content("iv3", vRoot, vContentType, "de"); + iv3.SetCultureName("iv3.de", "de"); + iv3.SetCultureName("iv3.ru", "ru"); + iv3.SetValue("ip", "iv3"); + iv3.SetValue("vp", "iv3.de", "de"); + iv3.SetValue("vp", "iv3.ru", "ru"); + ContentService.SaveAndPublish(iv3); + iv3.SetValue("ip", "iv3a"); + iv3.SetValue("vp", "iv3a.de", "de"); + iv3.SetValue("vp", "iv3a.ru", "ru"); + ContentService.Save(iv3); + + var result = ContentService.SaveAndPublishBranch(vRoot, publishBranchFilter, culture).ToArray(); + + var expectedContentNames = GetExpectedContentNamesForForceOptions(publishBranchFilter, true); + var expectedPublishResultTypes = GetExpectedPublishResultTypesForForceOptions(publishBranchFilter, true); + AssertPublishResults(result, x => x.Content.Name, expectedContentNames); + AssertPublishResults( + result, + x => x.Result, + expectedPublishResultTypes); + } + + private static string[] GetExpectedContentNamesForForceOptions(PublishBranchFilter publishBranchFilter, bool isVariant = false) + { + var rootName = isVariant ? "vroot.de" : "iroot"; + var childPrefix = isVariant ? "iv" : "ii"; + var childSuffix = isVariant ? ".de" : string.Empty; + if (publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished)) + { + return [rootName, $"{childPrefix}1{childSuffix}", $"{childPrefix}2{childSuffix}", $"{childPrefix}3{childSuffix}"]; + } + + return [rootName, $"{childPrefix}1{childSuffix}", $"{childPrefix}3{childSuffix}"]; + } + + private static PublishResultType[] GetExpectedPublishResultTypesForForceOptions(PublishBranchFilter publishBranchFilter, bool isVariant = false) + { + var successPublish = isVariant ? PublishResultType.SuccessPublishCulture : PublishResultType.SuccessPublish; + if (publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished) && publishBranchFilter.HasFlag(PublishBranchFilter.ForceRepublish)) + { + return [successPublish, + successPublish, + successPublish, + successPublish]; + } + + if (publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished)) + { + return [PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublishAlready, + successPublish, + successPublish]; + } + + if (publishBranchFilter.HasFlag(PublishBranchFilter.ForceRepublish)) + { + return [successPublish, + successPublish, + + successPublish]; + } + + return [PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublishAlready, + successPublish]; + } + private void AssertPublishResults(PublishResult[] values, Func getter, params T[] expected) { if (expected.Length != values.Length) @@ -479,16 +626,16 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest ContentTypeService.Save(vContentType); } - private IEnumerable SaveAndPublishInvariantBranch(IContent content, bool force, int method) + private IEnumerable SaveAndPublishInvariantBranch(IContent content, PublishBranchFilter publishBranchFilter, int method) { // ReSharper disable RedundantArgumentDefaultValue // ReSharper disable ArgumentsStyleOther switch (method) { case 1: - return ContentService.SaveAndPublishBranch(content, force, "*"); + return ContentService.SaveAndPublishBranch(content, publishBranchFilter, "*"); case 2: - return ContentService.SaveAndPublishBranch(content, force, cultures: new[] { "*" }); + return ContentService.SaveAndPublishBranch(content, publishBranchFilter, cultures: new[] { "*" }); default: throw new ArgumentOutOfRangeException(nameof(method)); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs index 15005f2e08..cdc3ebf4bf 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs @@ -669,7 +669,7 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest ContentService.Save(child2); // Act - ContentService.SaveAndPublishBranch(content, true); + ContentService.SaveAndPublishBranch(content, PublishBranchFilter.IncludeUnpublished); // Assert var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id;