Skip to content

Commit f7119a4

Browse files
authored
Merge pull request #2408 from umbraco/v15/feature/publish-modal-mandatory
Feature: Handle mandatory languages in publish'ish dialogs
2 parents f12472e + fe6a5f0 commit f7119a4

File tree

8 files changed

+150
-50
lines changed

8 files changed

+150
-50
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"Uncategorized",
2727
"uninitialize",
2828
"unprovide",
29+
"unpublishing",
2930
"variantable"
3031
],
3132
"exportall.config.folderListener": [],

src/packages/documents/documents/modals/publish-modal/document-publish-modal.element.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { UmbDocumentVariantState, type UmbDocumentVariantOptionModel } from '../../types.js';
2+
import { isNotPublishedMandatory } from '../utils.js';
23
import type { UmbDocumentPublishModalData, UmbDocumentPublishModalValue } from './document-publish-modal.token.js';
34
import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit';
45
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
@@ -17,6 +18,9 @@ export class UmbDocumentPublishModalElement extends UmbModalBaseElement<
1718
@state()
1819
_options: Array<UmbDocumentVariantOptionModel> = [];
1920

21+
@state()
22+
_hasNotSelectedMandatory?: boolean;
23+
2024
override firstUpdated() {
2125
this.#configureSelectionManager();
2226
}
@@ -25,25 +29,40 @@ export class UmbDocumentPublishModalElement extends UmbModalBaseElement<
2529
this.#selectionManager.setMultiple(true);
2630
this.#selectionManager.setSelectable(true);
2731

28-
// Only display variants that are relevant to pick from, i.e. variants that are draft 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:
2933
this._options =
3034
this.data?.options.filter(
31-
(option) => option.variant && option.variant.state !== UmbDocumentVariantState.NOT_CREATED,
35+
(option) => isNotPublishedMandatory(option) || option.variant?.state !== UmbDocumentVariantState.NOT_CREATED,
3236
) ?? [];
3337

3438
let selected = this.value?.selection ?? [];
3539

3640
// Filter selection based on options:
3741
selected = selected.filter((s) => this._options.some((o) => o.unique === s));
3842

39-
this.#selectionManager.setSelection(selected);
40-
4143
// Additionally select mandatory languages:
44+
// [NL]: I think for now lets make it an active choice to select the languages. If you just made them, they would be selected. So it just to underline the act of actually selecting these languages.
45+
/*
4246
this._options.forEach((variant) => {
4347
if (variant.language?.isMandatory) {
44-
this.#selectionManager.select(variant.unique);
48+
selected.push(variant.unique);
4549
}
4650
});
51+
*/
52+
53+
this.#selectionManager.setSelection(selected);
54+
55+
this.observe(
56+
this.#selectionManager.selection,
57+
(selection: Array<string>) => {
58+
if (!this._options && !selection) return;
59+
60+
//Getting not published mandatory options — the options that are mandatory and not currently published.
61+
const missingMandatoryOptions = this._options.filter(isNotPublishedMandatory);
62+
this._hasNotSelectedMandatory = missingMandatoryOptions.some((option) => !selection.includes(option.unique));
63+
},
64+
'observeSelection',
65+
);
4766
}
4867

