Skip to content

Commit bfff224

Browse files
authored
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.
1 parent f9c52b7 commit bfff224

File tree

8 files changed

+145
-34
lines changed

8 files changed

+145
-34
lines changed

src/Umbraco.Core/Services/ContentPublishingService.cs

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Umbraco.Cms.Core.Events;
1+
using Umbraco.Cms.Core.Events;
22
using Umbraco.Cms.Core.Models;
33
using Umbraco.Cms.Core.Models.ContentEditing;
44
using Umbraco.Cms.Core.Models.ContentPublishing;
@@ -54,7 +54,7 @@ public async Task<Attempt<ContentPublishingResult, ContentPublishingOperationSta
5454
}
5555
else
5656
{
57-
schedules.AddOrUpdate(culture, cultureToSchedule.Schedule!.PublishDate.Value.UtcDateTime,ContentScheduleAction.Release);
57+
schedules.AddOrUpdate(culture, cultureToSchedule.Schedule!.PublishDate.Value.UtcDateTime, ContentScheduleAction.Release);
5858
}
5959

6060
if (cultureToSchedule.Schedule!.UnpublishDate is null)
@@ -91,7 +91,7 @@ public async Task<Attempt<ContentPublishingResult, ContentPublishingOperationSta
9191
return Attempt.FailWithStatus(ContentPublishingOperationStatus.ContentNotFound, new ContentPublishingResult());
9292
}
9393

94-
// clear all schedules and publish nothing
94+
// If nothing is requested for publish or scheduling, clear all schedules and publish nothing.
9595
if (cultureAndSchedule.CulturesToPublishImmediately.Count == 0 &&
9696
cultureAndSchedule.Schedules.FullSchedule.Count == 0)
9797
{
@@ -102,11 +102,35 @@ public async Task<Attempt<ContentPublishingResult, ContentPublishingOperationSta
102102
new ContentPublishingResult { Content = content });
103103
}
104104

