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 eb269efbe3..ce5759057a 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 @@ -135,6 +135,9 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen #context = new UmbBlockGridEntriesContext(this); #controlValidator?: UmbFormControlValidator; + #typeLimitValidator?: UmbFormControlValidatorConfig; + #rangeUnderflowValidator?: UmbFormControlValidatorConfig; + #rangeOverflowValidator?: UmbFormControlValidatorConfig; @property({ type: String, attribute: 'area-key', reflect: true }) public set areaKey(value: string | null | undefined) { @@ -217,6 +220,14 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen null, ); + this.observe( + this.#context.hasTypeLimits, + (hasTypeLimits) => { + this.#setupBlockTypeLimitValidation(hasTypeLimits); + }, + null, + ); + this.#context.getManager().then((manager) => { this.observe( manager.layoutStylesheet, @@ -233,8 +244,6 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen new UmbFormControlValidator(this, this /*, this.#dataPath*/); } - #rangeUnderflowValidator?: UmbFormControlValidatorConfig; - #rangeOverflowValidator?: UmbFormControlValidatorConfig; async #setupRangeValidation(rangeLimit: UmbNumberRangeValueType | undefined) { if (this.#rangeUnderflowValidator) { this.removeValidator(this.#rangeUnderflowValidator); @@ -277,6 +286,38 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen } } + async #setupBlockTypeLimitValidation(hasTypeLimits: boolean | undefined) { + if (this.#typeLimitValidator) { + this.removeValidator(this.#typeLimitValidator); + this.#typeLimitValidator = undefined; + } + if (hasTypeLimits) { + console.log('hasTypeLimits'); + this.#typeLimitValidator = this.addValidator( + 'patternMismatch', + () => { + const invalids = this.#context.getInvalidBlockTypeLimits(); + return invalids + .map((invalidRule) => + this.localize.term( + invalidRule.amount < invalidRule.minRequirement + ? 'blockEditor_areaValidationEntriesShort' + : 'blockEditor_areaValidationEntriesExceed', + invalidRule.name, + invalidRule.amount, + invalidRule.minRequirement, + invalidRule.maxRequirement, + ), + ) + .join(', '); + }, + () => { + return !this.#context.checkBlockTypeLimitsValidity(); + }, + ); + } + } + // TODO: Missing ability to jump directly to creating a Block, when there is only one Block Type. [NL] override render() { return html` 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 a4e93e9d50..1e15578e15 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 @@ -8,7 +8,13 @@ import { import type { UmbBlockGridLayoutModel, UmbBlockGridTypeAreaType, UmbBlockGridTypeModel } from '../types.js'; import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from './block-grid-manager.context-token.js'; import type { UmbBlockGridScalableContainerContext } from './block-grid-scale-manager/block-grid-scale-manager.controller.js'; -import { UmbArrayState, UmbNumberState, UmbObjectState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; +import { + UmbArrayState, + UmbBooleanState, + UmbNumberState, + UmbObjectState, + UmbStringState, +} from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { pathFolderName } from '@umbraco-cms/backoffice/utils'; @@ -49,6 +55,9 @@ export class UmbBlockGridEntriesContext public readonly amountOfAllowedBlockTypes = this.#allowedBlockTypes.asObservablePart((x) => x.length); public readonly canCreate = this.#allowedBlockTypes.asObservablePart((x) => x.length > 0); + #hasTypeLimits = new UmbBooleanState(undefined); + public readonly hasTypeLimits = this.#hasTypeLimits.asObservable(); + firstAllowedBlockTypeName() { if (!this._manager) { throw new Error('Manager not ready'); @@ -241,8 +250,6 @@ export class UmbBlockGridEntriesContext 'observeThisLayouts', ); - this.removeUmbControllerByAlias('observeAreaType'); - const hostEl = this.getHostElement() as HTMLElement | undefined; if (hostEl) { hostEl.removeAttribute('data-area-alias'); @@ -309,6 +316,7 @@ export class UmbBlockGridEntriesContext #setupAllowedBlockTypes() { if (!this._manager) return; this.#allowedBlockTypes.setValue(this.#retrieveAllowedElementTypes()); + this.#setupAllowedBlockTypesLimits(); } #setupRangeLimits() { if (!this._manager) return; @@ -429,6 +437,100 @@ export class UmbBlockGridEntriesContext return []; } + /** + * @internal + */ + #setupAllowedBlockTypesLimits() { + if (!this._manager) return; + + if (this.#areaKey) { + // Area entries: + if (!this.#areaType) return; + + if (this.#areaType.specifiedAllowance && this.#areaType.specifiedAllowance?.length > 0) { + this.#hasTypeLimits.setValue(true); + } + } else if (this.#areaKey === null) { + // RESET + } + } + + #invalidBlockTypeLimits?: Array<{ + groupKey?: string; + key?: string; + name: string; + amount: number; + minRequirement: number; + maxRequirement: number; + }>; + + getInvalidBlockTypeLimits() { + return this.#invalidBlockTypeLimits ?? []; + } + /** + * @internal + * @returns {boolean} - True if the block type limits are valid, otherwise false. + */ + checkBlockTypeLimitsValidity(): boolean { + if (!this.#areaType || !this.#areaType.specifiedAllowance) return false; + + const layoutEntries = this._layoutEntries.getValue(); + + this.#invalidBlockTypeLimits = []; + + const hasInvalidRules = this.#areaType.specifiedAllowance.some((rule) => { + const minAllowed = rule.minAllowed || 0; + const maxAllowed = rule.maxAllowed || 0; + + // For block groups: + if (rule.groupKey) { + const groupElementTypeKeys = + this._manager + ?.getBlockTypes() + .filter((blockType) => blockType.groupKey === rule.groupKey && blockType.allowInAreas === true) + .map((x) => x.contentElementTypeKey) ?? []; + const groupAmount = layoutEntries.filter((entry) => { + const contentTypeKey = this._manager!.getContentTypeKeyOf(entry.contentUdi); + return contentTypeKey ? groupElementTypeKeys.indexOf(contentTypeKey) !== -1 : false; + }).length; + + if (groupAmount < minAllowed || (maxAllowed > 0 && groupAmount > maxAllowed)) { + this.#invalidBlockTypeLimits!.push({ + groupKey: rule.groupKey, + name: this._manager!.getBlockGroupName(rule.groupKey) ?? '?', + amount: groupAmount, + minRequirement: minAllowed, + maxRequirement: maxAllowed, + }); + return true; + } + } + // For specific elementTypes: + else if (rule.elementTypeKey) { + const amount = layoutEntries.filter((entry) => { + const contentTypeKey = this._manager!.getContentOf(entry.contentUdi)?.contentTypeKey; + return contentTypeKey === rule.elementTypeKey; + }).length; + console.log('amount', amount); + if (amount < minAllowed || (maxAllowed > 0 ? amount > maxAllowed : false)) { + this.#invalidBlockTypeLimits!.push({ + key: rule.elementTypeKey, + name: this._manager!.getContentTypeNameOf(rule.elementTypeKey) ?? '?', + amount: amount, + minRequirement: minAllowed, + maxRequirement: maxAllowed, + }); + return true; + } + } + + // Lets fail cause the rule was bad. + console.error('Invalid block type limit rule.', rule); + return false; + }); + return hasInvalidRules === false; + } + /** * Check if given contentUdi is allowed in the current area. * @param contentUdi {string} - The contentUdi of the content to check. diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts index 29d0aaec8e..db64e29d17 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts @@ -62,6 +62,9 @@ export class UmbBlockGridManagerContext< getBlockGroups() { return this.#blockGroups.value; } + getBlockGroupName(unique: string) { + return this.#blockGroups.getValue().find((group) => group.key === unique)?.name; + } constructor(host: UmbControllerHost) { super(host); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts index 0b3d26e95b..df113d9b81 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts @@ -148,6 +148,9 @@ export abstract class UmbBlockManagerContext< getContentTypeNameOf(contentTypeKey: string) { return this.#contentTypes.getValue().find((x) => x.unique === contentTypeKey)?.name; } + getContentTypeKeyOf(contentTypeKey: string) { + return this.#contentTypes.getValue().find((x) => x.unique === contentTypeKey)?.unique; + } getContentTypeHasProperties(contentTypeKey: string) { const properties = this.#contentTypes.getValue().find((x) => x.unique === contentTypeKey)?.properties; return properties ? properties.length > 0 : false;