diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context.ts index c9fc559a73f1..423daede3a63 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context.ts @@ -35,6 +35,9 @@ import { type UmbBlockDataModel, } from '@umbraco-cms/backoffice/block'; +// Default assumption when we can't determine if block has properties +const DEFAULT_BLOCK_HAS_PROPERTIES = true; + interface UmbBlockGridAreaTypeInvalidRuleType { groupKey?: string; key?: string; @@ -80,6 +83,9 @@ export class UmbBlockGridEntriesContext #hasTypeLimits = new UmbBooleanState(undefined); public readonly hasTypeLimits = this.#hasTypeLimits.asObservable(); + #singleBlockTypeHasProperties = new UmbBooleanState(undefined); + public readonly singleBlockTypeHasProperties = this.#singleBlockTypeHasProperties.asObservable(); + firstAllowedBlockTypeName() { if (!this._manager) { throw new Error('Manager not ready'); @@ -401,6 +407,47 @@ export class UmbBlockGridEntriesContext if (!this._manager) return; this.#allowedBlockTypes.setValue(this.#retrieveAllowedElementTypes()); this.#setupAllowedBlockTypesLimits(); + this.#setupSingleBlockTypePropertyCheck(); + } + + #setupSingleBlockTypePropertyCheck() { + if (!this._manager) return; + + // Observe allowed block types to check if single type has properties + this.observe( + this.allowedBlockTypes, + (blockTypes) => { + if (blockTypes.length === 1) { + const elementKey = blockTypes[0].contentElementTypeKey; + + // Wait for content types to be loaded before checking structure + this._manager?.contentTypesLoaded.then(() => { + const structure = this._manager?.getStructure(elementKey); + + if (structure) { + this.observe( + structure.contentTypeHasProperties, + (hasProperties) => { + this.#singleBlockTypeHasProperties.setValue(hasProperties ?? DEFAULT_BLOCK_HAS_PROPERTIES); + }, + 'observeSingleBlockTypeHasProperties', + ); + } else { + // If we can't get the structure, assume it has properties (safe default) + this.#singleBlockTypeHasProperties.setValue(DEFAULT_BLOCK_HAS_PROPERTIES); + } + }).catch(() => { + // If loading fails, assume it has properties (safe default) + this.#singleBlockTypeHasProperties.setValue(DEFAULT_BLOCK_HAS_PROPERTIES); + }); + } else { + // Not a single block type scenario, clear the state + this.#singleBlockTypeHasProperties.setValue(undefined); + this.removeUmbControllerByAlias('observeSingleBlockTypeHasProperties'); + } + }, + 'observeAllowedBlockTypesForPropertyCheck', + ); } #setupRangeLimits() { @@ -438,6 +485,14 @@ export class UmbBlockGridEntriesContext const allowedBlockTypes = this.#allowedBlockTypes.getValue(); if (allowedBlockTypes?.length === 1) { const elementKey = allowedBlockTypes[0].contentElementTypeKey; + const hasProperties = this.#singleBlockTypeHasProperties.getValue(); + + // Only append the workspace modal path if the block type has properties + // If it has no properties (only areas), return undefined to trigger inline creation + if (hasProperties === false) { + return undefined; + } + return pathBuilder({ view: 'create', index: index }) + 'modal/umb-modal-workspace/create/' + elementKey; } 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 b12673e6558e..6f79cf735c06 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 @@ -1,7 +1,7 @@ import type { UmbBlockGridEntryElement } from '../block-grid-entry/block-grid-entry.element.js'; import type { UmbBlockGridLayoutModel } from '../../types.js'; import { UmbBlockGridEntriesContext } from './block-grid-entries.context.js'; -import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, ifDefined, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { getAccumulatedValueOfIndex, getInterpolatedIndexOfPositionInWeightMap, @@ -194,6 +194,9 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen @state() private _styleElement?: HTMLLinkElement; + @state() + private _allowedBlockTypes: Array = []; + constructor() { super(); @@ -227,6 +230,14 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen null, ); + this.observe( + this.#context.allowedBlockTypes, + (blockTypes) => { + this._allowedBlockTypes = blockTypes; + }, + null, + ); + this.observe( this.#context.rangeLimits, (rangeLimits) => { @@ -383,6 +394,26 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen } // TODO: Missing ability to jump directly to creating a Block, when there is only one Block Type. [NL] + + async #createBlockInline(index: number) { + if (this._allowedBlockTypes.length !== 1) return; + + const elementKey = this._allowedBlockTypes[0].contentElementTypeKey; + const parentUnique = this.#context.getParentUnique(); + const originData = { + index: index, + areaKey: this._areaKey ?? null, + parentUnique: parentUnique ?? null, + }; + + const created = await this.#context.create(elementKey, {} as any, originData); + if (created) { + await this.#context.insert(created.layout, created.content, created.settings, originData); + } else { + throw new Error('Failed to create block'); + } + } + override render() { return html` ${this._styleElement} @@ -414,10 +445,17 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen `; } else if (this._isReadOnly === false) { + const createPath = this.#context.getPathForCreateBlock(-1); return html` + href=${ifDefined(createPath)} + label=${this.localize.term('blockEditor_addBlock')} + @click=${async (e: Event) => { + if (!createPath && this._allowedBlockTypes.length === 1) { + e.preventDefault(); + await this.#createBlockInline(-1); + } + }}> `; } else { return nothing; @@ -427,13 +465,20 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen #renderCreateButton() { if (this._isReadOnly && this._layoutEntries.length > 0) return nothing; + const createPath = this.#context.getPathForCreateBlock(-1); return html` + href=${ifDefined(createPath)} + ?disabled=${this._isReadOnly} + @click=${async (e: Event) => { + if (!createPath && this._allowedBlockTypes.length === 1) { + e.preventDefault(); + await this.#createBlockInline(-1); + } + }}> `; } 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 547fd3cb508b..02ba61b4b0a9 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 @@ -38,6 +38,9 @@ import { UMB_VARIANT_CONTEXT } from '@umbraco-cms/backoffice/variant'; import '../../components/block-list-entry/index.js'; +// Default assumption when we can't determine if block has properties +const DEFAULT_BLOCK_HAS_PROPERTIES = true; + const SORTER_CONFIG: UmbSorterConfig = { getUniqueOfElement: (element) => { return element.contentKey!; @@ -177,6 +180,9 @@ export class UmbPropertyEditorUIBlockListElement @state() private _useInlineEditingAsDefault?: boolean; + @state() + private _singleBlockTypeHasProperties?: boolean; + constructor() { super(); @@ -317,6 +323,36 @@ export class UmbPropertyEditorUIBlockListElement this.#managerContext.blockTypes, (blockTypes) => { this._blocks = blockTypes; + + // Check if single block type has properties + if (blockTypes.length === 1) { + const elementKey = blockTypes[0].contentElementTypeKey; + + // Wait for content types to be loaded before checking structure + this.#managerContext.contentTypesLoaded.then(() => { + const structure = this.#managerContext.getStructure(elementKey); + + if (structure) { + this.observe( + structure.contentTypeHasProperties, + (hasProperties) => { + this._singleBlockTypeHasProperties = hasProperties ?? DEFAULT_BLOCK_HAS_PROPERTIES; + }, + 'observeSingleBlockTypeHasProperties', + ); + } else { + // If we can't get the structure, assume it has properties (safe default) + this._singleBlockTypeHasProperties = DEFAULT_BLOCK_HAS_PROPERTIES; + } + }).catch(() => { + // If loading fails, assume it has properties (safe default) + this._singleBlockTypeHasProperties = DEFAULT_BLOCK_HAS_PROPERTIES; + }); + } else { + // Not a single block type scenario, clear the state + this._singleBlockTypeHasProperties = undefined; + this.removeUmbControllerByAlias('observeSingleBlockTypeHasProperties'); + } }, null, ); @@ -433,8 +469,13 @@ export class UmbPropertyEditorUIBlockListElement #getPathForCreateBlock(index: number): string | undefined { if (this._blocks?.length === 1) { const elementKey = this._blocks[0].contentElementTypeKey; + const hasProperties = this._singleBlockTypeHasProperties; + if (this._useInlineEditingAsDefault) { return undefined; + } else if (hasProperties === false) { + // Block has no properties (only areas), skip the modal + return undefined; } else { return ( this._catalogueRouteBuilder?.({ view: 'create', index: index }) + @@ -457,13 +498,18 @@ export class UmbPropertyEditorUIBlockListElement href=${ifDefined(createPath)} ?disabled=${this.readonly} @click=${async () => { - if (this._blocks?.length === 1 && this._useInlineEditingAsDefault) { - const originData = { index: -1 }; - const created = await this.#entriesContext.create(this._blocks[0].contentElementTypeKey, {}, originData); - if (created) { - this.#entriesContext.insert(created.layout, created.content, created.settings, originData); - } else { - throw new Error('Failed to create block'); + if (this._blocks?.length === 1) { + const hasProperties = this._singleBlockTypeHasProperties; + + // Create inline if: inline editing is enabled OR block has no properties + if (this._useInlineEditingAsDefault || hasProperties === false) { + const originData = { index: -1 }; + const created = await this.#entriesContext.create(this._blocks[0].contentElementTypeKey, {}, originData); + if (created) { + this.#entriesContext.insert(created.layout, created.content, created.settings, originData); + } else { + throw new Error('Failed to create block'); + } } } }}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-sign/components/entity-sign-bundle.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-sign/components/entity-sign-bundle.element.ts index 2fe6de0517ae..3ec7113708de 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-sign/components/entity-sign-bundle.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-sign/components/entity-sign-bundle.element.ts @@ -148,7 +148,8 @@ export class UmbEntitySignBundleElement extends UmbLitElement { (c) => c.alias, (c, i) => { return html`
- ${c.component}${this.localize.string(this._labels.get(c.alias) ?? '')} + ${c.component}${this.localize.string(this._labels.get(c.alias) ?? '')}
`; }, ) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-picker-modal/icon-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-picker-modal/icon-picker-modal.element.ts index 726a9a241983..e704fef32af2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-picker-modal/icon-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-picker-modal/icon-picker-modal.element.ts @@ -81,8 +81,7 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement
- ${this.renderSearch()} - ${this.renderColors()} + ${this.renderSearch()} ${this.renderColors()} ${this.data?.showEmptyOption && !this._isSearching ? html` @@ -137,21 +136,18 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement - ${ - this._colorList.map( - (color) => html` - - - `, - ) - } + ${this._colorList.map( + (color) => html` + + + `, + )} -
- `; +
`; } renderIcons() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/conditions/allow-delete/data-type-allow-delete-action.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/conditions/allow-delete/data-type-allow-delete-action.condition.ts index 5275983dfb2c..0687c7400ba1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/conditions/allow-delete/data-type-allow-delete-action.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/conditions/allow-delete/data-type-allow-delete-action.condition.ts @@ -1,9 +1,9 @@ -import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity'; import { UMB_DATA_TYPE_ITEM_REPOSITORY_ALIAS } from '../../../constants.js'; +import type { UmbDataTypeItemRepository } from '../../../repository/index.js'; +import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity'; import { UmbConditionBase, createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbDataTypeItemRepository } from '../../../repository/index.js'; export class UmbDataTypeAllowDeleteActionCondition extends UmbConditionBase implements UmbExtensionCondition { constructor(host: UmbControllerHost, args: UmbConditionControllerArguments) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/manifests.ts index 45c0d53ef68e..f148eb156d82 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/manifests.ts @@ -7,9 +7,9 @@ import { import { manifests as createManifests } from './create/manifests.js'; import { manifests as moveManifests } from './move-to/manifests.js'; import { manifests as duplicateManifests } from './duplicate/manifests.js'; -import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; import { UMB_DATA_TYPE_ALLOW_DELETE_CONDITION_ALIAS } from './conditions/allow-delete/index.js'; import { manifests as conditionManifests } from './conditions/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ {