From 90a2525ed5f808c488eeb2715878aedabe203f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 17 Apr 2026 13:20:44 +0200 Subject: [PATCH 1/5] View Contexts for Dashboards + Section Views to support Browser Title and Hints --- .../section-main-views.element.ts | 127 ++++++++++++++++-- 1 file changed, 113 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts index a9bcb5a78b9a..79f85f4fcdae 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts @@ -1,12 +1,15 @@ -import type { ManifestSectionView, UmbSectionViewElement } from '../extensions/index.js'; +import type { ManifestSectionView } from '../extensions/index.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, nothing, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router'; -import type { ManifestDashboard, UmbDashboardElement } from '@umbraco-cms/backoffice/dashboard'; +import type { ManifestDashboard } from '@umbraco-cms/backoffice/dashboard'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbExtensionsManifestInitializer, createExtensionElement } from '@umbraco-cms/backoffice/extension-api'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { pathFolderName } from '@umbraco-cms/backoffice/utils'; +import { UmbViewContext } from '@umbraco-cms/backoffice/view'; +import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint'; @customElement('umb-section-main-views') export class UmbSectionMainViewElement extends UmbLitElement { @@ -31,6 +34,13 @@ export class UmbSectionMainViewElement extends UmbLitElement { @state() private _routes: Array = []; + @state() + private _hintMap: Map = new Map(); + + #viewContexts = new Map(); + #hintObservers: Array = []; + #currentProvidedView?: UmbViewContext; + constructor() { super(); @@ -45,6 +55,46 @@ export class UmbSectionMainViewElement extends UmbLitElement { }); } + #getOrCreateViewContext(alias: string): UmbViewContext { + let context = this.#viewContexts.get(alias); + if (!context) { + context = new UmbViewContext(this, alias); + context.inherit(); + this.#viewContexts.set(alias, context); + } + return context; + } + + #cleanupViewContexts(viewAliases: Set) { + // Remove contexts that are no longer needed + for (const [alias, context] of this.#viewContexts) { + if (!viewAliases.has(alias)) { + context.destroy(); + this.#viewContexts.delete(alias); + } + } + this.#observeHints(); + } + + #observeHints() { + this.#hintObservers.forEach((observer) => observer.destroy()); + this._hintMap = new Map(); + this.#hintObservers = [...this.#viewContexts.entries()].map(([alias, context], index) => + this.observe( + context.firstHintOfVariant, + (hint) => { + if (hint) { + this._hintMap.set(alias, hint); + } else { + this._hintMap.delete(alias); + } + this.requestUpdate('_hintMap'); + }, + 'umbObserveHint_' + index, + ), + ); + } + #constructDashboardPath(manifest: ManifestDashboard) { const dashboardName = manifest.meta.label ?? manifest.name ?? manifest.alias; return 'dashboard/' + (manifest.meta.pathname ? manifest.meta.pathname : pathFolderName(dashboardName)); @@ -55,27 +105,55 @@ export class UmbSectionMainViewElement extends UmbLitElement { return 'view/' + (manifest.meta.pathname ? manifest.meta.pathname : pathFolderName(viewName)); } + #getViewName(view: ManifestSectionView) { + return view.meta?.label ? this.localize.string(view.meta.label) : (view.name ?? view.alias); + } + async #createRoutes() { + const viewAliases = new Set(); + const dashboardRoutes = this._dashboards?.map((manifest) => { + viewAliases.add(manifest.alias); + const context = this.#getOrCreateViewContext(manifest.alias); + context.setTitle(this.#getDashboardName(manifest)); return { path: this.#constructDashboardPath(manifest), component: () => createExtensionElement(manifest), - setup: (component: UmbDashboardElement) => { - component.manifest = manifest; + setup: (component?: any) => { + if (this.#currentProvidedView !== context) { + this.#currentProvidedView?.unprovide(); + } + if (component) { + this.#currentProvidedView = context; + context.provideAt(component); + component.manifest = manifest; + } }, } as UmbRoute; }); const viewRoutes = this._views?.map((manifest) => { + viewAliases.add(manifest.alias); + const context = this.#getOrCreateViewContext(manifest.alias); + context.setTitle(this.#getViewName(manifest)); return { path: this.#constructViewPath(manifest), component: () => createExtensionElement(manifest), - setup: (component: UmbSectionViewElement) => { - component.manifest = manifest; + setup: (component?: any) => { + if (this.#currentProvidedView !== context) { + this.#currentProvidedView?.unprovide(); + } + if (component) { + this.#currentProvidedView = context; + context.provideAt(component); + component.manifest = manifest; + } }, } as UmbRoute; }); + this.#cleanupViewContexts(viewAliases); + const routes = [...dashboardRoutes, ...viewRoutes]; if (routes.length > 0) { this._defaultView = routes[0].path; @@ -122,16 +200,20 @@ export class UmbSectionMainViewElement extends UmbLitElement { ${this._dashboards.map((dashboard) => { const dashboardPath = this.#constructDashboardPath(dashboard); + const dashboardName = this.#getDashboardName(dashboard); + const hint = this._hintMap.get(dashboard.alias); // If this path matches, or if this is the default view and the active path is empty. const isActive = this._activePath === dashboardPath || (this._defaultView === dashboardPath && this._activePath === ''); return html` - ${this.#getDashboardName(dashboard)} + + ${dashboardName} + ${hint && !isActive + ? html`${hint.text}` + : nothing} + `; })} @@ -145,14 +227,22 @@ export class UmbSectionMainViewElement extends UmbLitElement { ? html` ${this._views.map((view) => { - const viewName = view.meta.label ? this.localize.string(view.meta.label) : (view.name ?? view.alias); + const viewName = this.#getViewName(view); const viewPath = this.#constructViewPath(view); + const hint = this._hintMap.get(view.alias); // If this path matches, or if this is the default view and the active path is empty. const isActive = this._activePath === viewPath || (this._defaultView === viewPath && this._activePath === ''); return html` - +
+ + ${hint && !isActive + ? html`${hint.text}` + : nothing} +
${viewName}
`; @@ -179,6 +269,15 @@ export class UmbSectionMainViewElement extends UmbLitElement { #views uui-tab:first-child { border-left: 1px solid var(--uui-color-divider-standalone); } + + div[slot='icon'] { + position: relative; + } + + umb-badge { + font-size: var(--uui-type-small-size); + right: -1.5em; + } `, ]; } From 9ecc4058982eaa9083ede4907f0a1aa4f3fa4c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 4 May 2026 17:21:37 +0200 Subject: [PATCH 2/5] fix code --- .../hint-workspace-view.ts | 2 -- .../section-main-views.element.ts | 25 +++++++++++-------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/examples/workspace-view-hint/hint-workspace-view.ts b/src/Umbraco.Web.UI.Client/examples/workspace-view-hint/hint-workspace-view.ts index 6f5b7775c2b5..b88e911f56d3 100644 --- a/src/Umbraco.Web.UI.Client/examples/workspace-view-hint/hint-workspace-view.ts +++ b/src/Umbraco.Web.UI.Client/examples/workspace-view-hint/hint-workspace-view.ts @@ -1,8 +1,6 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, customElement, LitElement } from '@umbraco-cms/backoffice/external/lit'; import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api'; -import { UMB_WORKSPACE_VIEW_CONTEXT } from '@umbraco-cms/backoffice/workspace'; -import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/document'; @customElement('example-hint-workspace-view') diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts index 79f85f4fcdae..1ca929df48f9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts @@ -7,7 +7,7 @@ import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registr import { UmbExtensionsManifestInitializer, createExtensionElement } from '@umbraco-cms/backoffice/extension-api'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { pathFolderName } from '@umbraco-cms/backoffice/utils'; -import { UmbViewContext } from '@umbraco-cms/backoffice/view'; +import { UmbViewController } from '@umbraco-cms/backoffice/view'; import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint'; @@ -37,9 +37,9 @@ export class UmbSectionMainViewElement extends UmbLitElement { @state() private _hintMap: Map = new Map(); - #viewContexts = new Map(); + #viewContexts = new Map(); #hintObservers: Array = []; - #currentProvidedView?: UmbViewContext; + #currentProvidedView?: UmbViewController; constructor() { super(); @@ -55,10 +55,10 @@ export class UmbSectionMainViewElement extends UmbLitElement { }); } - #getOrCreateViewContext(alias: string): UmbViewContext { + #getOrCreateViewContext(alias: string): UmbViewController { let context = this.#viewContexts.get(alias); if (!context) { - context = new UmbViewContext(this, alias); + context = new UmbViewController(this, alias); context.inherit(); this.#viewContexts.set(alias, context); } @@ -81,7 +81,7 @@ export class UmbSectionMainViewElement extends UmbLitElement { this._hintMap = new Map(); this.#hintObservers = [...this.#viewContexts.entries()].map(([alias, context], index) => this.observe( - context.firstHintOfVariant, + context.hints.firstHint, (hint) => { if (hint) { this._hintMap.set(alias, hint); @@ -139,7 +139,7 @@ export class UmbSectionMainViewElement extends UmbLitElement { return { path: this.#constructViewPath(manifest), component: () => createExtensionElement(manifest), - setup: (component?: any) => { + setup: async (component?: any) => { if (this.#currentProvidedView !== context) { this.#currentProvidedView?.unprovide(); } @@ -208,8 +208,11 @@ export class UmbSectionMainViewElement extends UmbLitElement { return html` ${dashboardName} - ${hint && !isActive - ? html`${hint.text}` : nothing} @@ -275,8 +278,8 @@ export class UmbSectionMainViewElement extends UmbLitElement { } umb-badge { - font-size: var(--uui-type-small-size); - right: -1.5em; + top: var(--uui-size-2); + right: calc(var(--uui-size-2) * -1); } `, ]; From eba0826d6f83a361dc92096b9d40329fb0ae753e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 4 May 2026 17:37:35 +0200 Subject: [PATCH 3/5] use alias for observe ctrl alias --- .../section/section-main-views/section-main-views.element.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts index 1ca929df48f9..6d69fe8df4d5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts @@ -79,7 +79,7 @@ export class UmbSectionMainViewElement extends UmbLitElement { #observeHints() { this.#hintObservers.forEach((observer) => observer.destroy()); this._hintMap = new Map(); - this.#hintObservers = [...this.#viewContexts.entries()].map(([alias, context], index) => + this.#hintObservers = [...this.#viewContexts.entries()].map(([alias, context]) => this.observe( context.hints.firstHint, (hint) => { @@ -90,7 +90,7 @@ export class UmbSectionMainViewElement extends UmbLitElement { } this.requestUpdate('_hintMap'); }, - 'umbObserveHint_' + index, + 'umbObserveHint_' + alias, ), ); } From f8f373a24f4ee23dbdad531c919a880d848f958d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 5 May 2026 14:15:07 +0200 Subject: [PATCH 4/5] remove test code --- .../section/section-main-views/section-main-views.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts index 6d69fe8df4d5..6ef94bbdb8b0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts @@ -208,7 +208,7 @@ export class UmbSectionMainViewElement extends UmbLitElement { return html` ${dashboardName} - ${hint /*&& !isActive*/ + ${hint && !isActive ? html` Date: Tue, 5 May 2026 15:45:13 +0200 Subject: [PATCH 5/5] Position badge in section icon slot --- .../section/section-main-views/section-main-views.element.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts index 6ef94bbdb8b0..9dc5b6b7fd5b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts @@ -281,6 +281,11 @@ export class UmbSectionMainViewElement extends UmbLitElement { top: var(--uui-size-2); right: calc(var(--uui-size-2) * -1); } + + div[slot='icon'] umb-badge { + right: -1em; + top: -0.5em; + } `, ]; }