diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts index afab9ee669..a0b17d486d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts @@ -138,7 +138,6 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen public set areaKey(value: string | null | undefined) { this._areaKey = value; this.#context.setAreaKey(value ?? null); - this.#setupValidation(); } public get areaKey(): string | null | undefined { return this._areaKey; @@ -169,27 +168,43 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen constructor() { super(); - this.observe(this.#context.layoutEntries, (layoutEntries) => { - //const oldValue = this._layoutEntries; - this.#sorter.setModel(layoutEntries); - this._layoutEntries = layoutEntries; - //this.requestUpdate('layoutEntries', oldValue); - }); + this.observe( + this.#context.layoutEntries, + (layoutEntries) => { + //const oldValue = this._layoutEntries; + this.#sorter.setModel(layoutEntries); + this._layoutEntries = layoutEntries; + //this.requestUpdate('layoutEntries', oldValue); + }, + null, + ); - this.observe(this.#context.amountOfAllowedBlockTypes, (length) => { - this._canCreate = length > 0; - if (length === 1) { - this.observe( - this.#context.firstAllowedBlockTypeName(), - (firstAllowedName) => { - this._singleBlockTypeName = firstAllowedName; - }, - 'observeSingleBlockTypeName', - ); - } else { - this.removeUmbControllerByAlias('observeSingleBlockTypeName'); - } - }); + this.observe( + this.#context.amountOfAllowedBlockTypes, + (length) => { + this._canCreate = length > 0; + if (length === 1) { + this.observe( + this.#context.firstAllowedBlockTypeName(), + (firstAllowedName) => { + this._singleBlockTypeName = firstAllowedName; + }, + 'observeSingleBlockTypeName', + ); + } else { + this.removeUmbControllerByAlias('observeSingleBlockTypeName'); + } + }, + null, + ); + + this.observe( + this.#context.rangeLimits, + (rangeLimits) => { + this.#setupRangeValidation(rangeLimits); + }, + null, + ); this.#context.getManager().then((manager) => { this.observe( @@ -207,42 +222,25 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen this.#controlValidator = new UmbFormControlValidator(this, this /*, this.#dataPath*/); } - async #getLimitValidation() { - if (this._areaKey === null) { - // This validation setup is not be as configurable as it should be, but it is a start. Alternatively we should consume the manager and observe the configuration. [NL] - const manager = await this.#context.getManager(); - const config = manager.getEditorConfiguration(); - const min = config?.getValueByAlias('validationLimit')?.min ?? 0; - const max = config?.getValueByAlias('validationLimit')?.max ?? Infinity; - return { min, max }; - } else { - return { min: 3, max: 4 }; - } - } - #rangeUnderflowValidator?: UmbFormControlValidatorConfig; #rangeOverflowValidator?: UmbFormControlValidatorConfig; - async #setupValidation() { - const rangeLimit = await this.#getLimitValidation(); - - console.log('setup validation', this); - + async #setupRangeValidation(rangeLimit: UmbNumberRangeValueType | undefined) { if (this.#rangeUnderflowValidator) { this.removeValidator(this.#rangeUnderflowValidator); this.#rangeUnderflowValidator = undefined; } - if (rangeLimit.min !== 0) { + if (rangeLimit?.min !== 0) { this.#rangeUnderflowValidator = this.addValidator( 'rangeUnderflow', () => { return this.localize.term( 'validation_entriesShort', - rangeLimit.min, - rangeLimit.min - (this._layoutEntries.length ?? 0), + rangeLimit!.min, + (rangeLimit!.min ?? 0) - this._layoutEntries.length, ); }, () => { - return (this._layoutEntries.length ?? 0) < rangeLimit.min; + return this._layoutEntries.length < (rangeLimit?.min ?? 0); }, ); } @@ -251,18 +249,18 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen this.removeValidator(this.#rangeOverflowValidator); this.#rangeOverflowValidator = undefined; } - if (rangeLimit.max !== Infinity) { + if (rangeLimit?.max !== Infinity) { this.#rangeOverflowValidator = this.addValidator( 'rangeOverflow', () => { return this.localize.term( 'validation_entriesExceed', - rangeLimit.max, - (this._layoutEntries.length ?? 0) - rangeLimit.max, + rangeLimit!.max, + this._layoutEntries.length - (rangeLimit!.max ?? this._layoutEntries.length), ); }, () => { - return (this._layoutEntries.length ?? 0) > rangeLimit.max; + return (this._layoutEntries.length ?? 0) > (rangeLimit?.max ?? Infinity); }, ); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts index 7b24b49f4d..4edbccc8ff 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts @@ -4,10 +4,11 @@ import { UMB_BLOCK_GRID_ENTRY_CONTEXT, type UmbBlockGridWorkspaceData } from '.. import type { UmbBlockGridLayoutModel, UmbBlockGridTypeAreaType, UmbBlockGridTypeModel } from '../types.js'; import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from './block-grid-manager.context.js'; import type { UmbBlockGridScalableContainerContext } from './block-grid-scale-manager/block-grid-scale-manager.controller.js'; -import { UmbArrayState, UmbNumberState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState, UmbNumberState, UmbObjectState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; import { pathFolderName } from '@umbraco-cms/backoffice/utils'; +import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; export class UmbBlockGridEntriesContext extends UmbBlockEntriesContext< @@ -31,6 +32,9 @@ export class UmbBlockGridEntriesContext #parentUnique?: string | null; #areaKey?: string | null; + #rangeLimits = new UmbObjectState(undefined); + readonly rangeLimits = this.#rangeLimits.asObservable(); + #allowedBlockTypes = new UmbArrayState([], (x) => x.contentElementTypeKey); public readonly allowedBlockTypes = this.#allowedBlockTypes.asObservable(); public readonly amountOfAllowedBlockTypes = this.#allowedBlockTypes.asObservablePart((x) => x.length); @@ -121,6 +125,7 @@ export class UmbBlockGridEntriesContext if (!this._manager) return; this.#getAllowedBlockTypes(); + this.#getRangeLimits(); this.observe( this._manager.propertyAlias, @@ -185,6 +190,7 @@ export class UmbBlockGridEntriesContext this.removeUmbControllerByAlias('observeAreaType'); this.#getAllowedBlockTypes(); + this.#getRangeLimits(); } else { if (!this.#parentEntry) return; @@ -228,6 +234,7 @@ export class UmbBlockGridEntriesContext hostEl.style.setProperty('--umb-block-grid--area-column-span', areaType?.columnSpan?.toString() ?? ''); hostEl.style.setProperty('--umb-block-grid--area-row-span', areaType?.rowSpan?.toString() ?? ''); this.#getAllowedBlockTypes(); + this.#getRangeLimits(); }, 'observeAreaType', ); @@ -235,10 +242,14 @@ export class UmbBlockGridEntriesContext } #getAllowedBlockTypes() { - if (this.#areaKey === undefined || !this._manager) return; - + if (!this._manager) return; this.#allowedBlockTypes.setValue(this.#retrieveAllowedElementTypes()); } + #getRangeLimits() { + if (!this._manager) return; + const range = this.#retrieveRangeLimits(); + this.#rangeLimits.setValue(range); + } getPathForCreateBlock(index: number) { return this._catalogueRouteBuilderState.getValue()?.({ view: 'create', index: index }); @@ -324,10 +335,34 @@ export class UmbBlockGridEntriesContext // No specific permissions setup, so we will fallback to items allowed in areas: return this._manager.getBlockTypes().filter((x) => x.allowInAreas); + } else if (this.#areaKey === null) { + // If AreaKey is null, then we are in the root, looking for items allowed as root: + return this._manager.getBlockTypes().filter((x) => x.allowAtRoot); } - // If no AreaKey, then we are in the root, looking for items allowed as root: - return this._manager.getBlockTypes().filter((x) => x.allowAtRoot); + return []; + } + + /** + * @internal + * @returns an NumberRange of the min and max allowed items in the current area. Or undefined if not ready jet. + */ + #retrieveRangeLimits(): UmbNumberRangeValueType | undefined { + if (this.#areaKey != null) { + // Area entries: + if (!this.#areaType) return undefined; + + return { min: this.#areaType.minAllowed ?? 0, max: this.#areaType.maxAllowed ?? Infinity }; + } else if (this.#areaKey === null) { + if (!this._manager) return undefined; + + const config = this._manager.getEditorConfiguration(); + const min = config?.getValueByAlias('validationLimit')?.min ?? 0; + const max = config?.getValueByAlias('validationLimit')?.max ?? Infinity; + return { min, max }; + } + + return undefined; } /** diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts index 2685be90e0..b04465f3c4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts @@ -27,7 +27,7 @@ type FlagTypes = | 'valid'; // Acceptable as an internal interface/type, BUT if exposed externally this should be turned into a public interface in a separate file. -interface UmbFormControlValidatorConfig { +export interface UmbFormControlValidatorConfig { flagKey: FlagTypes; getMessageMethod: () => string; checkMethod: () => boolean;