From bfff224c3e553d1d820de02bb1fe8ac76d850328 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 8 Jan 2025 15:07:56 +0100 Subject: [PATCH] Display variant selection on unpublish only if the document is variant (#17893) * Display variant selection on unpublish only if the document is variant. * Allow for publish and unpublish of variant and invariant content. * Added integration tests for amends to ContentPublishingService. * Fixed assert. * Fixed assert and used consistent language codes. * Further integration tests. --- .../Services/ContentPublishingService.cs | 57 +++++++++++++++---- .../common/confirm/confirm-modal.token.ts | 2 +- .../entity-bulk-action/publish.bulk-action.ts | 8 ++- .../modal/document-publish-modal.element.ts | 7 ++- .../unpublish.bulk-action.ts | 8 ++- .../modal/document-unpublish-modal.element.ts | 49 +++++++++++----- .../ContentPublishingServiceTests.Publish.cs | 24 +++++++- ...ContentPublishingServiceTests.Unpublish.cs | 24 +++++++- 8 files changed, 145 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentPublishingService.cs b/src/Umbraco.Core/Services/ContentPublishingService.cs index 218a7ec2dc..0c5ec59e75 100644 --- a/src/Umbraco.Core/Services/ContentPublishingService.cs +++ b/src/Umbraco.Core/Services/ContentPublishingService.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.ContentPublishing; @@ -54,7 +54,7 @@ internal sealed class ContentPublishingService : IContentPublishingService } else { - schedules.AddOrUpdate(culture, cultureToSchedule.Schedule!.PublishDate.Value.UtcDateTime,ContentScheduleAction.Release); + schedules.AddOrUpdate(culture, cultureToSchedule.Schedule!.PublishDate.Value.UtcDateTime, ContentScheduleAction.Release); } if (cultureToSchedule.Schedule!.UnpublishDate is null) @@ -91,7 +91,7 @@ internal sealed class ContentPublishingService : IContentPublishingService return Attempt.FailWithStatus(ContentPublishingOperationStatus.ContentNotFound, new ContentPublishingResult()); } - // clear all schedules and publish nothing + // If nothing is requested for publish or scheduling, clear all schedules and publish nothing. if (cultureAndSchedule.CulturesToPublishImmediately.Count == 0 && cultureAndSchedule.Schedules.FullSchedule.Count == 0) { @@ -102,11 +102,35 @@ internal sealed class ContentPublishingService : IContentPublishingService new ContentPublishingResult { Content = content }); } + ISet culturesToPublishImmediately = cultureAndSchedule.CulturesToPublishImmediately; + var cultures = - cultureAndSchedule.CulturesToPublishImmediately.Union( + culturesToPublishImmediately.Union( cultureAndSchedule.Schedules.FullSchedule.Select(x => x.Culture)).ToArray(); - if (content.ContentType.VariesByCulture()) + // If cultures are provided for non variant content, and they include the default culture, consider + // the request as valid for publishing the content. + // This is necessary as in a bulk publishing context the cultures are selected and provided from the + // list of languages. + bool variesByCulture = content.ContentType.VariesByCulture(); + if (!variesByCulture) + { + ILanguage? defaultLanguage = await _languageService.GetDefaultLanguageAsync(); + if (defaultLanguage is not null) + { + if (cultures.Contains(defaultLanguage.IsoCode)) + { + cultures = ["*"]; + } + + if (culturesToPublishImmediately.Contains(defaultLanguage.IsoCode)) + { + culturesToPublishImmediately = new HashSet { "*" }; + } + } + } + + if (variesByCulture) { if (cultures.Any() is false) { @@ -120,7 +144,7 @@ internal sealed class ContentPublishingService : IContentPublishingService return Attempt.FailWithStatus(ContentPublishingOperationStatus.CannotPublishInvariantWhenVariant, new ContentPublishingResult()); } - var validCultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode); + IEnumerable validCultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode); if (validCultures.ContainsAll(cultures) is false) { scope.Complete(); @@ -151,9 +175,9 @@ internal sealed class ContentPublishingService : IContentPublishingService var userId = await _userIdKeyResolver.GetAsync(userKey); PublishResult? result = null; - if (cultureAndSchedule.CulturesToPublishImmediately.Any()) + if (culturesToPublishImmediately.Any()) { - result = _contentService.Publish(content, cultureAndSchedule.CulturesToPublishImmediately.ToArray(), userId); + result = _contentService.Publish(content, culturesToPublishImmediately.ToArray(), userId); } if (result?.Success != false && cultureAndSchedule.Schedules.FullSchedule.Any()) @@ -202,10 +226,10 @@ internal sealed class ContentPublishingService : IContentPublishingService Name = content.GetPublishName(culture) ?? string.Empty, Culture = culture, Segment = null, - Properties = content.Properties.Where(prop=>prop.PropertyType.VariesByCulture()).Select(prop=> new PropertyValueModel() + Properties = content.Properties.Where(prop => prop.PropertyType.VariesByCulture()).Select(prop => new PropertyValueModel() { Alias = prop.Alias, - Value = prop.GetValue(culture: culture, segment:null, published:false) + Value = prop.GetValue(culture: culture, segment: null, published: false) }) }) }; @@ -272,6 +296,19 @@ internal sealed class ContentPublishingService : IContentPublishingService var userId = await _userIdKeyResolver.GetAsync(userKey); + // If cultures are provided for non variant content, and they include the default culture, consider + // the request as valid for unpublishing the content. + // This is necessary as in a bulk unpublishing context the cultures are selected and provided from the + // list of languages. + if (cultures is not null && !content.ContentType.VariesByCulture()) + { + ILanguage? defaultLanguage = await _languageService.GetDefaultLanguageAsync(); + if (defaultLanguage is not null && cultures.Contains(defaultLanguage.IsoCode)) + { + cultures = null; + } + } + Attempt attempt; if (cultures is null) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/confirm/confirm-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/confirm/confirm-modal.token.ts index d840de6147..96cab96c20 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/confirm/confirm-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/confirm/confirm-modal.token.ts @@ -4,7 +4,7 @@ import type { TemplateResult } from '@umbraco-cms/backoffice/external/lit'; export interface UmbConfirmModalData { headline: string; content: TemplateResult | string; - color?: 'positive' | 'danger'; + color?: 'positive' | 'danger' | 'warning'; cancelLabel?: string; confirmLabel?: string; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-bulk-action/publish.bulk-action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-bulk-action/publish.bulk-action.ts index 541d693ae6..0d31b1ec2d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-bulk-action/publish.bulk-action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-bulk-action/publish.bulk-action.ts @@ -67,7 +67,7 @@ export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase< data: { headline: localizationController.term('content_readyToPublish'), content: localizationController.term('prompt_confirmListViewPublish'), - color: 'danger', + color: 'positive', confirmLabel: localizationController.term('actions_publish'), }, }) @@ -77,7 +77,11 @@ export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase< if (confirm !== false) { const variantId = new UmbVariantId(options[0].language.unique, null); const publishingRepository = new UmbDocumentPublishingRepository(this._host); - await publishingRepository.unpublish(this.selection[0], [variantId]); + for (let i = 0; i < this.selection.length; i++) { + const id = this.selection[i]; + await publishingRepository.publish(id, [ { variantId }]); + } + eventContext.dispatchEvent(event); } return; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/modal/document-publish-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/modal/document-publish-modal.element.ts index 17429a0c98..e8dd4fe40f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/modal/document-publish-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/modal/document-publish-modal.element.ts @@ -29,10 +29,13 @@ export class UmbDocumentPublishModalElement extends UmbModalBaseElement< this.#selectionManager.setMultiple(true); this.#selectionManager.setSelectable(true); - // Only display variants that are relevant to pick from, i.e. variants that are draft, not-published-mandatory or published with pending changes: + // Only display variants that are relevant to pick from, i.e. variants that are draft, not-published-mandatory or published with pending changes. + // If we don't know the state (e.g. from a bulk publishing selection) we need to consider it available for selection. this._options = this.data?.options.filter( - (option) => isNotPublishedMandatory(option) || option.variant?.state !== UmbDocumentVariantState.NOT_CREATED, + (option) => (option.variant && option.variant.state === null) || + isNotPublishedMandatory(option) || + option.variant?.state !== UmbDocumentVariantState.NOT_CREATED, ) ?? []; let selected = this.value?.selection ?? []; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-bulk-action/unpublish.bulk-action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-bulk-action/unpublish.bulk-action.ts index 77f3f3f87d..d33f63e604 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-bulk-action/unpublish.bulk-action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-bulk-action/unpublish.bulk-action.ts @@ -66,7 +66,7 @@ export class UmbDocumentUnpublishEntityBulkAction extends UmbEntityBulkActionBas data: { headline: localizationController.term('actions_unpublish'), content: localizationController.term('prompt_confirmListViewUnpublish'), - color: 'danger', + color: 'warning', confirmLabel: localizationController.term('actions_unpublish'), }, }) @@ -76,7 +76,11 @@ export class UmbDocumentUnpublishEntityBulkAction extends UmbEntityBulkActionBas if (confirm !== false) { const variantId = new UmbVariantId(options[0].language.unique, null); const publishingRepository = new UmbDocumentPublishingRepository(this._host); - await publishingRepository.unpublish(this.selection[0], [variantId]); + for (let i = 0; i < this.selection.length; i++) { + const id = this.selection[i]; + await publishingRepository.unpublish(id, [variantId]); + } + eventContext.dispatchEvent(event); } return; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/modal/document-unpublish-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/modal/document-unpublish-modal.element.ts index aa49be7455..0b462fc58e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/modal/document-unpublish-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/modal/document-unpublish-modal.element.ts @@ -47,17 +47,29 @@ export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement< @state() _hasInvalidSelection = true; + @state() + _isInvariant = false; + override firstUpdated() { - this.#configureSelectionManager(); this.#getReferences(); + + // If invariant, don't display the variant selection component. + if (this.data?.options.length === 1 && this.data.options[0].unique === "invariant") { + this._isInvariant = true; + this._hasInvalidSelection = false; + return; + } + + this.#configureSelectionManager(); } async #configureSelectionManager() { this._selectionManager.setMultiple(true); this._selectionManager.setSelectable(true); - // Only display variants that are relevant to pick from, i.e. variants that are draft or published with pending changes: - this._options = this.data?.options.filter((option) => isPublished(option)) ?? []; + // Only display variants that are relevant to pick from, i.e. variants that are published or published with pending changes. + // If we don't know the state (e.g. from a bulk publishing selection) we need to consider it available for selection. + this._options = this.data?.options.filter((option) => (option.variant && option.variant.state === null) || isPublished(option)) ?? []; let selected = this.value?.selection ?? []; @@ -104,7 +116,10 @@ export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement< #submit() { if (this._hasUnpublishPermission) { - this.value = { selection: this._selection }; + const selection = this._isInvariant + ? ["invariant"] + : this._selection; + this.value = { selection }; this.modalContext?.submit(); return; } @@ -121,17 +136,21 @@ export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement< override render() { return html` -

- - Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages. - -

- + ${!this._isInvariant + ? html` +

+ + Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages. + +

+ + ` + : nothing}

@@ -159,7 +178,7 @@ export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement< diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs index 0071745809..e7a740084d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs @@ -1,4 +1,4 @@ -using NUnit.Framework; +using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentPublishing; @@ -630,6 +630,28 @@ public partial class ContentPublishingServiceTests Assert.AreEqual(ContentPublishingOperationStatus.InvalidCulture, result.Status); } + [Test] + public async Task Can_Publish_Invariant_Content_With_Cultures_Provided_If_The_Default_Culture_Is_Exclusively_Provided() + { + var result = await ContentPublishingService.PublishAsync(Textpage.Key, MakeModel(new HashSet() { "en-US" }), Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + } + + [Test] + public async Task Can_Publish_Invariant_Content_With_Cultures_Provided_If_The_Default_Culture_Is_Provided_With_Other_Cultures() + { + var result = await ContentPublishingService.PublishAsync(Textpage.Key, MakeModel(new HashSet() { "en-US", "da-DK" }), Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + } + + [Test] + public async Task Cannot_Publish_Invariant_Content_With_Cultures_Provided_That_Do_Not_Include_The_Default_Culture() + { + var result = await ContentPublishingService.PublishAsync(Textpage.Key, MakeModel(new HashSet() { "da-DK" }), Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.InvalidCulture, result.Status); + } + private void AssertBranchResultSuccess(ContentPublishingBranchResult result, params Guid[] expectedKeys) { var items = result.SucceededItems.ToArray(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Unpublish.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Unpublish.cs index ca41cb228d..38c3c03829 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Unpublish.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Unpublish.cs @@ -1,4 +1,4 @@ -using NUnit.Framework; +using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.OperationStatus; @@ -323,4 +323,26 @@ public partial class ContentPublishingServiceTests content = ContentService.GetById(content.Key)!; Assert.AreEqual(2, content.PublishedCultures.Count()); } + + [Test] + public async Task Can_Unpublish_Invariant_Content_With_Cultures_Provided_If_The_Default_Culture_Is_Exclusively_Provided() + { + var result = await ContentPublishingService.UnpublishAsync(Textpage.Key, new HashSet() { "en-US" }, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + } + + [Test] + public async Task Can_Unpublish_Invariant_Content_With_Cultures_Provided_If_The_Default_Culture_Is_Provided_With_Other_Cultures() + { + var result = await ContentPublishingService.UnpublishAsync(Textpage.Key, new HashSet() { "en-US", "da-DK" }, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + } + + [Test] + public async Task Cannot_Unpublish_Invariant_Content_With_Cultures_Provided_That_Do_Not_Include_The_Default_Culture() + { + var result = await ContentPublishingService.UnpublishAsync(Textpage.Key, new HashSet() { "da-DK" }, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.CannotPublishVariantWhenNotVariant, result.Result); + } }