Merge pull request #2408 from umbraco/v15/feature/publish-modal-mandatory
Feature: Handle mandatory languages in publish'ish dialogs
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
"Uncategorized",
|
||||
"uninitialize",
|
||||
"unprovide",
|
||||
"unpublishing",
|
||||
"variantable"
|
||||
],
|
||||
"exportall.config.folderListener": [],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { UmbDocumentVariantState, type UmbDocumentVariantOptionModel } from '../../types.js';
|
||||
import { isNotPublishedMandatory } from '../utils.js';
|
||||
import type { UmbDocumentPublishModalData, UmbDocumentPublishModalValue } from './document-publish-modal.token.js';
|
||||
import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
|
||||
@@ -17,6 +18,9 @@ export class UmbDocumentPublishModalElement extends UmbModalBaseElement<
|
||||
@state()
|
||||
_options: Array<UmbDocumentVariantOptionModel> = [];
|
||||
|
||||
@state()
|
||||
_hasNotSelectedMandatory?: boolean;
|
||||
|
||||
override firstUpdated() {
|
||||
this.#configureSelectionManager();
|
||||
}
|
||||
@@ -25,10 +29,10 @@ 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 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:
|
||||
this._options =
|
||||
this.data?.options.filter(
|
||||
(option) => option.variant && option.variant.state !== UmbDocumentVariantState.NOT_CREATED,
|
||||
(option) => isNotPublishedMandatory(option) || option.variant?.state !== UmbDocumentVariantState.NOT_CREATED,
|
||||
) ?? [];
|
||||
|
||||
let selected = this.value?.selection ?? [];
|
||||
@@ -36,14 +40,29 @@ export class UmbDocumentPublishModalElement extends UmbModalBaseElement<
|
||||
// Filter selection based on options:
|
||||
selected = selected.filter((s) => this._options.some((o) => o.unique === s));
|
||||
|
||||
this.#selectionManager.setSelection(selected);
|
||||
|
||||
// Additionally select mandatory languages:
|
||||
// [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.
|
||||
/*
|
||||
this._options.forEach((variant) => {
|
||||
if (variant.language?.isMandatory) {
|
||||
this.#selectionManager.select(variant.unique);
|
||||
selected.push(variant.unique);
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
this.#selectionManager.setSelection(selected);
|
||||
|
||||
this.observe(
|
||||
this.#selectionManager.selection,
|
||||
(selection: Array<string>) => {
|
||||
if (!this._options && !selection) return;
|
||||
|
||||
//Getting not published mandatory options — the options that are mandatory and not currently published.
|
||||
const missingMandatoryOptions = this._options.filter(isNotPublishedMandatory);
|
||||
this._hasNotSelectedMandatory = missingMandatoryOptions.some((option) => !selection.includes(option.unique));
|
||||
},
|
||||
'observeSelection',
|
||||
);
|
||||
}
|
||||
|
||||
#submit() {
|
||||
@@ -63,6 +82,7 @@ export class UmbDocumentPublishModalElement extends UmbModalBaseElement<
|
||||
<umb-document-variant-language-picker
|
||||
.selectionManager=${this.#selectionManager}
|
||||
.variantLanguageOptions=${this._options}
|
||||
.requiredFilter=${isNotPublishedMandatory}
|
||||
.pickableFilter=${this.data?.pickableFilter}></umb-document-variant-language-picker>
|
||||
|
||||
<div slot="actions">
|
||||
@@ -71,6 +91,7 @@ export class UmbDocumentPublishModalElement extends UmbModalBaseElement<
|
||||
label="${this.localize.term('buttons_saveAndPublish')}"
|
||||
look="primary"
|
||||
color="positive"
|
||||
?disabled=${this._hasNotSelectedMandatory}
|
||||
@click=${this.#submit}></uui-button>
|
||||
</div>
|
||||
</umb-body-layout> `;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { UmbDocumentVariantState, type UmbDocumentVariantOptionModel } from '../../types.js';
|
||||
import { isNotPublishedMandatory } from '../utils.js';
|
||||
import type {
|
||||
UmbDocumentPublishWithDescendantsModalData,
|
||||
UmbDocumentPublishWithDescendantsModalValue,
|
||||
@@ -21,6 +22,9 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE
|
||||
@state()
|
||||
_options: Array<UmbDocumentVariantOptionModel> = [];
|
||||
|
||||
@state()
|
||||
_hasNotSelectedMandatory?: boolean;
|
||||
|
||||
override firstUpdated() {
|
||||
this.#configureSelectionManager();
|
||||
}
|
||||
@@ -29,10 +33,10 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE
|
||||
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:
|
||||
// Only display variants that are relevant to pick from, i.e. variants that are draft, not-published-mandatory or published with pending changes:
|
||||
this._options =
|
||||
this.data?.options.filter(
|
||||
(option) => option.variant && option.variant.state !== UmbDocumentVariantState.NOT_CREATED,
|
||||
(option) => isNotPublishedMandatory(option) || option.variant?.state !== UmbDocumentVariantState.NOT_CREATED,
|
||||
) ?? [];
|
||||
|
||||
let selected = this.value?.selection ?? [];
|
||||
@@ -40,14 +44,29 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE
|
||||
// Filter selection based on options:
|
||||
selected = selected.filter((s) => this._options.some((o) => o.unique === s));
|
||||
|
||||
this.#selectionManager.setSelection(selected);
|
||||
|
||||
// Additionally select mandatory languages:
|
||||
// [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.
|
||||
/*
|
||||
this._options.forEach((variant) => {
|
||||
if (variant.language?.isMandatory) {
|
||||
this.#selectionManager.select(variant.unique);
|
||||
selected.push(variant.unique);
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
this.#selectionManager.setSelection(selected);
|
||||
|
||||
this.observe(
|
||||
this.#selectionManager.selection,
|
||||
(selection: Array<string>) => {
|
||||
if (!this._options && !selection) return;
|
||||
|
||||
//Getting not published mandatory options — the options that are mandatory and not currently published.
|
||||
const missingMandatoryOptions = this._options.filter(isNotPublishedMandatory);
|
||||
this._hasNotSelectedMandatory = missingMandatoryOptions.some((option) => !selection.includes(option.unique));
|
||||
},
|
||||
'observeSelection',
|
||||
);
|
||||
}
|
||||
|
||||
#submit() {
|
||||
@@ -83,6 +102,7 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE
|
||||
<umb-document-variant-language-picker
|
||||
.selectionManager=${this.#selectionManager}
|
||||
.variantLanguageOptions=${this._options}
|
||||
.requiredFilter=${isNotPublishedMandatory}
|
||||
.pickableFilter=${this.data?.pickableFilter}></umb-document-variant-language-picker>
|
||||
|
||||
<uui-form-layout-item>
|
||||
@@ -99,6 +119,7 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE
|
||||
label="${this.localize.term('buttons_publishDescendants')}"
|
||||
look="primary"
|
||||
color="positive"
|
||||
?disabled=${this._hasNotSelectedMandatory}
|
||||
@click=${this.#submit}></uui-button>
|
||||
</div>
|
||||
</umb-body-layout> `;
|
||||
|
||||
@@ -51,6 +51,7 @@ export class UmbDocumentScheduleModalElement extends UmbModalBaseElement<
|
||||
}
|
||||
|
||||
// Only display variants that are relevant to pick from, i.e. variants that are draft or published with pending changes:
|
||||
// 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.)
|
||||
this._options =
|
||||
this.data?.options.filter(
|
||||
(option) => option.variant && option.variant.state !== UmbDocumentVariantState.NOT_CREATED,
|
||||
|
||||
@@ -25,7 +25,7 @@ export class UmbDocumentVariantLanguagePickerElement extends UmbLitElement {
|
||||
this.#selectionManager = value;
|
||||
this.observe(
|
||||
this.selectionManager.selection,
|
||||
async (selection) => {
|
||||
(selection) => {
|
||||
this._selection = selection;
|
||||
},
|
||||
'_selectionManager',
|
||||
@@ -46,6 +46,14 @@ export class UmbDocumentVariantLanguagePickerElement extends UmbLitElement {
|
||||
@property({ attribute: false })
|
||||
public pickableFilter?: (item: UmbDocumentVariantOptionModel) => boolean;
|
||||
|
||||
/**
|
||||
* A filter function that determines if an item should be highlighted as a must select.
|
||||
* @memberof UmbDocumentVariantLanguagePickerElement
|
||||
* @returns {boolean} - True if the item is pickableFilter, false otherwise.
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public requiredFilter?: (item: UmbDocumentVariantOptionModel) => boolean;
|
||||
|
||||
protected override updated(_changedProperties: PropertyValues): void {
|
||||
super.updated(_changedProperties);
|
||||
|
||||
@@ -71,29 +79,32 @@ export class UmbDocumentVariantLanguagePickerElement extends UmbLitElement {
|
||||
|
||||
#renderItem(option: UmbDocumentVariantOptionModel) {
|
||||
const pickable = this.pickableFilter ? this.pickableFilter(option) : () => true;
|
||||
const selected = this._selection.includes(option.unique);
|
||||
const mustSelect = (!selected && this.requiredFilter?.(option)) ?? false;
|
||||
return html`
|
||||
<uui-menu-item
|
||||
class=${mustSelect ? 'required' : ''}
|
||||
?selectable=${pickable}
|
||||
?disabled=${!pickable}
|
||||
label=${option.variant?.name ?? option.language.name}
|
||||
@selected=${() => this.selectionManager.select(option.unique)}
|
||||
@deselected=${() => this.selectionManager.deselect(option.unique)}
|
||||
?selected=${this._selection.includes(option.unique)}>
|
||||
?selected=${selected}>
|
||||
<uui-icon slot="icon" name="icon-globe"></uui-icon>
|
||||
${UmbDocumentVariantLanguagePickerElement.renderLabel(option)}
|
||||
${UmbDocumentVariantLanguagePickerElement.renderLabel(option, mustSelect)}
|
||||
</uui-menu-item>
|
||||
`;
|
||||
}
|
||||
|
||||
static renderLabel(option: UmbDocumentVariantOptionModel) {
|
||||
static renderLabel(option: UmbDocumentVariantOptionModel, mustSelect?: boolean) {
|
||||
return html`<div class="label" slot="label">
|
||||
<strong> ${option.language.name} </strong>
|
||||
<div class="label-status">${UmbDocumentVariantLanguagePickerElement.renderVariantStatus(option)}</div>
|
||||
${option.language.isMandatory && option.variant?.state !== UmbDocumentVariantState.PUBLISHED
|
||||
${option.language.isMandatory && mustSelect
|
||||
? html`<div class="label-status">
|
||||
<umb-localize key="languages_mandatoryLanguage">Mandatory language</umb-localize>
|
||||
</div>`
|
||||
: ''}
|
||||
: nothing}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -106,17 +117,17 @@ export class UmbDocumentVariantLanguagePickerElement extends UmbLitElement {
|
||||
case UmbDocumentVariantState.DRAFT:
|
||||
return html`<umb-localize key="content_unpublished">Draft</umb-localize>`;
|
||||
case UmbDocumentVariantState.NOT_CREATED:
|
||||
return html`<umb-localize key="content_notCreated">Not created</umb-localize>`;
|
||||
default:
|
||||
return nothing;
|
||||
return html`<umb-localize key="content_notCreated">Not created</umb-localize>`;
|
||||
}
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
#subtitle {
|
||||
margin-top: 0;
|
||||
.required {
|
||||
color: var(--uui-color-danger);
|
||||
--uui-menu-item-color-hover: var(--uui-color-danger-emphasis);
|
||||
}
|
||||
.label {
|
||||
padding: 0.5rem 0;
|
||||
|
||||
@@ -12,48 +12,72 @@ import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
import '../shared/document-variant-language-picker.element.js';
|
||||
|
||||
/**
|
||||
* @function isPublished
|
||||
* @param {UmbDocumentVariantOptionModel} option - the option to check.
|
||||
* @returns {boolean} boolean
|
||||
*/
|
||||
export function isPublished(option: UmbDocumentVariantOptionModel): boolean {
|
||||
return (
|
||||
option.variant?.state === UmbDocumentVariantState.PUBLISHED ||
|
||||
option.variant?.state === UmbDocumentVariantState.PUBLISHED_PENDING_CHANGES
|
||||
);
|
||||
}
|
||||
|
||||
@customElement('umb-document-unpublish-modal')
|
||||
export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement<
|
||||
UmbDocumentUnpublishModalData,
|
||||
UmbDocumentUnpublishModalValue
|
||||
> {
|
||||
#selectionManager = new UmbSelectionManager<string>(this);
|
||||
protected readonly _selectionManager = new UmbSelectionManager<string>(this);
|
||||
#referencesRepository = new UmbDocumentReferenceRepository(this);
|
||||
|
||||
@state()
|
||||
_options: Array<UmbDocumentVariantOptionModel> = [];
|
||||
|
||||
@state()
|
||||
_selection: Array<string> = [];
|
||||
|
||||
@state()
|
||||
_hasReferences = false;
|
||||
|
||||
@state()
|
||||
_hasUnpublishPermission = true;
|
||||
|
||||
@state()
|
||||
_hasInvalidSelection = true;
|
||||
|
||||
override firstUpdated() {
|
||||
this.#configureSelectionManager();
|
||||
this.#getReferences();
|
||||
}
|
||||
|
||||
async #configureSelectionManager() {
|
||||
this.#selectionManager.setMultiple(true);
|
||||
this.#selectionManager.setSelectable(true);
|
||||
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) =>
|
||||
option.variant &&
|
||||
(!option.variant.state ||
|
||||
option.variant.state === UmbDocumentVariantState.PUBLISHED ||
|
||||
option.variant.state === UmbDocumentVariantState.PUBLISHED_PENDING_CHANGES),
|
||||
) ?? [];
|
||||
this._options = this.data?.options.filter((option) => isPublished(option)) ?? [];
|
||||
|
||||
let selected = this.value?.selection ?? [];
|
||||
|
||||
// Filter selection based on options:
|
||||
selected = selected.filter((s) => this._options.some((o) => o.unique === s));
|
||||
|
||||
this.#selectionManager.setSelection(selected);
|
||||
this._selectionManager.setSelection(selected);
|
||||
|
||||
this.observe(
|
||||
this._selectionManager.selection,
|
||||
(selection) => {
|
||||
this._selection = selection;
|
||||
const selectionHasMandatory = this._options.some((o) => o.language.isMandatory && selection.includes(o.unique));
|
||||
const selectionDoesNotHaveAllMandatory = this._options.some(
|
||||
(o) => o.language.isMandatory && !selection.includes(o.unique),
|
||||
);
|
||||
this._hasInvalidSelection = selectionHasMandatory && selectionDoesNotHaveAllMandatory;
|
||||
},
|
||||
'observeSelection',
|
||||
);
|
||||
}
|
||||
|
||||
async #getReferences() {
|
||||
@@ -80,7 +104,7 @@ export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement<
|
||||
|
||||
#submit() {
|
||||
if (this._hasUnpublishPermission) {
|
||||
this.value = { selection: this.#selectionManager.getSelection() };
|
||||
this.value = { selection: this._selection };
|
||||
this.modalContext?.submit();
|
||||
return;
|
||||
}
|
||||
@@ -91,6 +115,10 @@ export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement<
|
||||
this.modalContext?.reject();
|
||||
}
|
||||
|
||||
private _requiredFilter = (variantOption: UmbDocumentVariantOptionModel): boolean => {
|
||||
return variantOption.language.isMandatory && !this._selection.includes(variantOption.unique);
|
||||
};
|
||||
|
||||
override render() {
|
||||
return html`<umb-body-layout headline=${this.localize.term('content_unpublish')}>
|
||||
<p id="subtitle">
|
||||
@@ -100,8 +128,9 @@ export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement<
|
||||
</p>
|
||||
|
||||
<umb-document-variant-language-picker
|
||||
.selectionManager=${this.#selectionManager}
|
||||
.selectionManager=${this._selectionManager}
|
||||
.variantLanguageOptions=${this._options}
|
||||
.requiredFilter=${this._hasInvalidSelection ? this._requiredFilter : undefined}
|
||||
.pickableFilter=${this.data?.pickableFilter}></umb-document-variant-language-picker>
|
||||
|
||||
<p>
|
||||
@@ -130,7 +159,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._hasUnpublishPermission || !this.#selectionManager.getSelection().length}
|
||||
?disabled=${this._hasInvalidSelection || !this._hasUnpublishPermission || this._selection.length === 0}
|
||||
look="primary"
|
||||
color="warning"
|
||||
@click=${this.#submit}></uui-button>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { UmbDocumentVariantState, type UmbDocumentVariantOptionModel } from '../types.js';
|
||||
|
||||
/**
|
||||
* @function isNotPublishedMandatory
|
||||
* @param {UmbDocumentVariantOptionModel} option - the option to check.
|
||||
* @returns {boolean} boolean
|
||||
*/
|
||||
export function isNotPublishedMandatory(option: UmbDocumentVariantOptionModel): boolean {
|
||||
return (
|
||||
option.language.isMandatory &&
|
||||
option.variant?.state !== UmbDocumentVariantState.PUBLISHED &&
|
||||
option.variant?.state !== UmbDocumentVariantState.PUBLISHED_PENDING_CHANGES
|
||||
);
|
||||
}
|
||||
@@ -418,9 +418,9 @@ export class UmbDocumentWorkspaceContext
|
||||
|
||||
/**
|
||||
* @function propertyValueByAlias
|
||||
* @param {string} propertyAlias
|
||||
* @param {UmbVariantId} variantId
|
||||
* @returns {Promise<Observable<ReturnType | undefined> | undefined>}
|
||||
* @param {string} propertyAlias - The alias of the property
|
||||
* @param {UmbVariantId} variantId - The variant
|
||||
* @returns {Promise<Observable<ReturnType | undefined> | undefined>} - An observable for the value of the property
|
||||
* @description Get an Observable for the value of this property.
|
||||
*/
|
||||
async propertyValueByAlias<PropertyValueType = unknown>(
|
||||
@@ -436,9 +436,9 @@ export class UmbDocumentWorkspaceContext
|
||||
|
||||
/**
|
||||
* Get the current value of the property with the given alias and variantId.
|
||||
* @param alias
|
||||
* @param variantId
|
||||
* @returns The value or undefined if not set or found.
|
||||
* @param {string} alias - The alias of the property
|
||||
* @param {UmbVariantId | undefined} variantId - The variant id of the property
|
||||
* @returns {ReturnType | undefined} The value or undefined if not set or found.
|
||||
*/
|
||||
getPropertyValue<ReturnType = unknown>(alias: string, variantId?: UmbVariantId) {
|
||||
const currentData = this.#data.getCurrent();
|
||||
@@ -489,23 +489,21 @@ export class UmbDocumentWorkspaceContext
|
||||
};
|
||||
|
||||
async #determineVariantOptions() {
|
||||
const activeVariants = this.splitView.getActiveVariants();
|
||||
|
||||
const activeVariantIds = activeVariants.map((activeVariant) => UmbVariantId.Create(activeVariant));
|
||||
// TODO: We need to filter the selected array, so it only contains one of each variantId. [NL]
|
||||
const changedVariantIds = this.#data.getChangedVariants();
|
||||
const selected = activeVariantIds.concat(changedVariantIds);
|
||||
// Selected can contain entries that are not part of the options, therefor the modal filters selection based on options.
|
||||
|
||||
const readOnlyCultures = this.readOnlyState.getStates().map((s) => s.variantId.culture);
|
||||
const selectedCultures = selected.map((x) => x.toString()).filter((v, i, a) => a.indexOf(v) === i);
|
||||
const writable = selectedCultures.filter((x) => readOnlyCultures.includes(x) === false);
|
||||
|
||||
const options = await firstValueFrom(this.variantOptions);
|
||||
|
||||
const activeVariants = this.splitView.getActiveVariants();
|
||||
const activeVariantIds = activeVariants.map((activeVariant) => UmbVariantId.Create(activeVariant));
|
||||
const changedVariantIds = this.#data.getChangedVariants();
|
||||
const selectedVariantIds = activeVariantIds.concat(changedVariantIds);
|
||||
|
||||
// Selected can contain entries that are not part of the options, therefor the modal filters selection based on options.
|
||||
const readOnlyCultures = this.readOnlyState.getStates().map((s) => s.variantId.culture);
|
||||
let selected = selectedVariantIds.map((x) => x.toString()).filter((v, i, a) => a.indexOf(v) === i);
|
||||
selected = selected.filter((x) => readOnlyCultures.includes(x) === false);
|
||||
|
||||
return {
|
||||
options,
|
||||
selected: writable,
|
||||
selected,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -796,6 +794,8 @@ export class UmbDocumentWorkspaceContext
|
||||
|
||||
if (!variants.length) return;
|
||||
|
||||
// TODO: Validate content & Save changes for the selected variants — This was how it worked in v.13 [NL]
|
||||
|
||||
const unique = this.getUnique();
|
||||
if (!unique) throw new Error('Unique is missing');
|
||||
await this.publishingRepository.publish(unique, variants);
|
||||
@@ -833,6 +833,8 @@ export class UmbDocumentWorkspaceContext
|
||||
|
||||
if (!variantIds.length) return;
|
||||
|
||||
// TODO: Validate content & Save changes for the selected variants — This was how it worked in v.13 [NL]
|
||||
|
||||
const unique = this.getUnique();
|
||||
if (!unique) throw new Error('Unique is missing');
|
||||
await this.publishingRepository.publishWithDescendants(
|
||||
|
||||
Reference in New Issue
Block a user