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.
This commit is contained in:
@@ -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<string> 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<string> { "*" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<string> 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<ContentPublishingOperationStatus> attempt;
|
||||
if (cultures is null)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ?? [];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`<umb-body-layout headline=${this.localize.term('content_unpublish')}>
|
||||
<p id="subtitle">
|
||||
<umb-localize key="content_languagesToUnpublish">
|
||||
Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages.
|
||||
</umb-localize>
|
||||
</p>
|
||||
|
||||
<umb-document-variant-language-picker
|
||||
.selectionManager=${this._selectionManager}
|
||||
.variantLanguageOptions=${this._options}
|
||||
.requiredFilter=${this._hasInvalidSelection ? this._requiredFilter : undefined}
|
||||
.pickableFilter=${this.data?.pickableFilter}></umb-document-variant-language-picker>
|
||||
${!this._isInvariant
|
||||
? html`
|
||||
<p id="subtitle">
|
||||
<umb-localize key="content_languagesToUnpublish">
|
||||
Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages.
|
||||
</umb-localize>
|
||||
</p>
|
||||
<umb-document-variant-language-picker
|
||||
.selectionManager=${this._selectionManager}
|
||||
.variantLanguageOptions=${this._options}
|
||||
.requiredFilter=${this._hasInvalidSelection ? this._requiredFilter : undefined}
|
||||
.pickableFilter=${this.data?.pickableFilter}></umb-document-variant-language-picker>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<p>
|
||||
<umb-localize key="prompt_confirmUnpublish">
|
||||
@@ -159,7 +178,7 @@ export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement<
|
||||
<uui-button label=${this.localize.term('general_close')} @click=${this.#close}></uui-button>
|
||||
<uui-button
|
||||
label="${this.localize.term('actions_unpublish')}"
|
||||
?disabled=${this._hasInvalidSelection || !this._hasUnpublishPermission || this._selection.length === 0}
|
||||
?disabled=${this._hasInvalidSelection || !this._hasUnpublishPermission || (!this._isInvariant && this._selection.length === 0)}
|
||||
look="primary"
|
||||
color="warning"
|
||||
@click=${this.#submit}></uui-button>
|
||||
|
||||
@@ -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<string>() { "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<string>() { "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<string>() { "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();
|
||||
|
||||
@@ -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<string>() { "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<string>() { "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<string>() { "da-DK" }, Constants.Security.SuperUserKey);
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual(ContentPublishingOperationStatus.CannotPublishVariantWhenNotVariant, result.Result);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user