105+
ISet<string> culturesToPublishImmediately = cultureAndSchedule.CulturesToPublishImmediately;
106+
105107
var cultures =
106-
cultureAndSchedule.CulturesToPublishImmediately.Union(
108+
culturesToPublishImmediately.Union(
107109
cultureAndSchedule.Schedules.FullSchedule.Select(x => x.Culture)).ToArray();
108110

109-
if (content.ContentType.VariesByCulture())
111+
// If cultures are provided for non variant content, and they include the default culture, consider
112+
// the request as valid for publishing the content.
113+
// This is necessary as in a bulk publishing context the cultures are selected and provided from the
114+
// list of languages.
115+
bool variesByCulture = content.ContentType.VariesByCulture();
116+
if (!variesByCulture)
117+
{
118+
ILanguage? defaultLanguage = await _languageService.GetDefaultLanguageAsync();
119+
if (defaultLanguage is not null)
120+
{
121+
if (cultures.Contains(defaultLanguage.IsoCode))
122+
{
123+
cultures = ["*"];
124+
}
125+
126+
if (culturesToPublishImmediately.Contains(defaultLanguage.IsoCode))
127+
{
128+
culturesToPublishImmediately = new HashSet<string> { "*" };
129+
}
130+
}
131+
}
132+
133+
if (variesByCulture)
110134
{
111135
if (cultures.Any() is false)
112136
{
@@ -120,7 +144,7 @@ public async Task<Attempt<ContentPublishingResult, ContentPublishingOperationSta
120144
return Attempt.FailWithStatus(ContentPublishingOperationStatus.CannotPublishInvariantWhenVariant, new ContentPublishingResult());
121145
}
122146

123-
var validCultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode);
147+
IEnumerable<string> validCultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode);
124148
if (validCultures.ContainsAll(cultures) is false)
125149
{
126150
scope.Complete();
@@ -151,9 +175,9 @@ public async Task<Attempt<ContentPublishingResult, ContentPublishingOperationSta
151175
var userId = await _userIdKeyResolver.GetAsync(userKey);
152176

153177
PublishResult? result = null;
154-
if (cultureAndSchedule.CulturesToPublishImmediately.Any())
178+
if (culturesToPublishImmediately.Any())
155179
{
156-
result = _contentService.Publish(content, cultureAndSchedule.CulturesToPublishImmediately.ToArray(), userId);
180+
result = _contentService.Publish(content, culturesToPublishImmediately.ToArray(), userId);
157181
}
158182

159183
if (result?.Success != false && cultureAndSchedule.Schedules.FullSchedule.Any())
@@ -202,10 +226,10 @@ private async Task<ContentValidationResult> ValidateCurrentContentAsync(IContent
202226
Name = content.GetPublishName(culture) ?? string.Empty,
203227
Culture = culture,
204228
Segment = null,
205-
Properties = content.Properties.Where(prop=>prop.PropertyType.VariesByCulture()).Select(prop=> new PropertyValueModel()
229+
Properties = content.Properties.Where(prop => prop.PropertyType.VariesByCulture()).Select(prop => new PropertyValueModel()
206230
{
207231
Alias = prop.Alias,
208-
Value = prop.GetValue(culture: culture, segment:null, published:false)
232+
Value = prop.GetValue(culture: culture, segment: null, published: false)
209233
})
210234
})
211235
};
@@ -272,6 +296,19 @@ public async Task<Attempt<ContentPublishingOperationStatus>> UnpublishAsync(Guid
272296

273297
var userId = await _userIdKeyResolver.GetAsync(userKey);
274298

299+
// If cultures are provided for non variant content, and they include the default culture, consider
300+
// the request as valid for unpublishing the content.
301+
// This is necessary as in a bulk unpublishing context the cultures are selected and provided from the
302+
// list of languages.
303+
if (cultures is not null && !content.ContentType.VariesByCulture())
304+
{
305+
ILanguage? defaultLanguage = await _languageService.GetDefaultLanguageAsync();
306+
if (defaultLanguage is not null && cultures.Contains(defaultLanguage.IsoCode))
307+
{
308+
cultures = null;
309+
}
310+
}
311+
275312
Attempt<ContentPublishingOperationStatus> attempt;
276313
if (cultures is null)
277314
{

src/Umbraco.Web.UI.Client/src/packages/core/modal/common/confirm/confirm-modal.token.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { TemplateResult } from '@umbraco-cms/backoffice/external/lit';
44
export interface UmbConfirmModalData {
55
headline: string;
66
content: TemplateResult | string;
7-
color?: 'positive' | 'danger';
7+
color?: 'positive' | 'danger' | 'warning';
88
cancelLabel?: string;
99
confirmLabel?: string;
1010
}

src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-bulk-action/publish.bulk-action.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase<
6767
data: {
6868
headline: localizationController.term('content_readyToPublish'),
6969
content: localizationController.term('prompt_confirmListViewPublish'),
70-
color: 'danger',
70+
color: 'positive',
7171
confirmLabel: localizationController.term('actions_publish'),
7272
},
7373
})
@@ -77,7 +77,11 @@ export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase<
7777
if (confirm !== false) {
7878
const variantId = new UmbVariantId(options[0].language.unique, null);
7979
const publishingRepository = new UmbDocumentPublishingRepository(this._host);
80-
await publishingRepository.unpublish(this.selection[0], [variantId]);
80+
for (let i = 0; i < this.selection.length; i++) {
81+
const id = this.selection[i];
82+
await publishingRepository.publish(id, [ { variantId }]);
83+
}
84+
8185
eventContext.dispatchEvent(event);
8286
}
8387
return;

src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/modal/document-publish-modal.element.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@ export class UmbDocumentPublishModalElement extends UmbModalBaseElement<
2929
this.#selectionManager.setMultiple(true);
3030
this.#selectionManager.setSelectable(true);
3131

32-
// Only display variants that are relevant to pick from, i.e. variants that are draft, not-published-mandatory or published with pending changes:
32+
// Only display variants that are relevant to pick from, i.e. variants that are draft, not-published-mandatory or published with pending changes.
33+
// If we don't know the state (e.g. from a bulk publishing selection) we need to consider it available for selection.
3334
this._options =
3435
this.data?.options.filter(
35-
(option) => isNotPublishedMandatory(option) || option.variant?.state !== UmbDocumentVariantState.NOT_CREATED,
36+
(option) => (option.variant && option.variant.state === null) ||
37+
isNotPublishedMandatory(option) ||
38+
option.variant?.state !== UmbDocumentVariantState.NOT_CREATED,
3639
) ?? [];
3740

3841
let selected = this.value?.selection ?? [];

