Merge pull request #18109 from umbraco/v15/bugfix/media-picker-mandatory-validation
Fix: media picker mandatory validation
This commit is contained in:
@@ -28,7 +28,6 @@ import { UmbRoutePathAddendumContext } from '@umbraco-cms/backoffice/router';
|
||||
* The Element will render a Property Editor based on the Property Editor UI alias passed to the element.
|
||||
* This will also render all Property Actions related to the Property Editor UI Alias.
|
||||
*/
|
||||
|
||||
@customElement('umb-property')
|
||||
export class UmbPropertyElement extends UmbLitElement {
|
||||
/**
|
||||
@@ -178,6 +177,7 @@ export class UmbPropertyElement extends UmbLitElement {
|
||||
#validationMessageBinder?: UmbBindServerValidationToFormControl;
|
||||
#valueObserver?: UmbObserverController<unknown>;
|
||||
#configObserver?: UmbObserverController<UmbPropertyEditorConfigCollection | undefined>;
|
||||
#validationMessageObserver?: UmbObserverController<string | undefined>;
|
||||
#extensionsController?: UmbExtensionsApiInitializer<any>;
|
||||
|
||||
constructor() {
|
||||
@@ -293,6 +293,7 @@ export class UmbPropertyElement extends UmbLitElement {
|
||||
// cleanup:
|
||||
this.#valueObserver?.destroy();
|
||||
this.#configObserver?.destroy();
|
||||
this.#validationMessageObserver?.destroy();
|
||||
this.#controlValidator?.destroy();
|
||||
oldElement?.removeEventListener('change', this._onPropertyEditorChange as any as EventListener);
|
||||
oldElement?.removeEventListener('property-value-change', this._onPropertyEditorChange as any as EventListener);
|
||||
@@ -330,7 +331,7 @@ export class UmbPropertyElement extends UmbLitElement {
|
||||
},
|
||||
null,
|
||||
);
|
||||
this.#configObserver = this.observe(
|
||||
this.#validationMessageObserver = this.observe(
|
||||
this.#propertyContext.validationMandatoryMessage,
|
||||
(mandatoryMessage) => {
|
||||
if (mandatoryMessage) {
|
||||
|
||||
@@ -76,8 +76,8 @@ export class UmbRepositoryItemsManager<ItemType extends { unique: string }> exte
|
||||
return this.#uniques.getValue();
|
||||
}
|
||||
|
||||
setUniques(uniques: string[]): void {
|
||||
this.#uniques.setValue(uniques);
|
||||
setUniques(uniques: string[] | undefined): void {
|
||||
this.#uniques.setValue(uniques ?? []);
|
||||
}
|
||||
|
||||
getItems(): Array<ItemType> {
|
||||
|
||||
@@ -221,7 +221,7 @@ export type UmbSorterConfig<T, ElementType extends HTMLElement = HTMLElement> =
|
||||
Partial<Pick<INTERNAL_UmbSorterConfig<T, ElementType>, 'ignorerSelector' | 'containerSelector' | 'identifier'>>;
|
||||
|
||||
/**
|
||||
|
||||
|
||||
* @class UmbSorterController
|
||||
* @implements {UmbControllerInterface}
|
||||
* @description This controller can make user able to sort items.
|
||||
@@ -346,10 +346,8 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
|
||||
}
|
||||
}
|
||||
|
||||
setModel(model: Array<T>): void {
|
||||
if (this.#model) {
|
||||
this.#model = model;
|
||||
}
|
||||
setModel(model: Array<T> | undefined): void {
|
||||
this.#model = model ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -168,7 +168,6 @@ export function UmbFormControlMixin<
|
||||
/*if (e.composedPath().some((x) => x === this)) {
|
||||
return;
|
||||
}*/
|
||||
this.pristine = false;
|
||||
this.checkValidity();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { UmbMediaItemRepository } from '../../repository/index.js';
|
||||
import { UMB_IMAGE_CROPPER_EDITOR_MODAL, UMB_MEDIA_PICKER_MODAL } from '../../modals/index.js';
|
||||
import type { UmbMediaItemModel, UmbCropModel, UmbMediaPickerPropertyValueEntry } from '../../types.js';
|
||||
import type { UmbUploadableItem } from '../../dropzone/types.js';
|
||||
@@ -9,12 +8,13 @@ import { UmbId } from '@umbraco-cms/backoffice/id';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
|
||||
import { UmbSorterController, UmbSorterResolvePlacementAsGrid } from '@umbraco-cms/backoffice/sorter';
|
||||
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
|
||||
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
|
||||
import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router';
|
||||
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
|
||||
|
||||
import '@umbraco-cms/backoffice/imaging';
|
||||
import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
|
||||
import { UmbRepositoryItemsManager } from '@umbraco-cms/backoffice/repository';
|
||||
import { UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from '@umbraco-cms/backoffice/media';
|
||||
|
||||
type UmbRichMediaCardModel = {
|
||||
unique: string;
|
||||
@@ -26,7 +26,11 @@ type UmbRichMediaCardModel = {
|
||||
};
|
||||
|
||||
@customElement('umb-input-rich-media')
|
||||
export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, '') {
|
||||
export class UmbInputRichMediaElement extends UmbFormControlMixin<
|
||||
Array<UmbMediaPickerPropertyValueEntry>,
|
||||
typeof UmbLitElement,
|
||||
undefined
|
||||
>(UmbLitElement, undefined) {
|
||||
#sorter = new UmbSorterController<UmbMediaPickerPropertyValueEntry>(this, {
|
||||
getUniqueOfElement: (element) => {
|
||||
return element.id;
|
||||
@@ -37,24 +41,22 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement,
|
||||
identifier: 'Umb.SorterIdentifier.InputRichMedia',
|
||||
itemSelector: 'uui-card-media',
|
||||
containerSelector: '.container',
|
||||
//resolvePlacement: (args) => args.pointerX < args.relatedRect.left + args.relatedRect.width * 0.5,
|
||||
resolvePlacement: UmbSorterResolvePlacementAsGrid,
|
||||
onChange: ({ model }) => {
|
||||
this.#items = model;
|
||||
this.#sortCards(model);
|
||||
this.value = model;
|
||||
this.dispatchEvent(new UmbChangeEvent());
|
||||
},
|
||||
});
|
||||
|
||||
#sortCards(model: Array<UmbMediaPickerPropertyValueEntry>) {
|
||||
const idToIndexMap: { [unique: string]: number } = {};
|
||||
model.forEach((item, index) => {
|
||||
idToIndexMap[item.key] = index;
|
||||
});
|
||||
/**
|
||||
* Sets the input to required, meaning validation will fail if the value is empty.
|
||||
* @type {boolean}
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
required?: boolean;
|
||||
|
||||
const cards = [...this._cards];
|
||||
this._cards = cards.sort((a, b) => idToIndexMap[a.unique] - idToIndexMap[b.unique]);
|
||||
}
|
||||
@property({ type: String })
|
||||
requiredMessage?: string;
|
||||
|
||||
/**
|
||||
* This is a minimum amount of selected items in this input.
|
||||
@@ -93,15 +95,16 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement,
|
||||
maxMessage = 'This field exceeds the allowed amount of items';
|
||||
|
||||
@property({ type: Array })
|
||||
public set items(value: Array<UmbMediaPickerPropertyValueEntry>) {
|
||||
public override set value(value: Array<UmbMediaPickerPropertyValueEntry> | undefined) {
|
||||
super.value = value;
|
||||
this.#sorter.setModel(value);
|
||||
this.#items = value;
|
||||
this.#itemManager.setUniques(value?.map((x) => x.mediaKey));
|
||||
// Maybe the new value is using an existing media, and there we need to update the cards despite no repository update.
|
||||
this.#populateCards();
|
||||
}
|
||||
public get items(): Array<UmbMediaPickerPropertyValueEntry> {
|
||||
return this.#items;
|
||||
public override get value(): Array<UmbMediaPickerPropertyValueEntry> | undefined {
|
||||
return super.value;
|
||||
}
|
||||
#items: Array<UmbMediaPickerPropertyValueEntry> = [];
|
||||
|
||||
@property({ type: Array })
|
||||
allowedContentTypeIds?: string[] | undefined;
|
||||
@@ -112,11 +115,6 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement,
|
||||
@property({ type: Boolean })
|
||||
multiple = false;
|
||||
|
||||
@property()
|
||||
public override get value() {
|
||||
return this.items?.map((item) => item.mediaKey).join(',');
|
||||
}
|
||||
|
||||
@property({ type: Array })
|
||||
public preselectedCrops?: Array<UmbCropModel>;
|
||||
|
||||
@@ -174,15 +172,17 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement,
|
||||
@state()
|
||||
private _routeBuilder?: UmbModalRouteBuilder;
|
||||
|
||||
#itemRepository = new UmbMediaItemRepository(this);
|
||||
|
||||
#modalManager?: UmbModalManagerContext;
|
||||
readonly #itemManager = new UmbRepositoryItemsManager<UmbMediaItemModel>(
|
||||
this,
|
||||
UMB_MEDIA_ITEM_REPOSITORY_ALIAS,
|
||||
(x) => x.unique,
|
||||
);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
|
||||
this.#modalManager = instance;
|
||||
this.observe(this.#itemManager.items, () => {
|
||||
this.#populateCards();
|
||||
});
|
||||
|
||||
new UmbModalRouteRegistrationController(this, UMB_IMAGE_CROPPER_EDITOR_MODAL)
|
||||
@@ -191,7 +191,7 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement,
|
||||
const key = params.key;
|
||||
if (!key) return false;
|
||||
|
||||
const item = this.items.find((item) => item.key === key);
|
||||
const item = this.value?.find((item) => item.key === key);
|
||||
if (!item) return false;
|
||||
|
||||
return {
|
||||
@@ -212,7 +212,7 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement,
|
||||
};
|
||||
})
|
||||
.onSubmit((value) => {
|
||||
this.items = this.items.map((item) => {
|
||||
this.value = this.value?.map((item) => {
|
||||
if (item.key !== value.key) return item;
|
||||
|
||||
const focalPoint = this.focalPointEnabled ? value.focalPoint : null;
|
||||
@@ -231,15 +231,30 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement,
|
||||
this._routeBuilder = routeBuilder;
|
||||
});
|
||||
|
||||
this.addValidator(
|
||||
'valueMissing',
|
||||
() => this.requiredMessage ?? UMB_VALIDATION_EMPTY_LOCALIZATION_KEY,
|
||||
() => {
|
||||
return !this.readonly && !!this.required && (!this.value || this.value.length === 0);
|
||||
},
|
||||
);
|
||||
|
||||
this.addValidator(
|
||||
'rangeUnderflow',
|
||||
() => this.minMessage,
|
||||
() => !!this.min && this.items?.length < this.min,
|
||||
() =>
|
||||
!this.readonly &&
|
||||
// Only if min is set:
|
||||
!!this.min &&
|
||||
// if the value is empty and not required, we should not validate the min:
|
||||
!(this.value?.length === 0 && this.required == false) &&
|
||||
// Validate the min:
|
||||
(this.value?.length ?? 0) < this.min,
|
||||
);
|
||||
this.addValidator(
|
||||
'rangeOverflow',
|
||||
() => this.maxMessage,
|
||||
() => !!this.max && this.items?.length > this.max,
|
||||
() => !this.readonly && !!this.value && !!this.max && this.value?.length > this.max,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -248,28 +263,29 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement,
|
||||
}
|
||||
|
||||
async #populateCards() {
|
||||
const missingCards = this.items.filter((item) => !this._cards.find((card) => card.unique === item.key));
|
||||
if (!missingCards.length) return;
|
||||
const mediaItems = this.#itemManager.getItems();
|
||||
|
||||
if (!this.items?.length) {
|
||||
if (!mediaItems.length) {
|
||||
this._cards = [];
|
||||
return;
|
||||
}
|
||||
// Check if all media items is loaded.
|
||||
// But notice, it would be nicer UX if we could show a loading state on the cards that are missing(loading) their items.
|
||||
const missingCards = mediaItems.filter((item) => !this._cards.find((card) => card.unique === item.unique));
|
||||
const removedCards = this._cards.filter((card) => !mediaItems.find((item) => card.unique === item.unique));
|
||||
if (missingCards.length === 0 && removedCards.length === 0) return;
|
||||
|
||||
const uniques = this.items.map((item) => item.mediaKey);
|
||||
|
||||
const { data: items } = await this.#itemRepository.requestItems(uniques);
|
||||
|
||||
this._cards = this.items.map((item) => {
|
||||
const media = items?.find((x) => x.unique === item.mediaKey);
|
||||
return {
|
||||
unique: item.key,
|
||||
media: item.mediaKey,
|
||||
name: media?.name ?? '',
|
||||
icon: media?.mediaType?.icon,
|
||||
isTrashed: media?.isTrashed ?? false,
|
||||
};
|
||||
});
|
||||
this._cards =
|
||||
this.value?.map((item) => {
|
||||
const media = mediaItems.find((x) => x.unique === item.mediaKey);
|
||||
return {
|
||||
unique: item.key,
|
||||
media: item.mediaKey,
|
||||
name: media?.name ?? '',
|
||||
icon: media?.mediaType?.icon,
|
||||
isTrashed: media?.isTrashed ?? false,
|
||||
};
|
||||
}) ?? [];
|
||||
}
|
||||
|
||||
#pickableFilter: (item: UmbMediaItemModel) => boolean = (item) => {
|
||||
@@ -290,12 +306,13 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement,
|
||||
focalPoint: null,
|
||||
}));
|
||||
|
||||
this.#items = [...this.#items, ...additions];
|
||||
this.value = [...(this.value ?? []), ...additions];
|
||||
this.dispatchEvent(new UmbChangeEvent());
|
||||
}
|
||||
|
||||
async #openPicker() {
|
||||
const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_PICKER_MODAL, {
|
||||
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
|
||||
const modalHandler = modalManager?.open(this, UMB_MEDIA_PICKER_MODAL, {
|
||||
data: {
|
||||
multiple: this.multiple,
|
||||
startNode: this.startNode,
|
||||
@@ -319,8 +336,7 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement,
|
||||
confirmLabel: this.localize.term('actions_remove'),
|
||||
});
|
||||
|
||||
this.#items = this.#items.filter((x) => x.key !== item.unique);
|
||||
this._cards = this._cards.filter((x) => x.unique !== item.unique);
|
||||
this.value = this.value?.filter((x) => x.key !== item.unique);
|
||||
|
||||
this.dispatchEvent(new UmbChangeEvent());
|
||||
}
|
||||
@@ -356,8 +372,7 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement,
|
||||
}
|
||||
|
||||
#renderAddButton() {
|
||||
// TODO: Stop preventing adding more, instead implement proper validation for user feedback. [NL]
|
||||
if ((this._cards && this.max && this._cards.length >= this.max) || (this._cards.length && !this.multiple)) return;
|
||||
if (this._cards && this._cards.length && !this.multiple) return;
|
||||
if (this.readonly && this._cards.length > 0) {
|
||||
return nothing;
|
||||
} else {
|
||||
@@ -365,6 +380,10 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement,
|
||||
<uui-button
|
||||
id="btn-add"
|
||||
look="placeholder"
|
||||
@blur=${() => {
|
||||
this.pristine = false;
|
||||
this.checkValidity();
|
||||
}}
|
||||
@click=${this.#openPicker}
|
||||
label=${this.localize.term('general_choose')}
|
||||
?disabled=${this.readonly}>
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
} from '@umbraco-cms/backoffice/property-editor';
|
||||
|
||||
import '../../components/input-rich-media/input-rich-media.element.js';
|
||||
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
|
||||
import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
|
||||
|
||||
const elementName = 'umb-property-editor-ui-media-picker';
|
||||
|
||||
@@ -37,6 +37,16 @@ export class UmbPropertyEditorUIMediaPickerElement
|
||||
this._max = minMax?.max ?? Infinity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input to mandatory, meaning validation will fail if the value is empty.
|
||||
* @type {boolean}
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
mandatory?: boolean;
|
||||
|
||||
@property({ type: String })
|
||||
mandatoryMessage = UMB_VALIDATION_EMPTY_LOCALIZATION_KEY;
|
||||
|
||||
/**
|
||||
* Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content.
|
||||
* @type {boolean}
|
||||
@@ -82,8 +92,17 @@ export class UmbPropertyEditorUIMediaPickerElement
|
||||
});
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-rich-media')!);
|
||||
}
|
||||
|
||||
override focus() {
|
||||
return this.shadowRoot?.querySelector<UmbInputRichMediaElement>('umb-input-rich-media')?.focus();
|
||||
}
|
||||
|
||||
#onChange(event: CustomEvent & { target: UmbInputRichMediaElement }) {
|
||||
this.value = event.target.items;
|
||||
const isEmpty = event.target.value?.length === 0;
|
||||
this.value = isEmpty ? undefined : event.target.value;
|
||||
this.dispatchEvent(new UmbPropertyValueChangeEvent());
|
||||
}
|
||||
|
||||
@@ -93,12 +112,14 @@ export class UmbPropertyEditorUIMediaPickerElement
|
||||
.alias=${this._alias}
|
||||
.allowedContentTypeIds=${this._allowedMediaTypes}
|
||||
.focalPointEnabled=${this._focalPointEnabled}
|
||||
.items=${this.value ?? []}
|
||||
.value=${this.value ?? []}
|
||||
.max=${this._max}
|
||||
.min=${this._min}
|
||||
.preselectedCrops=${this._preselectedCrops}
|
||||
.startNode=${this._startNode}
|
||||
.variantId=${this._variantId}
|
||||
.required=${this.mandatory}
|
||||
.requiredMessage=${this.mandatoryMessage}
|
||||
?multiple=${this._multiple}
|
||||
@change=${this.#onChange}
|
||||
?readonly=${this.readonly}>
|
||||
|
||||
@@ -119,6 +119,10 @@ export class UmbPropertyEditorUIContentPickerElement
|
||||
this.#setPickerRootUnique();
|
||||
}
|
||||
|
||||
override focus() {
|
||||
return this.shadowRoot?.querySelector<UmbInputContentElement>('umb-input-content')?.focus();
|
||||
}
|
||||
|
||||
async #setPickerRootUnique() {
|
||||
// If we have a root unique value, we don't need to fetch it from the dynamic root
|
||||
if (this._rootUnique) return;
|
||||
|
||||
Reference in New Issue
Block a user