Skip to content

Commit a7bd578

Browse files
authored
Prevent saving or publishing not created variants (#18312)
* prevent actions from handling not created variants * extend pickable filter locally on the modal
1 parent 5f9b192 commit a7bd578

File tree

8 files changed

+126
-44
lines changed

8 files changed

+126
-44
lines changed

src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-workspace-base.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
type UmbEntityVariantModel,
2525
type UmbEntityVariantOptionModel,
2626
} from '@umbraco-cms/backoffice/variant';
27-
import { UmbReadOnlyVariantStateManager } from '@umbraco-cms/backoffice/utils';
27+
import { UmbDeprecation, UmbReadOnlyVariantStateManager } from '@umbraco-cms/backoffice/utils';
2828
import { UmbDataTypeItemRepositoryManager } from '@umbraco-cms/backoffice/data-type';
2929
import { appendToFrozenArray, mergeObservables, UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
3030
import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language';
@@ -481,7 +481,7 @@ export abstract class UmbContentDetailWorkspaceContextBase<
481481
};
482482
}
483483

484-
protected _readOnlyLanguageVariantsFilter = (option: VariantOptionModelType) => {
484+
protected _saveableVariantsFilter = (option: VariantOptionModelType) => {
485485
const readOnlyCultures = this.readOnlyState.getStates().map((s) => s.variantId.culture);
486486
return readOnlyCultures.includes(option.culture) === false;
487487
};
@@ -494,17 +494,28 @@ export abstract class UmbContentDetailWorkspaceContextBase<
494494
* @param {DetailModelType} saveData - The data to validate
495495
* @memberof UmbContentDetailWorkspaceContextBase
496496
*/
497-
protected async _runMandatoryValidationForSaveData(saveData: DetailModelType) {
498-
this.runMandatoryValidationForSaveData(saveData);
497+
protected async _runMandatoryValidationForSaveData(saveData: DetailModelType, variantIds: Array<UmbVariantId> = []) {
498+
new UmbDeprecation({
499+
removeInVersion: '17',
500+
deprecated: '_runMandatoryValidationForSaveData',
501+
solution: 'Use the public runMandatoryValidationForSaveData instead.',
502+
}).warn();
503+
this.runMandatoryValidationForSaveData(saveData, variantIds);
499504
}
500505

501506
/**
502507
* Run the mandatory validation for the save data
503508
* @param {DetailModelType} saveData - The data to validate
504509
* @memberof UmbContentDetailWorkspaceContextBase
505510
*/
506-
public async runMandatoryValidationForSaveData(saveData: DetailModelType) {
511+
public async runMandatoryValidationForSaveData(saveData: DetailModelType, variantIds: Array<UmbVariantId> = []) {
507512
// Check that the data is valid before we save it.
513+
const missingVariants = variantIds.filter((variant) => {
514+
return !saveData.variants.some((y) => variant.compare(y));
515+
});
516+
if (missingVariants.length > 0) {
517+
throw new Error('One or more selected variants have not been created');
518+
}
508519
// Check variants have a name:
509520
const variantsWithoutAName = saveData.variants.filter((x) => !x.name);
510521
if (variantsWithoutAName.length > 0) {
@@ -593,7 +604,7 @@ export abstract class UmbContentDetailWorkspaceContextBase<
593604
.open(this, this.#saveModalToken, {
594605
data: {
595606
options,
596-
pickableFilter: this._readOnlyLanguageVariantsFilter,
607+
pickableFilter: this._saveableVariantsFilter,
597608
},
598609
value: { selection: selected },
599610
})
@@ -608,7 +619,7 @@ export abstract class UmbContentDetailWorkspaceContextBase<
608619
}
609620

610621
const saveData = await this.constructSaveData(variantIds);
611-
await this.runMandatoryValidationForSaveData(saveData);
622+
await this.runMandatoryValidationForSaveData(saveData, variantIds);
612623
if (this.#validateOnSubmit) {
613624
await this.askServerToValidate(saveData, variantIds);
614625
return this.validateAndSubmit(

src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/save-modal/document-save-modal.element.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ export class UmbDocumentSaveModalElement extends UmbModalBaseElement<
1717
@state()
1818
_options: Array<UmbDocumentVariantOptionModel> = [];
1919

20+
#pickableFilter = (option: UmbDocumentVariantOptionModel) => {
21+
if (!option.variant) {
22+
// If not data present, then its not pickable.
23+
return false;
24+
}
25+
return this.data?.pickableFilter ? this.data.pickableFilter(option) : true;
26+
};
27+
2028
override firstUpdated() {
2129
this.#configureSelectionManager();
2230
}
@@ -30,8 +38,10 @@ export class UmbDocumentSaveModalElement extends UmbModalBaseElement<
3038

3139
let selected = this.value?.selection ?? [];
3240

41+
const validOptions = this._options.filter((o) => this.#pickableFilter!(o));
42+
3343
// Filter selection based on options:
34-
selected = selected.filter((s) => this._options.some((o) => o.unique === s));
44+
selected = selected.filter((s) => validOptions.some((o) => o.unique === s));
3545

3646
this.#selectionManager.setSelection(selected);
3747
}
@@ -54,7 +64,7 @@ export class UmbDocumentSaveModalElement extends UmbModalBaseElement<
5464
<umb-document-variant-language-picker
5565
.selectionManager=${this.#selectionManager}
5666
.variantLanguageOptions=${this._options}
57-
.pickableFilter=${this.data?.pickableFilter}></umb-document-variant-language-picker>
67+
.pickableFilter=${this.#pickableFilter}></umb-document-variant-language-picker>
5868
5969
<div slot="actions">
6070
<uui-button label=${this.localize.term('general_close')} @click=${this.#close}></uui-button>

src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/shared/document-variant-language-picker.element.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class UmbDocumentVariantLanguagePickerElement extends UmbLitElement {
4141
_selection: Array<string> = [];
4242

4343
@state()
44-
_isAllSelected?: boolean;
44+
_isAllSelected: boolean = false;
4545

4646
/**
4747
* A filter function that determines if an item is pickableFilter or not.
@@ -62,6 +62,10 @@ export class UmbDocumentVariantLanguagePickerElement extends UmbLitElement {
6262
protected override updated(_changedProperties: PropertyValues): void {
6363
super.updated(_changedProperties);
6464

65+
if (_changedProperties.has('variantLanguageOptions')) {
66+
this._isAllSelected = this.#isAllSelected();
67+
}
68+
6569
if (this.selectionManager && this.pickableFilter) {
6670
this.#selectionManager.setAllowLimitation((unique) => {
6771
const option = this.variantLanguageOptions.find((o) => o.unique === unique);
@@ -86,7 +90,7 @@ export class UmbDocumentVariantLanguagePickerElement extends UmbLitElement {
8690
const allUniques = this.variantLanguageOptions.map((o) => o.unique);
8791
const filter = this.selectionManager.getAllowLimitation();
8892
const allowedUniques = allUniques.filter((unique) => filter(unique));
89-
return this._selection.length === allowedUniques.length;
93+
return this._selection.length !== 0 && this._selection.length === allowedUniques.length;
9094
}
9195

9296
override render() {
@@ -160,16 +164,18 @@ export class UmbDocumentVariantLanguagePickerElement extends UmbLitElement {
160164
.required {
161165
color: var(--uui-color-danger);
162166
--uui-menu-item-color-hover: var(--uui-color-danger-emphasis);
167+
--uui-menu-item-color-disabled: var(--uui-color-danger);
163168
}
164169
.label {
165-
padding: 0.5rem 0;
170+
padding: var(--uui-size-space-3) 0;
166171
}
167172
.label-status {
168-
font-size: 0.8rem;
173+
font-size: var(--uui-type-small-size);
169174
}
170175
171176
uui-menu-item {
172177
--uui-menu-item-flat-structure: 1;
178+
--uui-menu-item-border-radius: var(--uui-border-radius);
173179
}
174180
175181
uui-checkbox {

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE
2626
@state()
2727
_hasNotSelectedMandatory?: boolean;
2828

29+
#pickableFilter = (option: UmbDocumentVariantOptionModel) => {
30+
if (!option.variant) {
31+
// If not data present, then its not pickable.
32+
return false;
33+
}
34+
return this.data?.pickableFilter ? this.data.pickableFilter(option) : true;
35+
};
36+
2937
override firstUpdated() {
3038
this.#configureSelectionManager();
3139
}
@@ -42,8 +50,10 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE
4250

4351
let selected = this.value?.selection ?? [];
4452

53+
const validOptions = this._options.filter((o) => this.#pickableFilter!(o));
54+
4555
// Filter selection based on options:
46-
selected = selected.filter((s) => this._options.some((o) => o.unique === s));
56+
selected = selected.filter((s) => validOptions.some((o) => o.unique === s));
4757

4858
// Additionally select mandatory languages:
4959
// [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.
@@ -105,7 +115,7 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE
105115
.selectionManager=${this.#selectionManager}
106116
.variantLanguageOptions=${this._options}
107117
.requiredFilter=${isNotPublishedMandatory}
108-
.pickableFilter=${this.data?.pickableFilter}></umb-document-variant-language-picker>
118+
.pickableFilter=${this.#pickableFilter}></umb-document-variant-language-picker>
109119
110120
<uui-form-layout-item>
111121
<uui-toggle
@@ -140,7 +150,7 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE
140150
css`
141151
:host {
142152
display: block;
143-
width: 400px;
153+
min-width: 460px;
144154
max-width: 90vw;
145155
}
146156
`,

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ export class UmbDocumentPublishModalElement extends UmbModalBaseElement<
2121
@state()
2222
_hasNotSelectedMandatory?: boolean;
2323

24+
#pickableFilter = (option: UmbDocumentVariantOptionModel) => {
25+
if (!option.variant || option.variant.state === UmbDocumentVariantState.NOT_CREATED) {
26+
return false;
27+
}
28+
return this.data?.pickableFilter ? this.data.pickableFilter(option) : true;
29+
};
30+
2431
override firstUpdated() {
2532
this.#configureSelectionManager();
2633
}
@@ -41,8 +48,10 @@ export class UmbDocumentPublishModalElement extends UmbModalBaseElement<
4148

4249
let selected = this.value?.selection ?? [];
4350

51+
const validOptions = this._options.filter((o) => this.#pickableFilter(o));
52+
4453
// Filter selection based on options:
45-
selected = selected.filter((s) => this._options.some((o) => o.unique === s));
54+
selected = selected.filter((s) => validOptions.some((o) => o.unique === s));
4655

4756
// Additionally select mandatory languages:
4857
// [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.
@@ -87,7 +96,7 @@ export class UmbDocumentPublishModalElement extends UmbModalBaseElement<
8796
.selectionManager=${this.#selectionManager}
8897
.variantLanguageOptions=${this._options}
8998
.requiredFilter=${isNotPublishedMandatory}
90-
.pickableFilter=${this.data?.pickableFilter}></umb-document-variant-language-picker>
99+
.pickableFilter=${this.#pickableFilter}></umb-document-variant-language-picker>
91100
92101
<div slot="actions">
93102
<uui-button label=${this.localize.term('general_close')} @click=${this.#close}></uui-button>
@@ -98,15 +107,15 @@ export class UmbDocumentPublishModalElement extends UmbModalBaseElement<
98107
?disabled=${this._hasNotSelectedMandatory}
99108
@click=${this.#submit}></uui-button>
100109
</div>
101-
</umb-body-layout> `;
110+
</umb-body-layout>`;
102111
}
103112

104113
static override styles = [
105114
UmbTextStyles,
106115
css`
107116
:host {
108117
display: block;
109-
width: 400px;
118+
min-width: 460px;
110119
max-width: 90vw;
111120
}
112121
`,

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

Lines changed: 40 additions & 14 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 { UmbDocumentVariantLanguagePickerElement } from '../../../modals/index.js';
34
import type { UmbDocumentScheduleModalData, UmbDocumentScheduleModalValue } from './document-schedule-modal.token.js';
45
import { css, customElement, html, repeat, state, when } from '@umbraco-cms/backoffice/external/lit';
@@ -18,12 +19,23 @@ export class UmbDocumentScheduleModalElement extends UmbModalBaseElement<
1819
@state()
1920
_options: Array<UmbDocumentVariantOptionModel> = [];
2021

22+
@state()
23+
_hasNotSelectedMandatory?: boolean;
24+
2125
@state()
2226
_selection: UmbDocumentScheduleModalValue['selection'] = [];
2327

2428
@state()
2529
_isAllSelected?: boolean;
2630

31+
#pickableFilter = (option: UmbDocumentVariantOptionModel) => {
32+
if (!option.variant || option.variant.state === UmbDocumentVariantState.NOT_CREATED) {
33+
// If not data present, then its not pickable.
34+
return false;
35+
}
36+
return this.data?.pickableFilter ? this.data.pickableFilter(option) : true;
37+
};
38+
2739
constructor() {
2840
super();
2941
this.observe(
@@ -46,28 +58,41 @@ export class UmbDocumentScheduleModalElement extends UmbModalBaseElement<
4658
this.#selectionManager.setMultiple(true);
4759
this.#selectionManager.setSelectable(true);
4860

49-
const pickableFilter = this.data?.pickableFilter;
61+
this.#selectionManager.setAllowLimitation((unique) => {
62+
const option = this._options.find((o) => o.unique === unique);
63+
return option ? this.#pickableFilter(option) : true;
64+
});
5065

51-
if (pickableFilter) {
52-
this.#selectionManager.setAllowLimitation((unique) => {
53-
const option = this.data?.options.find((o) => o.unique === unique);
54-
return option ? pickableFilter(option) : true;
55-
});
56-
}
57-
58-
// Only display variants that are relevant to pick from, i.e. variants that are draft or published with pending changes:
59-
// 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.)
66+
// Only display variants that are relevant to pick from, i.e. variants that are draft, not-published-mandatory or published with pending changes.
67+
// If we don't know the state (e.g. from a bulk publishing selection) we need to consider it available for selection.
6068
this._options =
6169
this.data?.options.filter(
62-
(option) => option.variant && option.variant.state !== UmbDocumentVariantState.NOT_CREATED,
70+
(option) =>
71+
(option.variant && option.variant.state === null) ||
72+
isNotPublishedMandatory(option) ||
73+
option.variant?.state !== UmbDocumentVariantState.NOT_CREATED,
6374
) ?? [];
6475

76+
const validOptions = this._options.filter((o) => this.#pickableFilter!(o));
77+
6578
let selected = this.value?.selection ?? [];
6679

6780
// Filter selection based on options:
68-
selected = selected.filter((s) => this._options.some((o) => o.unique === s.unique));
81+
selected = selected.filter((s) => validOptions.some((o) => o.unique === s.unique));
6982

7083
this.#selectionManager.setSelection(selected.map((s) => s.unique));
84+
85+
this.observe(
86+
this.#selectionManager.selection,
87+
(selection: Array<string>) => {
88+
if (!this._options && !selection) return;
89+
90+
//Getting not published mandatory options — the options that are mandatory and not currently published.
91+
const missingMandatoryOptions = this._options.filter(isNotPublishedMandatory);
92+
this._hasNotSelectedMandatory = missingMandatoryOptions.some((option) => !selection.includes(option.unique));
93+
},
94+
'observeSelection',
95+
);
7196
}
7297

7398
#submit() {
@@ -99,7 +124,7 @@ export class UmbDocumentScheduleModalElement extends UmbModalBaseElement<
99124
const allUniques = this._options.map((o) => o.unique);
100125
const filter = this.#selectionManager.getAllowLimitation();
101126
const allowedUniques = allUniques.filter((unique) => filter(unique));
102-
return this._selection.length === allowedUniques.length;
127+
return this._selection.length !== 0 && this._selection.length === allowedUniques.length;
103128
}
104129

105130
override render() {
@@ -126,6 +151,7 @@ export class UmbDocumentScheduleModalElement extends UmbModalBaseElement<
126151
label="${this.localize.term('buttons_schedulePublish')}"
127152
look="primary"
128153
color="positive"
154+
?disabled=${this._hasNotSelectedMandatory}
129155
@click=${this.#submit}></uui-button>
130156
</div>
131157
</umb-body-layout> `;
@@ -147,7 +173,7 @@ export class UmbDocumentScheduleModalElement extends UmbModalBaseElement<
147173
}
148174

149175
#renderItem(option: UmbDocumentVariantOptionModel) {
150-
const pickable = this.data?.pickableFilter ? this.data.pickableFilter(option) : () => true;
176+
const pickable = this.#pickableFilter(option);
151177

152178
return html`
153179
<uui-menu-item

0 commit comments

Comments
 (0)