src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-bulk-action/unpublish.bulk-action.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export class UmbDocumentUnpublishEntityBulkAction extends UmbEntityBulkActionBas
6666
data: {
6767
headline: localizationController.term('actions_unpublish'),
6868
content: localizationController.term('prompt_confirmListViewUnpublish'),
69-
color: 'danger',
69+
color: 'warning',
7070
confirmLabel: localizationController.term('actions_unpublish'),
7171
},
7272
})
@@ -76,7 +76,11 @@ export class UmbDocumentUnpublishEntityBulkAction extends UmbEntityBulkActionBas
7676
if (confirm !== false) {
7777
const variantId = new UmbVariantId(options[0].language.unique, null);
7878
const publishingRepository = new UmbDocumentPublishingRepository(this._host);
79-
await publishingRepository.unpublish(this.selection[0], [variantId]);
79+
for (let i = 0; i < this.selection.length; i++) {
80+
const id = this.selection[i];
81+
await publishingRepository.unpublish(id, [variantId]);
82+
}
83+
8084
eventContext.dispatchEvent(event);
8185
}
8286
return;

src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/modal/document-unpublish-modal.element.ts

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,29 @@ export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement<
4747
@state()
4848
_hasInvalidSelection = true;
4949

50+
@state()
51+
_isInvariant = false;
52+
5053
override firstUpdated() {
51-
this.#configureSelectionManager();
5254
this.#getReferences();
55+
56+
// If invariant, don't display the variant selection component.
57+
if (this.data?.options.length === 1 && this.data.options[0].unique === "invariant") {
58+
this._isInvariant = true;
59+
this._hasInvalidSelection = false;
60+
return;
61+
}
62+
63+
this.#configureSelectionManager();
5364
}
5465

5566
async #configureSelectionManager() {
5667
this._selectionManager.setMultiple(true);
5768
this._selectionManager.setSelectable(true);
5869

59-
// Only display variants that are relevant to pick from, i.e. variants that are draft or published with pending changes:
60-
this._options = this.data?.options.filter((option) => isPublished(option)) ?? [];
70+
// Only display variants that are relevant to pick from, i.e. variants that are published or published with pending changes.
71+
// If we don't know the state (e.g. from a bulk publishing selection) we need to consider it available for selection.
72+
this._options = this.data?.options.filter((option) => (option.variant && option.variant.state === null) || isPublished(option)) ?? [];
6173

6274
let selected = this.value?.selection ?? [];
6375

@@ -104,7 +116,10 @@ export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement<
104116

105117
#submit() {
106118
if (this._hasUnpublishPermission) {
107-
this.value = { selection: this._selection };
119+
const selection = this._isInvariant
120+
? ["invariant"]
121+
: this._selection;
122+
this.value = { selection };
108123
this.modalContext?.submit();
109124
return;
110125
}
@@ -121,17 +136,21 @@ export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement<
121136

