diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts index 4778221731..1d029be7bf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts @@ -13,6 +13,8 @@ import { UMB_BLOCK_GRID, type UmbBlockGridLayoutModel } from '@umbraco-cms/backo import '../block-grid-block-inline/index.js'; import '../block-grid-block/index.js'; import '../block-scale-handler/index.js'; +import { UmbObserveValidationStateController } from '@umbraco-cms/backoffice/validation'; +import { UmbDataPathBlockElementDataQuery } from '@umbraco-cms/backoffice/block'; /** * @element umb-block-grid-entry */ @@ -37,6 +39,16 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper this._blockViewProps.contentUdi = value; this.setAttribute('data-element-udi', value); this.#context.setContentUdi(value); + + new UmbObserveValidationStateController( + this, + `$.contentData[${UmbDataPathBlockElementDataQuery({ udi: value })}]`, + (hasMessages) => { + this._contentInvalid = hasMessages; + this._blockViewProps.contentInvalid = hasMessages; + }, + 'observeMessagesForContent', + ); } private _contentUdi?: string | undefined; // @@ -89,6 +101,14 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper @state() _inlineCreateAboveWidth?: string; + // 'content-invalid' attribute is used for styling purpose. + @property({ type: Boolean, attribute: 'content-invalid', reflect: true }) + _contentInvalid?: boolean; + + // 'settings-invalid' attribute is used for styling purpose. + @property({ type: Boolean, attribute: 'settings-invalid', reflect: true }) + _settingsInvalid?: boolean; + @state() _blockViewProps: UmbBlockEditorCustomViewProperties = { contentUdi: undefined!, @@ -178,6 +198,20 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper this.#context.settings, (settings) => { this.#updateBlockViewProps({ settings }); + + this.removeUmbControllerByAlias('observeMessagesForSettings'); + if (settings) { + // Observe settings validation state: + new UmbObserveValidationStateController( + this, + `$.settingsData[${UmbDataPathBlockElementDataQuery(settings)}]`, + (hasMessages) => { + this._settingsInvalid = hasMessages; + this._blockViewProps.settingsInvalid = hasMessages; + }, + 'observeMessagesForSettings', + ); + } }, null, ); @@ -340,20 +374,34 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
${this._inlineEditingMode ? this.#renderInlineEditBlock() : this.#renderRefBlock()} ${this._showContentEdit && this._workspaceEditContentPath - ? html` + ? html` + ${this._contentInvalid + ? html`!` + : nothing} ` : nothing} ${this._hasSettings && this._workspaceEditSettingsPath - ? html` + ? html` + ${this._settingsInvalid + ? html`!` + : nothing} ` : nothing} this.#context.requestDelete()}> @@ -361,6 +409,9 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper + ${!this._showContentEdit && this._contentInvalid + ? html`!` + : nothing} ${this._canScale ? html` this.#context.scaleManager.onScaleMouseDown(e)}> @@ -387,11 +438,21 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper :host { position: relative; display: block; + --umb-block-grid-entry-actions-opacity: 0; } + :host([settings-invalid]), + :host([content-invalid]), + :host(:hover), + :host(:focus-within) { + --umb-block-list-entry-actions-opacity: 1; + } + uui-action-bar { position: absolute; top: var(--uui-size-2); right: var(--uui-size-2); + opacity: var(--umb-block-list-entry-actions-opacity, 0); + transition: opacity 120ms; } uui-button-inline-create { top: 0px; @@ -424,7 +485,7 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper display: none; inset: 0; border: 1px solid transparent; - border-radius: 3px; + border-radius: var(--uui-border-radius); box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.7), inset 0 0 0 1px rgba(255, 255, 255, 0.7); @@ -442,6 +503,15 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper .umb-block-grid__block { height: 100%; } + + :host([settings-invalid])::after, + :host([content-invalid])::after { + border-color: var(--uui-color-danger); + } + + uui-badge { + z-index: 2; + } `, ]; } 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 0fdd6f4cdb..15427f71f9 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 @@ -16,9 +16,10 @@ import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/ 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'; +import { UmbFormControlMixin, UmbValidationContext } from '@umbraco-cms/backoffice/validation'; import type { UmbBlockTypeGroup } from '@umbraco-cms/backoffice/block-type'; import type { UmbBlockGridTypeModel, UmbBlockGridValueModel } from '@umbraco-cms/backoffice/block-grid'; +import { UmbBlockElementDataValidationPathTranslator } from '@umbraco-cms/backoffice/block'; /** * @element umb-property-editor-ui-block-grid @@ -28,6 +29,9 @@ export class UmbPropertyEditorUIBlockGridElement extends UmbFormControlMixin(UmbLitElement) implements UmbPropertyEditorUiElement { + #validationContext = new UmbValidationContext(this).provide(); + #contentDataPathTranslator?: UmbBlockElementDataValidationPathTranslator; + #settingsDataPathTranslator?: UmbBlockElementDataValidationPathTranslator; #context = new UmbBlockGridManagerContext(this); // private _value: UmbBlockGridValueModel = { @@ -73,6 +77,32 @@ export class UmbPropertyEditorUIBlockGridElement constructor() { super(); + this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => { + this.observe( + context.dataPath, + (dataPath) => { + // Translate paths for content elements: + this.#contentDataPathTranslator?.destroy(); + if (dataPath) { + // Set the data path for the local validation context: + this.#validationContext.setDataPath(dataPath); + + this.#contentDataPathTranslator = new UmbBlockElementDataValidationPathTranslator(this, 'contentData'); + } + + // Translate paths for settings elements: + this.#settingsDataPathTranslator?.destroy(); + if (dataPath) { + // Set the data path for the local validation context: + this.#validationContext.setDataPath(dataPath); + + this.#settingsDataPathTranslator = new UmbBlockElementDataValidationPathTranslator(this, 'settingsData'); + } + }, + 'observeDataPath', + ); + }); + // TODO: Prevent initial notification from these observes this.consumeContext(UMB_PROPERTY_CONTEXT, (propertyContext) => { this.observe(