Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot I dont think this works. It would need to call a method, when its not suppose to open a modal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right! I've fixed this in commit c318a42.

The Block Grid now properly creates blocks inline when they have no properties:

  • Context returns undefined (not a catalogue path) when block has no properties
  • Element detects undefined path and calls #createBlockInline() method
  • Method uses context.create() and context.insert() to create the block directly
  • Click handler prevents default navigation when path is undefined

This matches the Block List implementation where blocks are created inline without opening any modal.

return undefined;
}

return pathBuilder({ view: 'create', index: index }) + 'modal/umb-modal-workspace/create/' + elementKey;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { UmbBlockGridEntryElement } from '../block-grid-entry/block-grid-entry.element.js';

Check notice on line 1 in src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (v17/bugfix/20704)

✅ Getting better: Overall Code Complexity

The mean cyclomatic complexity decreases from 4.21 to 4.20, threshold = 4. This file has many conditional statements (e.g. if, for, while) across its implementation, leading to lower code health. Avoid adding more conditionals.
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,
Expand Down Expand Up @@ -194,6 +194,9 @@
@state()
private _styleElement?: HTMLLinkElement;

@state()
private _allowedBlockTypes: Array<any> = [];

constructor() {
super();

Expand Down Expand Up @@ -227,6 +230,14 @@
null,
);

this.observe(
this.#context.allowedBlockTypes,
(blockTypes) => {
this._allowedBlockTypes = blockTypes;
},
null,
);

Check warning on line 240 in src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (v17/bugfix/20704)

❌ Getting worse: Complex Method

UmbBlockGridEntriesElement.constructor already has high cyclomatic complexity, and now it increases in Lines of Code from 84 to 91. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
this.observe(
this.#context.rangeLimits,
(rangeLimits) => {
Expand Down Expand Up @@ -383,6 +394,26 @@
}

// 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}
Expand Down Expand Up @@ -414,10 +445,17 @@
</uui-button-group>
`;
} else if (this._isReadOnly === false) {
const createPath = this.#context.getPathForCreateBlock(-1);
return html`
<uui-button-inline-create
href=${this.#context.getPathForCreateBlock(-1) ?? ''}
label=${this.localize.term('blockEditor_addBlock')}></uui-button-inline-create>
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);
}
}}></uui-button-inline-create>
`;
} else {
return nothing;
Expand All @@ -427,13 +465,20 @@
#renderCreateButton() {
if (this._isReadOnly && this._layoutEntries.length > 0) return nothing;

const createPath = this.#context.getPathForCreateBlock(-1);
return html`
<uui-button
look="placeholder"
color=${this.pristine === false && this.validity.valid === false ? 'invalid' : 'default'}
label=${this._configCreateLabel ?? this._createLabel ?? ''}
href=${this.#context.getPathForCreateBlock(-1) ?? ''}
?disabled=${this._isReadOnly}></uui-button>
href=${ifDefined(createPath)}
?disabled=${this._isReadOnly}
@click=${async (e: Event) => {
if (!createPath && this._allowedBlockTypes.length === 1) {
e.preventDefault();
await this.#createBlockInline(-1);
}
}}></uui-button>
`;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@

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<UmbBlockListLayoutModel, UmbBlockListEntryElement> = {
getUniqueOfElement: (element) => {
return element.contentKey!;
Expand Down Expand Up @@ -177,6 +180,9 @@
@state()
private _useInlineEditingAsDefault?: boolean;

@state()
private _singleBlockTypeHasProperties?: boolean;

constructor() {
super();

Expand Down Expand Up @@ -317,6 +323,36 @@
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');
}

Check warning on line 355 in src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (v17/bugfix/20704)

❌ Getting worse: Complex Method

UmbPropertyEditorUIBlockListElement.constructor increases in cyclomatic complexity from 21 to 24, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.

Check warning on line 355 in src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (v17/bugfix/20704)

❌ New issue: Bumpy Road Ahead

UmbPropertyEditorUIBlockListElement.constructor has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is 2 blocks per function. The Bumpy Road code smell is a function that contains multiple chunks of nested conditional logic. The deeper the nesting and the more bumps, the lower the code health.
},
null,
);
Expand Down Expand Up @@ -433,8 +469,13 @@
#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 }) +
Expand All @@ -457,13 +498,18 @@
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');
}
}
}
}}></uui-button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ export class UmbEntitySignBundleElement extends UmbLitElement {
(c) => c.alias,
(c, i) => {
return html`<div class="sign-container ${i > 1 ? 'hide-in-overview' : ''}" style=${`--i:${i}`}>
<span class="badge-icon">${c.component}</span><span class="label">${this.localize.string(this._labels.get(c.alias) ?? '')}</span>
<span class="badge-icon">${c.component}</span
><span class="label">${this.localize.string(this._labels.get(c.alias) ?? '')}</span>
</div>`;
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,7 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
return html`
<umb-body-layout headline=${this.localize.term('defaultdialogs_selectIcon')}>
<div id="container">
${this.renderSearch()}
${this.renderColors()}
${this.renderSearch()} ${this.renderColors()}
<uui-scroll-container id="icons">
${this.data?.showEmptyOption && !this._isSearching
? html`
Expand Down Expand Up @@ -137,21 +136,18 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
value=${ifDefined(this.value.color)}
label=${this.localize.term('defaultdialogs_colorSwitcher')}
@change=${this.#onColorChange}>
${
this._colorList.map(
(color) => html`
<uui-color-swatch
label=${this.localize.term('colors_' + toCamelCase(color.alias))}
title=${this.localize.term('colors_' + toCamelCase(color.alias))}
value=${color.alias}
style="--uui-swatch-color: var(${color.varName})">
</uui-color-swatch>
`,
)
}
${this._colorList.map(
(color) => html`
<uui-color-swatch
label=${this.localize.term('colors_' + toCamelCase(color.alias))}
title=${this.localize.term('colors_' + toCamelCase(color.alias))}
value=${color.alias}
style="--uui-swatch-color: var(${color.varName})">
</uui-color-swatch>
`,
)}
</uui-color-swatches>
<hr />
`;
<hr /> `;
}

renderIcons() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<never> implements UmbExtensionCondition {
constructor(host: UmbControllerHost, args: UmbConditionControllerArguments<never>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<UmbExtensionManifest | UmbExtensionManifestKind> = [
{
Expand Down