Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5139980
Initial plan
Copilot Dec 22, 2025
d322bef
Add tab badges for validation errors in block workspace
Copilot Dec 22, 2025
2044e23
Build successful - frontend changes compiled
Copilot Dec 22, 2025
5e79590
Merge branch 'main' into copilot/fix-validation-error-highlight
nielslyngsoe Feb 6, 2026
ff21836
remove double naming
nielslyngsoe Feb 6, 2026
05190e4
remove double naming in no router block workspace
nielslyngsoe Feb 6, 2026
4134325
fixing code
nielslyngsoe Feb 6, 2026
13d7be0
debugger
nielslyngsoe Feb 6, 2026
c8e361b
binding view contexts
nielslyngsoe Feb 6, 2026
047c60c
inline mode binding
nielslyngsoe Feb 6, 2026
e478fa9
fix routing to generic tab
nielslyngsoe Feb 9, 2026
1b9c56b
remove log
nielslyngsoe Feb 9, 2026
8aefa73
Update src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/v…
nielslyngsoe Feb 9, 2026
fa5acd1
Update src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/v…
nielslyngsoe Feb 9, 2026
e0bff0a
handle setup contexts for root properties
nielslyngsoe Feb 9, 2026
d6564cc
refactor flow
nielslyngsoe Feb 9, 2026
1495a9c
Merge branch 'main' into v17/bugfix/21178
nielslyngsoe Feb 9, 2026
5af4667
cherry-pick from #21672
nielslyngsoe Feb 10, 2026
5249c65
cherry pick tab rendering to handle one more case
nielslyngsoe Feb 10, 2026
276a2b5
Merge branch 'v17/hotfix/21476' into v17/bugfix/21178
nielslyngsoe Feb 10, 2026
5591dad
move the root route down for it to stay an empty path.
nielslyngsoe Feb 10, 2026
fb3c447
Merge branch 'v17/hotfix/21476' into v17/bugfix/21178
nielslyngsoe Feb 10, 2026
4b27eed
Merge branch 'main' into v17/bugfix/21178
nielslyngsoe Feb 10, 2026
f607330
Revert empty root path commit
nielslyngsoe Feb 10, 2026
fa945fd
fullPath for root includes 'root'
nielslyngsoe Feb 10, 2026
436afd1
revert claude settings commit
nielslyngsoe Feb 10, 2026
d20fe86
refactor accordingly to feedback
nielslyngsoe Feb 10, 2026
5d1568a
Merge branch 'v17/hotfix/21476' into v17/bugfix/21178
nielslyngsoe Feb 10, 2026
86f8821
renme to generic
nielslyngsoe Feb 10, 2026
aecd435
Merge branch 'v17/hotfix/rename-root-tab-to-generic' into v17/hotfix/…
nielslyngsoe Feb 10, 2026
b9caf27
only use label
nielslyngsoe Feb 10, 2026
bd349cf
Merge branch 'v17/hotfix/rename-root-tab-to-generic' into v17/hotfix/…
nielslyngsoe Feb 10, 2026
d7f5ce7
Merge branch 'release/17.2' into v17/hotfix/21476
nielslyngsoe Feb 10, 2026
86399fd
Merge branch 'v17/hotfix/21476' into v17/bugfix/21178
nielslyngsoe Feb 10, 2026
6a8f6ee
Merge branch 'main' into v17/bugfix/21178
nielslyngsoe Feb 10, 2026
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 @@ -22,7 +22,7 @@ import {
import { UmbReadOnlyVariantGuardManager } from '@umbraco-cms/backoffice/utils';
import { UmbDataTypeItemRepositoryManager } from '@umbraco-cms/backoffice/data-type';
import { UmbVariantPropertyGuardManager } from '@umbraco-cms/backoffice/property';
import { UmbHintContext, type UmbVariantHint } from '@umbraco-cms/backoffice/hint';
import { UmbViewContext } from '@umbraco-cms/backoffice/view';

export class UmbBlockElementManager<LayoutDataType extends UmbBlockLayoutBaseModel = UmbBlockLayoutBaseModel>
extends UmbControllerBase
Expand Down Expand Up @@ -66,7 +66,7 @@ export class UmbBlockElementManager<LayoutDataType extends UmbBlockLayoutBaseMod

readonly validation = new UmbValidationController(this);

readonly hints;
readonly view;

constructor(
host: UmbBlockWorkspaceContext<LayoutDataType>,
Expand All @@ -75,11 +75,14 @@ export class UmbBlockElementManager<LayoutDataType extends UmbBlockLayoutBaseMod
) {
super(host);

this.hints = new UmbHintContext<UmbVariantHint>(this, { viewAlias: workspaceViewAlias });
this.hints.inherit();
new UmbContentValidationToHintsManager<UmbContentTypeModel>(this, this.structure, this.validation, this.hints, [
workspaceViewAlias,
]);
this.view = new UmbViewContext(this, workspaceViewAlias);
new UmbContentValidationToHintsManager<UmbContentTypeModel>(
this,
this.structure,
this.validation,
this.view.hints,
[],
);

// Ugly, but we just inherit these from the workspace context: [NL]
this.name = host.name;
Expand Down Expand Up @@ -264,7 +267,7 @@ export class UmbBlockElementManager<LayoutDataType extends UmbBlockLayoutBaseMod
// Provide Validation Context for this view:
this.validation.provideAt(host);

this.hints.provideAt(host);
this.view.provideAt(host);
}

public override destroy(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM

window.addEventListener('willchangestate', this.#onWillNavigate);

this.content.view.inheritFrom(this.view);
this.settings.view.inheritFrom(this.view);

this.addValidationContext(this.content.validation);
this.addValidationContext(this.settings.validation);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import { UMB_BLOCK_WORKSPACE_CONTEXT } from '../../block-workspace.context-token.js';

Check warning on line 1 in src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Overall Code Complexity

This module has a mean cyclomatic complexity of 4.70 across 10 functions. The mean complexity threshold is 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 './block-workspace-view-edit-tab.element.js';
import type { UmbBlockLayoutBaseModel } from '../../../types.js';
import type UmbBlockElementManager from '../../block-element-manager.js';
import { css, html, customElement, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbPropertyTypeContainerMergedModel } from '@umbraco-cms/backoffice/content-type';
import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace';
import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint';
import { UmbViewController } from '@umbraco-cms/backoffice/view';
import { encodeFolderName } from '@umbraco-cms/backoffice/router';

/**
* @internal only for use inside this class.
* Gets the view alias for a given tab. This is used to create a unique view context for each tab in the block workspace.
* @param {UmbPropertyTypeContainerMergedModel} tab - The tab to get the view alias for.
* @returns {string} The view alias for the tab.
*/
function getViewAliasForTab(tab: UmbPropertyTypeContainerMergedModel): string {
return 'tab/' + encodeFolderName(tab.name ?? '');
}

/**
* @element umb-block-workspace-view-edit-content-no-router
Expand All @@ -24,34 +39,41 @@
private _tabs?: Array<UmbPropertyTypeContainerMergedModel>;

@state()
private _activeTabKey?: string | null | undefined;
private _activeTabKey?: string | null;

#blockWorkspace?: typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE;
#blockManager?: UmbBlockElementManager<UmbBlockLayoutBaseModel>;
#tabsStructureHelper = new UmbContentTypeContainerStructureHelper(this);

@state()
private _hintMap: Map<string | null, UmbVariantHint> = new Map();

#tabViewContexts: Array<UmbViewController> = [];

constructor() {
super();

this.#tabsStructureHelper.setIsRoot(true);
this.#tabsStructureHelper.setContainerChildType('Tab');
this.observe(this.#tabsStructureHelper.childContainers, (tabs) => {
this._tabs = tabs;
this.#checkDefaultTabName();
this.#setupViewContexts();
});

this.observe(
this.#tabsStructureHelper.hasProperties,
(hasRootProperties) => {
this._hasRootProperties = hasRootProperties;
this.#checkDefaultTabName();
this.#setupViewContexts();
},
'observeRootProperties',
);

this.consumeContext(UMB_BLOCK_WORKSPACE_CONTEXT, (context) => {
this.#blockWorkspace = context;
this.#blockManager = context?.content;
// block manager does not need to be setup this in file as that it being done by the implementation of this element.
this.#tabsStructureHelper.setStructureManager(context?.content.structure);

this.#observeRootGroups();
});
}
Expand All @@ -60,31 +82,118 @@
if (!this.#blockWorkspace) return;

this.observe(
await this.#blockWorkspace.content.structure.hasRootContainers('Group'),
await this.#blockManager?.structure.hasRootContainers('Group'),
(hasRootGroups) => {
this._hasRootGroups = hasRootGroups;
this.#checkDefaultTabName();
this._hasRootGroups = hasRootGroups ?? false;
this.#setupViewContexts();
},
'observeGroups',
);
}

#setupViewContexts() {
if (!this._tabs || !this.#blockManager) return;

// Create view contexts for root groups/properties
if (this._hasRootGroups || this._hasRootProperties) {
this.#createViewContext(null, '#general_generic');
}

// Create view contexts for all tabs
this._tabs.forEach((tab) => {
const viewAlias = getViewAliasForTab(tab);
this.#createViewContext(viewAlias, tab.name ?? '');
});

this.#checkDefaultTabName();
}

#createViewContext(viewAlias: string | null, tabName: string) {
if (!this.#blockManager) {
throw new Error('Block Manager not found');
}
if (!this.#tabViewContexts.find((context) => context.viewAlias === viewAlias)) {
const view = new UmbViewController(this, viewAlias);
this.#tabViewContexts.push(view);

if (viewAlias === null) {
// for the root tab, we need to filter hints
view.hints.setPathFilter((paths) => {
const firstPath = paths[0];
// Treat empty paths as "not in a tab", so they belong to the root tab
if (!firstPath) {
return true;
}
return firstPath.includes('tab/') === false;
});
}

view.setTitle(tabName);
view.inheritFrom(this.#blockManager.view);

this.observe(
view.firstHintOfVariant,
(hint) => {
if (hint) {
this._hintMap.set(viewAlias, hint);
} else {
this._hintMap.delete(viewAlias);
}
this.requestUpdate('_hintMap');
},
'umbObserveState_' + viewAlias,
);
}
}

#checkDefaultTabName() {
if (!this._tabs || !this.#blockWorkspace) return;

// Find the default tab to grab
if (this._activeTabKey === undefined) {
if (this._hasRootGroups || this._hasRootProperties) {
this._activeTabKey = null;
const context = this.#tabViewContexts.find((context) => context.viewAlias === null);
if (context) {
this._activeTabKey = null;
this.#provideViewContext(null);
}
} else if (this._tabs.length > 0) {
const tab = this._tabs[0];
this._activeTabKey = tab.ownerId ?? tab.ids[0];
if (tab) {
this._activeTabKey = tab.ownerId ?? tab.ids[0];
this.#provideViewContext(getViewAliasForTab(tab));
}

Check warning on line 165 in src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Complex Method

UmbBlockWorkspaceViewEditContentNoRouterElement.checkDefaultTabName has a cyclomatic complexity of 9, 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 165 in src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Bumpy Road Ahead

UmbBlockWorkspaceViewEditContentNoRouterElement.checkDefaultTabName 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.
}
}
}

#setTabKey(tabKey: string | null | undefined) {
this._activeTabKey = tabKey;
#setCurrentTabPath(tabKey: string | null, viewAlias: string | null) {
// find the key of the view context that we want to show based on the path, and set it to active
const context = this.#tabViewContexts.find((context) => context.viewAlias === viewAlias);
if (context) {
this._activeTabKey = tabKey;
this.#provideViewContext(viewAlias);
}
}

#currentProvidedView?: UmbViewController;

#provideViewContext(viewAlias: string | null) {
const view = this.#tabViewContexts.find((context) => context.viewAlias === viewAlias);
if (this.#currentProvidedView === view) {
return;
}
this.#currentProvidedView?.unprovide();
if (!view) {
throw new Error(`View context with alias ${viewAlias} not found`);
}
this.#currentProvidedView = view;
// ViewAlias null is only for the root tab, therefor we can implement this hack.
if (viewAlias === null) {
// Specific hack for the Generic tab to only show its name if there are other tabs.
view.setTitle(this._tabs && this._tabs?.length > 0 ? '#general_generic' : undefined);
}
view.provideAt(this);

Check warning on line 196 in src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Complex Method

UmbBlockWorkspaceViewEditContentNoRouterElement.provideViewContext has a cyclomatic complexity of 9, 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.
}

override render() {
Expand All @@ -94,23 +203,15 @@
${this._tabs.length > 1 || (this._tabs.length === 1 && (this._hasRootGroups || this._hasRootProperties))
? html`<uui-tab-group slot="header">
${(this._hasRootGroups || this._hasRootProperties) && this._tabs.length > 0
? html`<uui-tab
label=${this.localize.term('general_generic')}
.active=${this._activeTabKey === null}
@click=${() => this.#setTabKey(null)}></uui-tab>`
? this.#renderTab(null, null, '#general_generic')
: nothing}
${repeat(
this._tabs,
(tab) => tab.name,
(tab) => {
const tabKey = tab.ownerId ?? tab.ids[0];

return html`<uui-tab
label=${this.localize.string(tab.name ?? '#general_unnamed')}
.active=${this._activeTabKey === tabKey}
@click=${() => this.#setTabKey(tabKey)}
>${tab.name}</uui-tab
>`;
const viewAlias = 'tab/' + encodeFolderName(tab.name || '');
return this.#renderTab(tabKey, viewAlias, tab.name);
},
)}
</uui-tab-group>`
Expand All @@ -125,6 +226,22 @@
`;
}

#renderTab(tabKey: string | null, viewAlias: string | null, name: string) {
const hint = this._hintMap.get(viewAlias);
const active = this._activeTabKey === tabKey;
return html`<uui-tab
label=${this.localize.string(name ?? '#general_unnamed')}
.active=${active}
@click=${() => this.#setCurrentTabPath(tabKey, viewAlias)}
data-mark="content-tab:${viewAlias ?? 'root'}"
>${hint && !active
? html`<umb-badge slot="extra" .color=${hint.color ?? 'default'} ?attention=${hint.color === 'invalid'}
>${hint.text}</umb-badge
>`
: nothing}</uui-tab
>`;
}

static override styles = [
UmbTextStyles,
css`
Expand All @@ -136,6 +253,9 @@

padding: calc(var(--uui-size-layout-1));
}
umb-badge {
--uui-badge-inset: 0 0 auto auto;
}
`,
];
}
Expand Down
Loading
Loading