4968
#submit() {
@@ -63,6 +82,7 @@ export class UmbDocumentPublishModalElement extends UmbModalBaseElement<
6382
<umb-document-variant-language-picker
6483
.selectionManager=${this.#selectionManager}
6584
.variantLanguageOptions=${this._options}
85+
.requiredFilter=${isNotPublishedMandatory}
6686
.pickableFilter=${this.data?.pickableFilter}></umb-document-variant-language-picker>
6787
6888
<div slot="actions">
@@ -71,6 +91,7 @@ export class UmbDocumentPublishModalElement extends UmbModalBaseElement<
7191
label="${this.localize.term('buttons_saveAndPublish')}"
7292
look="primary"
7393
color="positive"
94+
?disabled=${this._hasNotSelectedMandatory}
7495
@click=${this.#submit}></uui-button>
7596
</div>
7697
</umb-body-layout> `;

src/packages/documents/documents/modals/publish-with-descendants-modal/document-publish-with-descendants-modal.element.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { UmbDocumentVariantState, type UmbDocumentVariantOptionModel } from '../../types.js';
2+
import { isNotPublishedMandatory } from '../utils.js';
23
import type {
34
UmbDocumentPublishWithDescendantsModalData,
45
UmbDocumentPublishWithDescendantsModalValue,
@@ -21,6 +22,9 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE
2122
@state()
2223
_options: Array<UmbDocumentVariantOptionModel> = [];
2324

25+
@state()
26+
_hasNotSelectedMandatory?: boolean;
27+
2428
override firstUpdated() {
2529
this.#configureSelectionManager();
2630
}
@@ -29,25 +33,40 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE
2933
this.#selectionManager.setMultiple(true);
3034
this.#selectionManager.setSelectable(true);
3135

32-
// Only display variants that are relevant to pick from, i.e. variants that are draft or published with pending changes:
36+
// Only display variants that are relevant to pick from, i.e. variants that are draft, not-published-mandatory or published with pending changes:
3337
this._options =
3438
this.data?.options.filter(
35-
(option) => option.variant && option.variant.state !== UmbDocumentVariantState.NOT_CREATED,
39+
(option) => isNotPublishedMandatory(option) || option.variant?.state !== UmbDocumentVariantState.NOT_CREATED,
3640
) ?? [];
3741

3842
let selected = this.value?.selection ?? [];
3943

4044
// Filter selection based on options:
4145
selected = selected.filter((s) => this._options.some((o) => o.unique === s));
4246

43-
this.#selectionManager.setSelection(selected);
44-
4547
// Additionally select mandatory languages:
48+
// [NL]: I think for now lets make it an active choice to select the languages. If you just made them, they would be selected. So it just to underline the act of actually selecting these languages.
49+
/*
4650
this._options.forEach((variant) => {
4751
if (variant.language?.isMandatory) {
48-
this.#selectionManager.select(variant.unique);
52+
selected.push(variant.unique);
4953
}
5054
});
55+
*/
56+
57+
this.#selectionManager.setSelection(selected);
58+
59+
this.observe(
60+
this.#selectionManager.selection,
61+
(selection: Array<string>) => {
62+
if (!this._options && !selection) return;
63+
64+
//Getting not published mandatory options — the options that are mandatory and not currently published.
65+
const missingMandatoryOptions = this._options.filter(isNotPublishedMandatory);
66+
this._hasNotSelectedMandatory = missingMandatoryOptions.some((option) => !selection.includes(option.unique));
67+
},
68+
'observeSelection',
69+
);
5170
}
5271

5372
#submit() {
@@ -83,6 +102,7 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE
83102
<umb-document-variant-language-picker
84103
.selectionManager=${this.#selectionManager}
85104
.variantLanguageOptions=${this._options}
105+
.requiredFilter=${isNotPublishedMandatory}
86106
.pickableFilter=${this.data?.pickableFilter}></umb-document-variant-language-picker>
87107
88108
<uui-form-layout-item>
@@ -99,6 +119,7 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE
99119
label="${this.localize.term('buttons_publishDescendants')}"
100120
look="primary"
101121
color="positive"
122+
?disabled=${this._hasNotSelectedMandatory}
102123
@click=${this.#submit}></uui-button>
103124
</div>
104125
</umb-body-layout> `;

src/packages/documents/documents/modals/schedule-modal/document-schedule-modal.element.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export class UmbDocumentScheduleModalElement extends UmbModalBaseElement<
5151
}
5252

5353
// Only display variants that are relevant to pick from, i.e. variants that are draft or published with pending changes:
54+
// TODO:[NL] I would say we should change this, the act of scheduling should be equivalent to save & publishing. Resulting in content begin saved as part of carrying out the action. (But this requires a update in the workspace.)
5455
this._options =
5556
this.data?.options.filter(
5657
(option) => option.variant && option.variant.state !== UmbDocumentVariantState.NOT_CREATED,

src/packages/documents/documents/modals/shared/document-variant-language-picker.element.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class UmbDocumentVariantLanguagePickerElement extends UmbLitElement {
2525
this.#selectionManager = value;
2626
this.observe(
2727
this.selectionManager.selection,
28-
async (selection) => {
28+
(selection) => {
2929
this._selection = selection;
3030
},
3131
'_selectionManager',
@@ -46,6 +46,14 @@ export class UmbDocumentVariantLanguagePickerElement extends UmbLitElement {
4646
@property({ attribute: false })
4747
public pickableFilter?: (item: UmbDocumentVariantOptionModel) => boolean;
4848

49+
/**
50+
* A filter function that determines if an item should be highlighted as a must select.
51+
* @memberof UmbDocumentVariantLanguagePickerElement
52+
* @returns {boolean} - True if the item is pickableFilter, false otherwise.
53+
*/
54+
@property({ attribute: false })
55+
public requiredFilter?: (item: UmbDocumentVariantOptionModel) => boolean;
56+
4957
protected override updated(_changedProperties: PropertyValues): void {
5058
super.updated(_changedProperties);
5159

@@ -71,29 +79,32 @@ export class UmbDocumentVariantLanguagePickerElement extends UmbLitElement {
7179

7280
#renderItem(option: UmbDocumentVariantOptionModel) {
7381
const pickable = this.pickableFilter ? this.pickableFilter(option) : () => true;
82+
const selected = this._selection.includes(option.unique);
83+
const mustSelect = (!selected && this.requiredFilter?.(option)) ?? false;
7484
return html`
7585
<uui-menu-item
86+
class=${mustSelect ? 'required' : ''}
7687
?selectable=${pickable}
7788
?disabled=${!pickable}
7889
label=${option.variant?.name ?? option.language.name}
7990
@selected=${() => this.selectionManager.select(option.unique)}
8091
@deselected=${() => this.selectionManager.deselect(option.unique)}
81-
?selected=${this._selection.includes(option.unique)}>
92+
?selected=${selected}>
8293
<uui-icon slot="icon" name="icon-globe"></uui-icon>
83-
${UmbDocumentVariantLanguagePickerElement.renderLabel(option)}
94+
${UmbDocumentVariantLanguagePickerElement.renderLabel(option, mustSelect)}
8495
</uui-menu-item>
8596
`;
8697
}
8798

88-
static renderLabel(option: UmbDocumentVariantOptionModel) {
99+
static renderLabel(option: UmbDocumentVariantOptionModel, mustSelect?: boolean) {
89100
return html`<div class="label" slot="label">
90101
<strong> ${option.language.name} </strong>
91102
<div class="label-status">${UmbDocumentVariantLanguagePickerElement.renderVariantStatus(option)}</div>
92-
${option.language.isMandatory && option.variant?.state !== UmbDocumentVariantState.PUBLISHED
103+
${option.language.isMandatory && mustSelect
93104
? html`<div class="label-status">
94105
<umb-localize key="languages_mandatoryLanguage">Mandatory language</umb-localize>
95106
</div>`
96-
: ''}
107+
: nothing}
97108
</div>`;
98109
}
99110

@@ -106,17 +117,17 @@ export class UmbDocumentVariantLanguagePickerElement extends UmbLitElement {
106117
case UmbDocumentVariantState.DRAFT:
107118
return html`<umb-localize key="content_unpublished">Draft</umb-localize>`;
108119
case UmbDocumentVariantState.NOT_CREATED:
109-
return html`<umb-localize key="content_notCreated">Not created</umb-localize>`;
110120
default:
111-
return nothing;
121+
return html`<umb-localize key="content_notCreated">Not created</umb-localize>`;
112122
}
113123
}
114124

115125
static override styles = [
116126
UmbTextStyles,
117127
css`
118-
#subtitle {
119-
margin-top: 0;
128+
.required {
129+
color: var(--uui-color-danger);
130+
--uui-menu-item-color-hover: var(--uui-color-danger-emphasis);
120131
}
121132
.label {
122133
padding: 0.5rem 0;

src/packages/documents/documents/modals/unpublish-modal/document-unpublish-modal.element.ts

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,48 +12,72 @@ import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
1212

1313
import '../shared/document-variant-language-picker.element.js';
1414

15+
/**
16+
* @function isPublished
17+
* @param {UmbDocumentVariantOptionModel} option - the option to check.
18+
* @returns {boolean} boolean
19+
*/
20+
export function isPublished(option: UmbDocumentVariantOptionModel): boolean {
21+
return (
22+
option.variant?.state === UmbDocumentVariantState.PUBLISHED ||
23+
option.variant?.state === UmbDocumentVariantState.PUBLISHED_PENDING_CHANGES
24+
);
25+
}
26+
1527
@customElement('umb-document-unpublish-modal')
1628
export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement<
1729
UmbDocumentUnpublishModalData,
1830
UmbDocumentUnpublishModalValue
1931
> {
20-
#selectionManager = new UmbSelectionManager<string>(this);
32+
protected readonly _selectionManager = new UmbSelectionManager<string>(this);
2133
#referencesRepository = new UmbDocumentReferenceRepository(this);
2234

2335
@state()
2436
_options: Array<UmbDocumentVariantOptionModel> = [];
2537

38+
@state()
39+
_selection: Array<string> = [];
40+
2641
@state()
2742
_hasReferences = false;
2843

2944
@state()
3045
_hasUnpublishPermission = true;
3146

47+
@state()
48+
_hasInvalidSelection = true;
49+
3250
override firstUpdated() {
3351
this.#configureSelectionManager();
3452
this.#getReferences();
3553
}
3654

3755
async #configureSelectionManager() {
38-
this.#selectionManager.setMultiple(true);
39-
this.#selectionManager.setSelectable(true);
56+
this._selectionManager.setMultiple(true);
57+
this._selectionManager.setSelectable(true);
4058

4159
// Only display variants that are relevant to pick from, i.e. variants that are draft or published with pending changes:
42-
this._options =
43-
this.data?.options.filter(
44-
(option) =>
45-
option.variant &&
46-
(!option.variant.state ||
47-
option.variant.state === UmbDocumentVariantState.PUBLISHED ||
48-
option.variant.state === UmbDocumentVariantState.PUBLISHED_PENDING_CHANGES),
49-
) ?? [];
60+
this._options = this.data?.options.filter((option) => isPublished(option)) ?? [];
5061

5162
let selected = this.value?.selection ?? [];
5263

5364
// Filter selection based on options:
5465
selected = selected.filter((s) => this._options.some((o) => o.unique === s));
5566

56-
this.#selectionManager.setSelection(selected);
67+
this._selectionManager.setSelection(selected);
68+
69+
this.observe(
70+
this._selectionManager.selection,
71+
(selection) => {
72+
this._selection = selection;
73+
const selectionHasMandatory = this._options.some((o) => o.language.isMandatory && selection.includes(o.unique));
74+
const selectionDoesNotHaveAllMandatory = this._options.some(
75+
(o) => o.language.isMandatory && !selection.includes(o.unique),
76+
);
77+
this._hasInvalidSelection = selectionHasMandatory && selectionDoesNotHaveAllMandatory;
78+
},
79+
'observeSelection',
80+
);
5781
}
5882

5983
async #getReferences() {
@@ -80,7 +104,7 @@ export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement<
80104

81105
#submit() {
82106
if (this._hasUnpublishPermission) {
83-
this.value = { selection: this.#selectionManager.getSelection() };
107+
this.value = { selection: this._selection };
84108
this.modalContext?.submit();
85109
return;
86110
}
@@ -91,6 +115,10 @@ export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement<
91115
this.modalContext?.reject();
92116
}
93117

118+
private _requiredFilter = (variantOption: UmbDocumentVariantOptionModel): boolean => {
119+
return variantOption.language.isMandatory && !this._selection.includes(variantOption.unique);
120+
};
121+
94122
override render() {
95123
return html`<umb-body-layout headline=${this.localize.term('content_unpublish')}>
96124
<p id="subtitle">
@@ -100,8 +128,9 @@ export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement<
100128
</p>
101129
102130
<umb-document-variant-language-picker
103-
.selectionManager=${this.#selectionManager}
131+
.selectionManager=${this._selectionManager}
104132
.variantLanguageOptions=${this._options}
133+
.requiredFilter=${this._hasInvalidSelection ? this._requiredFilter : undefined}
105134
.pickableFilter=${this.data?.pickableFilter}></umb-document-variant-language-picker>
106135
107136
<p>
@@ -130,7 +159,7 @@ export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement<
130159
<uui-button label=${this.localize.term('general_close')} @click=${this.#close}></uui-button>
131160
<uui-button
132161
label="${this.localize.term('actions_unpublish')}"
133-
?disabled=${!this._hasUnpublishPermission || !this.#selectionManager.getSelection().length}
162+
?disabled=${this._hasInvalidSelection || !this._hasUnpublishPermission || this._selection.length === 0}
134163
look="primary"
135164
color="warning"
136165
@click=${this.#submit}></uui-button>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { UmbDocumentVariantState, type UmbDocumentVariantOptionModel } from '../types.js';
2+
3+
/**
4+
* @function isNotPublishedMandatory
5+
* @param {UmbDocumentVariantOptionModel} option - the option to check.
6+
* @returns {boolean} boolean
7+
*/
8+
export function isNotPublishedMandatory(option: UmbDocumentVariantOptionModel): boolean {
9+
return (
10+
option.language.isMandatory &&
11+
option.variant?.state !== UmbDocumentVariantState.PUBLISHED &&
12+
option.variant?.state !== UmbDocumentVariantState.PUBLISHED_PENDING_CHANGES
13+
);
14+
}

0 commit comments

Comments
 (0)