From d29c2d6c86606a77ac3d05693070f2cd35ce4341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 May 2024 16:07:56 +0200 Subject: [PATCH 1/3] root validation impl --- .../block-grid-entries.element.ts | 35 ++++++++++++++++++- .../property-editor-ui-block-grid.element.ts | 18 +++++----- .../property-editor-ui-block-list.element.ts | 4 +-- .../block/context/block-manager.context.ts | 3 ++ .../input-number-range.element.ts | 4 +-- .../src/packages/core/models/index.ts | 2 +- .../validation/mixins/form-control.mixin.ts | 1 - ...-editor-ui-document-type-picker.element.ts | 4 +-- ...perty-editor-ui-document-picker.element.ts | 4 +-- ...rty-editor-ui-media-type-picker.element.ts | 10 ++---- ...y-editor-ui-media-entity-picker.element.ts | 4 +-- ...property-editor-ui-media-picker.element.ts | 4 +-- ...y-editor-ui-member-group-picker.element.ts | 4 +-- ...property-editor-ui-number-range.element.ts | 12 +++---- 14 files changed, 69 insertions(+), 40 deletions(-) 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 bcdc14b897..76ee38b35f 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 @@ -11,6 +11,8 @@ import { html, customElement, state, repeat, css, property, nothing } from '@umb import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import '../block-grid-entry/index.js'; import { UmbSorterController, type UmbSorterConfig, type resolvePlacementArgs } from '@umbraco-cms/backoffice/sorter'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; /** * Notice this utility method is not really shareable with others as it also takes areas into account. [NL] @@ -98,7 +100,7 @@ const SORTER_CONFIG: UmbSorterConfig('validationLimit')?.min ?? 0; + const max = config?.getValueByAlias('validationLimit')?.max ?? Infinity; + + this.addValidator( + 'rangeUnderflow', + () => { + return this.localize.term('validation_entriesShort', [min, min - (this._layoutEntries.length ?? 0)]); + }, + () => { + return (this._layoutEntries.length ?? 0) < min; + }, + ); + + this.addValidator( + 'rangeOverflow', + () => { + return this.localize.term('validation_entriesExceed', [max, (this._layoutEntries.length ?? 0) - max]); + }, + () => { + return (this._layoutEntries.length ?? 0) > max; + }, + ); + } + } + // TODO: Missing ability to jump directly to creating a Block, when there is only one Block Type. [NL] render() { return html` diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts index 21e61c2da5..fba24dea2d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts @@ -7,16 +7,19 @@ import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extensi import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import type { UmbBlockTypeGroup } from '@umbraco-cms/backoffice/block-type'; import type { UmbBlockGridTypeModel, UmbBlockGridValueModel } from '@umbraco-cms/backoffice/block-grid'; -import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models'; import '../../components/block-grid-entries/index.js'; import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; /** * @element umb-property-editor-ui-block-grid */ @customElement('umb-property-editor-ui-block-grid') -export class UmbPropertyEditorUIBlockGridElement extends UmbLitElement implements UmbPropertyEditorUiElement { +export class UmbPropertyEditorUIBlockGridElement + extends UmbFormControlMixin(UmbLitElement) + implements UmbPropertyEditorUiElement +{ #context = new UmbBlockGridManagerContext(this); // private _value: UmbBlockGridValueModel = { @@ -28,10 +31,10 @@ export class UmbPropertyEditorUIBlockGridElement extends UmbLitElement implement public set config(config: UmbPropertyEditorConfigCollection | undefined) { if (!config) return; - const validationLimit = config.getValueByAlias('validationLimit'); + /*const validationLimit = config.getValueByAlias('validationLimit'); - this._limitMin = validationLimit?.min; - this._limitMax = validationLimit?.max; + this.#limitMin = validationLimit?.min; + this.#limitMax = validationLimit?.max;*/ const blocks = config.getValueByAlias>('blocks') ?? []; this.#context.setBlockTypes(blocks); @@ -45,11 +48,6 @@ export class UmbPropertyEditorUIBlockGridElement extends UmbLitElement implement this.#context.setEditorConfiguration(config); } - // - @state() - private _limitMin?: number; - @state() - private _limitMax?: number; @state() private _layoutColumns?: number; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts index e9e8b905dd..1b94730563 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts @@ -14,7 +14,7 @@ import { } from '@umbraco-cms/backoffice/property-editor'; import type { UmbBlockLayoutBaseModel } from '@umbraco-cms/backoffice/block'; import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type'; -import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models'; +import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/modal'; import type { UmbSorterConfig } from '@umbraco-cms/backoffice/sorter'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; @@ -74,7 +74,7 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement public set config(config: UmbPropertyEditorConfigCollection | undefined) { if (!config) return; - const validationLimit = config.getValueByAlias('validationLimit'); + const validationLimit = config.getValueByAlias('validationLimit'); this._limitMin = validationLimit?.min; this._limitMax = validationLimit?.max; 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 4f86a9475e..0178c48acb 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 @@ -68,6 +68,9 @@ export abstract class UmbBlockManagerContext< setEditorConfiguration(configs: UmbPropertyEditorConfigCollection) { this._editorConfiguration.setValue(configs); } + getEditorConfiguration(): UmbPropertyEditorConfigCollection | undefined { + return this._editorConfiguration.getValue(); + } setBlockTypes(blockTypes: Array) { this.#blockTypes.setValue(blockTypes); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-number-range/input-number-range.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-number-range/input-number-range.element.ts index 83f02dbbaf..cd522a0a1d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-number-range/input-number-range.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-number-range/input-number-range.element.ts @@ -2,7 +2,7 @@ import { css, customElement, html, ifDefined, property, state } from '@umbraco-c import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models'; +import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; import type { UUIInputElement } from '@umbraco-cms/backoffice/external/uui'; function getNumberOrUndefined(value: string) { @@ -43,7 +43,7 @@ export class UmbInputNumberRangeElement extends UmbFormControlMixin(UmbLitElemen } @property({ type: Object }) - validationRange?: NumberRangeValueType; + validationRange?: UmbNumberRangeValueType; private updateValue() { const newValue = diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts index 56d28dd753..c1f1a5034e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts @@ -9,7 +9,7 @@ export interface ServertimeOffset { offset: number; } -export interface NumberRangeValueType { +export interface UmbNumberRangeValueType { min?: number; max?: number; } 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 1e58626a9e..8e12d5ba56 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 @@ -96,7 +96,6 @@ export function UmbFormControlMixin< /** * Value of this form control. - * If you dont want the setFormValue to be called on the ElementInternals, then prevent calling this method, by not calling super.value = newValue in your implementation of the value setter method. * @type {string} * @attr value * @default '' diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/property-editors/document-type-picker/property-editor-ui-document-type-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/property-editors/document-type-picker/property-editor-ui-document-type-picker.element.ts index 477ba1415d..5cfb04658f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/property-editors/document-type-picker/property-editor-ui-document-type-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/property-editors/document-type-picker/property-editor-ui-document-type-picker.element.ts @@ -2,7 +2,7 @@ import type { UmbInputDocumentTypeElement } from '../../components/input-documen import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; -import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models'; +import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; @@ -14,7 +14,7 @@ export class UmbPropertyEditorUIDocumentTypePickerElement extends UmbLitElement public set config(config: UmbPropertyEditorConfigCollection | undefined) { if (!config) return; - const minMax = config?.getValueByAlias('validationLimit'); + const minMax = config?.getValueByAlias('validationLimit'); this.min = minMax?.min ?? 0; this.max = minMax?.max ?? Infinity; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-editors/document-picker/property-editor-ui-document-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-editors/document-picker/property-editor-ui-document-picker.element.ts index cf4793ae28..9a6a311b0f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-editors/document-picker/property-editor-ui-document-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-editors/document-picker/property-editor-ui-document-picker.element.ts @@ -3,7 +3,7 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; -import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models'; +import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; @@ -16,7 +16,7 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl public set config(config: UmbPropertyEditorConfigCollection | undefined) { if (!config) return; - const minMax = config.getValueByAlias('validationLimit'); + const minMax = config.getValueByAlias('validationLimit'); if (minMax) { this._min = minMax.min && minMax.min > 0 ? minMax.min : 0; this._max = minMax.max && minMax.max > 0 ? minMax.max : 1; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/property-editors/media-type-picker/property-editor-ui-media-type-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/property-editors/media-type-picker/property-editor-ui-media-type-picker.element.ts index b683681d17..1872d689ea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/property-editors/media-type-picker/property-editor-ui-media-type-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/property-editors/media-type-picker/property-editor-ui-media-type-picker.element.ts @@ -2,7 +2,7 @@ import type { UmbInputMediaTypeElement } from '../../components/index.js'; import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; -import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models'; +import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; @@ -14,7 +14,7 @@ export class UmbPropertyEditorUIMediaTypePickerElement extends UmbLitElement imp public set config(config: UmbPropertyEditorConfigCollection | undefined) { if (!config) return; - const minMax = config?.getValueByAlias('validationLimit'); + const minMax = config?.getValueByAlias('validationLimit'); this.min = minMax?.min ?? 0; this.max = minMax?.max ?? Infinity; } @@ -32,11 +32,7 @@ export class UmbPropertyEditorUIMediaTypePickerElement extends UmbLitElement imp render() { return html` - + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-entity-picker/property-editor-ui-media-entity-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-entity-picker/property-editor-ui-media-entity-picker.element.ts index 6e6bbafd45..6594afd924 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-entity-picker/property-editor-ui-media-entity-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-entity-picker/property-editor-ui-media-entity-picker.element.ts @@ -2,7 +2,7 @@ import { html, customElement, property } from '@umbraco-cms/backoffice/external/ import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; -import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models'; +import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; import type { UmbInputMediaElement } from '@umbraco-cms/backoffice/media'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; @@ -25,7 +25,7 @@ export class UmbPropertyEditorUIMediaEntityPickerElement extends UmbLitElement i public set config(config: UmbPropertyEditorConfigCollection | undefined) { if (!config) return; - const minMax = config?.getValueByAlias('validationLimit'); + const minMax = config?.getValueByAlias('validationLimit'); this.#min = minMax?.min ?? 0; this.#max = minMax?.max ?? Infinity; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts index 6a98cca0e5..83c86d901a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts @@ -4,7 +4,7 @@ import { customElement, html, property, state } from '@umbraco-cms/backoffice/ex import { UmbId } from '@umbraco-cms/backoffice/id'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; -import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models'; +import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; @@ -49,7 +49,7 @@ export class UmbPropertyEditorUIMediaPickerElement extends UmbLitElement impleme const filter = config.getValueByAlias('filter'); this._allowedMediaTypes = filter?.split(',') ?? []; - const minMax = config.getValueByAlias('validationLimit'); + const minMax = config.getValueByAlias('validationLimit'); this._limitMin = minMax?.min ?? 0; this._limitMax = minMax?.max ?? Infinity; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/property-editor/member-group-picker/property-editor-ui-member-group-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/property-editor/member-group-picker/property-editor-ui-member-group-picker.element.ts index e223e3fa18..397bdf9e69 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/property-editor/member-group-picker/property-editor-ui-member-group-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/property-editor/member-group-picker/property-editor-ui-member-group-picker.element.ts @@ -1,7 +1,7 @@ import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; -import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models'; +import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; import type { UmbInputMemberGroupElement } from '@umbraco-cms/backoffice/member-group'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; @@ -17,7 +17,7 @@ export class UmbPropertyEditorUIMemberGroupPickerElement extends UmbLitElement i public set config(config: UmbPropertyEditorConfigCollection | undefined) { if (!config) return; - const minMax = config?.getValueByAlias('validationLimit'); + const minMax = config?.getValueByAlias('validationLimit'); this.min = minMax?.min ?? 0; this.max = minMax?.max ?? Infinity; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/number-range/property-editor-ui-number-range.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/number-range/property-editor-ui-number-range.element.ts index 86e7400d20..8afc35ad70 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/number-range/property-editor-ui-number-range.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/number-range/property-editor-ui-number-range.element.ts @@ -3,7 +3,7 @@ import { customElement, html, property, state } from '@umbraco-cms/backoffice/ex import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; -import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models'; +import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; @@ -14,7 +14,7 @@ import '../../core/components/input-number-range/input-number-range.element.js'; */ @customElement('umb-property-editor-ui-number-range') export class UmbPropertyEditorUINumberRangeElement - extends UmbFormControlMixin(UmbLitElement, undefined) + extends UmbFormControlMixin(UmbLitElement, undefined) implements UmbPropertyEditorUiElement { @state() @@ -24,10 +24,10 @@ export class UmbPropertyEditorUINumberRangeElement _maxValue?: number; @state() - _validationRange?: NumberRangeValueType; + _validationRange?: UmbNumberRangeValueType; @property({ type: Object }) - public set value(value: NumberRangeValueType | undefined) { + public set value(value: UmbNumberRangeValueType | undefined) { this.#value = value || { min: undefined, max: undefined }; this._minValue = value?.min; this._maxValue = value?.max; @@ -35,11 +35,11 @@ export class UmbPropertyEditorUINumberRangeElement public get value() { return this.#value; } - #value: NumberRangeValueType = { min: undefined, max: undefined }; + #value: UmbNumberRangeValueType = { min: undefined, max: undefined }; public set config(config: UmbPropertyEditorConfigCollection) { if (!config) return; - this._validationRange = config.getValueByAlias('validationRange'); + this._validationRange = config.getValueByAlias('validationRange'); } #onChange(event: CustomEvent & { target: UmbInputNumberRangeElement }) { From 480cab5f15278fcd2a92ffc69ee89b053934ce5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 24 May 2024 11:27:56 +0200 Subject: [PATCH 2/3] entity limit client validation --- .../block-grid-entries.element.ts | 55 ++++++++++++++++--- .../property-editor-ui-block-grid.element.ts | 2 +- .../validation/context/validation.context.ts | 3 +- .../form-control-validator.controller.ts | 2 +- .../validation/mixins/form-control.mixin.ts | 14 ++++- .../workspace/document-workspace.context.ts | 1 + 6 files changed, 63 insertions(+), 14 deletions(-) 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 76ee38b35f..afab9ee669 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 @@ -11,7 +11,11 @@ import { html, customElement, state, repeat, css, property, nothing } from '@umb import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import '../block-grid-entry/index.js'; import { UmbSorterController, type UmbSorterConfig, type resolvePlacementArgs } from '@umbraco-cms/backoffice/sorter'; -import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import { + UmbFormControlMixin, + UmbFormControlValidator, + type UmbFormControlValidatorConfig, +} from '@umbraco-cms/backoffice/validation'; import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; /** @@ -128,6 +132,7 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen }); #context = new UmbBlockGridEntriesContext(this); + #controlValidator: UmbFormControlValidator; @property({ attribute: false }) public set areaKey(value: string | null | undefined) { @@ -198,33 +203,66 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen 'observeStylesheet', ); }); + + this.#controlValidator = new UmbFormControlValidator(this, this /*, this.#dataPath*/); } - async #setupValidation() { + 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 }; + } + } - this.addValidator( + #rangeUnderflowValidator?: UmbFormControlValidatorConfig; + #rangeOverflowValidator?: UmbFormControlValidatorConfig; + async #setupValidation() { + const rangeLimit = await this.#getLimitValidation(); + + console.log('setup validation', this); + + if (this.#rangeUnderflowValidator) { + this.removeValidator(this.#rangeUnderflowValidator); + this.#rangeUnderflowValidator = undefined; + } + if (rangeLimit.min !== 0) { + this.#rangeUnderflowValidator = this.addValidator( 'rangeUnderflow', () => { - return this.localize.term('validation_entriesShort', [min, min - (this._layoutEntries.length ?? 0)]); + return this.localize.term( + 'validation_entriesShort', + rangeLimit.min, + rangeLimit.min - (this._layoutEntries.length ?? 0), + ); }, () => { - return (this._layoutEntries.length ?? 0) < min; + return (this._layoutEntries.length ?? 0) < rangeLimit.min; }, ); + } - this.addValidator( + if (this.#rangeOverflowValidator) { + this.removeValidator(this.#rangeOverflowValidator); + this.#rangeOverflowValidator = undefined; + } + if (rangeLimit.max !== Infinity) { + this.#rangeOverflowValidator = this.addValidator( 'rangeOverflow', () => { - return this.localize.term('validation_entriesExceed', [max, (this._layoutEntries.length ?? 0) - max]); + return this.localize.term( + 'validation_entriesExceed', + rangeLimit.max, + (this._layoutEntries.length ?? 0) - rangeLimit.max, + ); }, () => { - return (this._layoutEntries.length ?? 0) > max; + return (this._layoutEntries.length ?? 0) > rangeLimit.max; }, ); } @@ -247,6 +285,7 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen `, )} + ${this._canCreate ? this.#renderCreateButton() : nothing} `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts index fba24dea2d..0c5bbe1676 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts @@ -100,7 +100,7 @@ export class UmbPropertyEditorUIBlockGridElement } render() { - return html``; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context.ts index 38d815a4c8..d80a066427 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context.ts @@ -68,12 +68,13 @@ export class UmbValidationContext extends UmbContextBase i const resultsStatus = await Promise.all(this.#validators.map((v) => v.validate())).then( () => Promise.resolve(true), - () => Promise.reject(false), + () => Promise.resolve(false), ); // If we have any messages then we are not valid, otherwise lets check the validation results: [NL] // This enables us to keep client validations though UI is not present anymore — because the client validations got defined as messages. [NL] const isValid = this.messages.getHasAnyMessages() ? false : resultsStatus; + this.#isValid = isValid; if (isValid === false) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/form-control-validator.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/form-control-validator.controller.ts index b857634f2b..a351208f12 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/form-control-validator.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/form-control-validator.controller.ts @@ -7,7 +7,7 @@ import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerAlias, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbFormControlValidator extends UmbControllerBase implements UmbValidator { - // The path to the data that this validator is validating. Public so the ValidationContext can access it. + // The path to the data that this validator is validating. readonly #dataPath?: string; #context?: typeof UMB_VALIDATION_CONTEXT.TYPE; 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 8e12d5ba56..2685be90e0 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 @@ -34,7 +34,11 @@ interface UmbFormControlValidatorConfig { } export interface UmbFormControlMixinInterface extends HTMLElement { - addValidator: (flagKey: FlagTypes, getMessageMethod: () => string, checkMethod: () => boolean) => void; + addValidator: ( + flagKey: FlagTypes, + getMessageMethod: () => string, + checkMethod: () => boolean, + ) => UmbFormControlValidatorConfig; removeValidator: (obj: UmbFormControlValidatorConfig) => void; //static formAssociated: boolean; //protected getFormElement(): HTMLElement | undefined | null; // allows for null as it makes it simpler to just implement a querySelector as that might return null. [NL] @@ -56,7 +60,11 @@ export declare abstract class UmbFormControlMixinElement { protected _internals: ElementInternals; protected _runValidators(): void; - addValidator: (flagKey: FlagTypes, getMessageMethod: () => string, checkMethod: () => boolean) => void; + addValidator: ( + flagKey: FlagTypes, + getMessageMethod: () => string, + checkMethod: () => boolean, + ) => UmbFormControlValidatorConfig; removeValidator: (obj: UmbFormControlValidatorConfig) => void; protected addFormControlElement(element: UmbNativeFormControlElement): void; @@ -209,7 +217,7 @@ export function UmbFormControlMixin< flagKey: flagKey, getMessageMethod: getMessageMethod, checkMethod: checkMethod, - }; + } satisfies UmbFormControlValidatorConfig; this.#validators.push(validator); return validator; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index dd8ea44dee..771623fa02 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -676,6 +676,7 @@ export class UmbDocumentWorkspaceContext // Create the validation repository if it does not exist. (we first create this here when we need it) [NL] this.#validationRepository ??= new UmbDocumentValidationRepository(this); + // We ask the server first to get a concatenated set of validation messages. So we see both front-end and back-end validation messages [NL] if (this.getIsNew()) { const parent = this.#parent.getValue(); if (!parent) throw new Error('Parent is not set'); From 703e9190692ca8160ae7d3a945f2b25f22f7cf11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 24 May 2024 13:35:53 +0200 Subject: [PATCH 3/3] implement range validation for areas --- .../block-grid-entries.element.ts | 92 +++++++++---------- .../context/block-grid-entries.context.ts | 45 ++++++++- .../validation/mixins/form-control.mixin.ts | 2 +- 3 files changed, 86 insertions(+), 53 deletions(-) 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;