122137
override render() {
123138
return html`<umb-body-layout headline=${this.localize.term('content_unpublish')}>
124-
<p id="subtitle">
125-
<umb-localize key="content_languagesToUnpublish">
126-
Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages.
127-
</umb-localize>
128-
</p>
129139
130-
<umb-document-variant-language-picker
131-
.selectionManager=${this._selectionManager}
132-
.variantLanguageOptions=${this._options}
133-
.requiredFilter=${this._hasInvalidSelection ? this._requiredFilter : undefined}
134-
.pickableFilter=${this.data?.pickableFilter}></umb-document-variant-language-picker>
140+
${!this._isInvariant
141+
? html`
142+
<p id="subtitle">
143+
<umb-localize key="content_languagesToUnpublish">
144+
Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages.
145+
</umb-localize>
146+
</p>
147+
<umb-document-variant-language-picker
148+
.selectionManager=${this._selectionManager}
149+
.variantLanguageOptions=${this._options}
150+
.requiredFilter=${this._hasInvalidSelection ? this._requiredFilter : undefined}
151+
.pickableFilter=${this.data?.pickableFilter}></umb-document-variant-language-picker>
152+
`
153+
: nothing}
135154
136155
<p>
137156
<umb-localize key="prompt_confirmUnpublish">
@@ -159,7 +178,7 @@ export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement<
159178
<uui-button label=${this.localize.term('general_close')} @click=${this.#close}></uui-button>
160179
<uui-button
161180
label="${this.localize.term('actions_unpublish')}"
162-
?disabled=${this._hasInvalidSelection || !this._hasUnpublishPermission || this._selection.length === 0}
181+
?disabled=${this._hasInvalidSelection || !this._hasUnpublishPermission || (!this._isInvariant && this._selection.length === 0)}
163182
look="primary"
164183
color="warning"
165184
@click=${this.#submit}></uui-button>

tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using NUnit.Framework;
1+
using NUnit.Framework;
22
using Umbraco.Cms.Core;
33
using Umbraco.Cms.Core.Models;
44
using Umbraco.Cms.Core.Models.ContentPublishing;
@@ -630,6 +630,28 @@ public async Task Cannot_Publish_Non_Existing_Culture(string cultureCode)
630630
Assert.AreEqual(ContentPublishingOperationStatus.InvalidCulture, result.Status);
631631
}
632632

633+
[Test]
634+
public async Task Can_Publish_Invariant_Content_With_Cultures_Provided_If_The_Default_Culture_Is_Exclusively_Provided()
635+
{
636+
var result = await ContentPublishingService.PublishAsync(Textpage.Key, MakeModel(new HashSet<string>() { "en-US" }), Constants.Security.SuperUserKey);
637+
Assert.IsTrue(result.Success);
638+
}
639+
640+
[Test]
641+
public async Task Can_Publish_Invariant_Content_With_Cultures_Provided_If_The_Default_Culture_Is_Provided_With_Other_Cultures()
642+
{
643+
var result = await ContentPublishingService.PublishAsync(Textpage.Key, MakeModel(new HashSet<string>() { "en-US", "da-DK" }), Constants.Security.SuperUserKey);
644+
Assert.IsTrue(result.Success);
645+
}
646+
647+
[Test]
648+
public async Task Cannot_Publish_Invariant_Content_With_Cultures_Provided_That_Do_Not_Include_The_Default_Culture()
649+
{
650+
var result = await ContentPublishingService.PublishAsync(Textpage.Key, MakeModel(new HashSet<string>() { "da-DK" }), Constants.Security.SuperUserKey);
651+
Assert.IsFalse(result.Success);
652+
Assert.AreEqual(ContentPublishingOperationStatus.InvalidCulture, result.Status);
653+
}
654+
633655
private void AssertBranchResultSuccess(ContentPublishingBranchResult result, params Guid[] expectedKeys)
634656
{
635657
var items = result.SucceededItems.ToArray();

tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Unpublish.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using NUnit.Framework;
1+
using NUnit.Framework;
22
using Umbraco.Cms.Core;
33
using Umbraco.Cms.Core.Models;
44
using Umbraco.Cms.Core.Services.OperationStatus;
@@ -323,4 +323,26 @@ public async Task Cannot_Unpublish_Non_Existing_Culture(string cultureCode)
323323
content = ContentService.GetById(content.Key)!;
324324
Assert.AreEqual(2, content.PublishedCultures.Count());
325325
}
326+
327+
[Test]
328+
public async Task Can_Unpublish_Invariant_Content_With_Cultures_Provided_If_The_Default_Culture_Is_Exclusively_Provided()
329+
{
330+
var result = await ContentPublishingService.UnpublishAsync(Textpage.Key, new HashSet<string>() { "en-US" }, Constants.Security.SuperUserKey);
331+
Assert.IsTrue(result.Success);
332+
}
333+
334+
[Test]
335+
public async Task Can_Unpublish_Invariant_Content_With_Cultures_Provided_If_The_Default_Culture_Is_Provided_With_Other_Cultures()
336+
{
337+
var result = await ContentPublishingService.UnpublishAsync(Textpage.Key, new HashSet<string>() { "en-US", "da-DK" }, Constants.Security.SuperUserKey);
338+
Assert.IsTrue(result.Success);
339+
}
340+
341+
[Test]
342+
public async Task Cannot_Unpublish_Invariant_Content_With_Cultures_Provided_That_Do_Not_Include_The_Default_Culture()
343+
{
344+
var result = await ContentPublishingService.UnpublishAsync(Textpage.Key, new HashSet<string>() { "da-DK" }, Constants.Security.SuperUserKey);
345+
Assert.IsFalse(result.Success);
346+
Assert.AreEqual(ContentPublishingOperationStatus.CannotPublishVariantWhenNotVariant, result.Result);
347+
}
326348
}

0 commit comments

Comments
 (0)