diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts index 4eb142766152..bd3bf92958bd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts @@ -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 extends UmbControllerBase @@ -66,7 +66,7 @@ export class UmbBlockElementManager, @@ -75,11 +75,14 @@ export class UmbBlockElementManager(this, { viewAlias: workspaceViewAlias }); - this.hints.inherit(); - new UmbContentValidationToHintsManager(this, this.structure, this.validation, this.hints, [ - workspaceViewAlias, - ]); + this.view = new UmbViewContext(this, workspaceViewAlias); + new UmbContentValidationToHintsManager( + this, + this.structure, + this.validation, + this.view.hints, + [], + ); // Ugly, but we just inherit these from the workspace context: [NL] this.name = host.name; @@ -264,7 +267,7 @@ export class UmbBlockElementManager; @state() - private _activeTabKey?: string | null | undefined; + private _activeTabKey?: string | null; #blockWorkspace?: typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE; + #blockManager?: UmbBlockElementManager; #tabsStructureHelper = new UmbContentTypeContainerStructureHelper(this); + @state() + private _hintMap: Map = new Map(); + + #tabViewContexts: Array = []; + constructor() { super(); @@ -36,22 +57,23 @@ export class UmbBlockWorkspaceViewEditContentNoRouterElement extends UmbLitEleme 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(); }); } @@ -60,31 +82,118 @@ export class UmbBlockWorkspaceViewEditContentNoRouterElement extends UmbLitEleme 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)); + } } } } - #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); } override render() { @@ -94,23 +203,15 @@ export class UmbBlockWorkspaceViewEditContentNoRouterElement extends UmbLitEleme ${this._tabs.length > 1 || (this._tabs.length === 1 && (this._hasRootGroups || this._hasRootProperties)) ? html` ${(this._hasRootGroups || this._hasRootProperties) && this._tabs.length > 0 - ? html` this.#setTabKey(null)}>` + ? this.#renderTab(null, null, '#general_generic') : nothing} ${repeat( this._tabs, (tab) => tab.name, (tab) => { const tabKey = tab.ownerId ?? tab.ids[0]; - - return html` this.#setTabKey(tabKey)} - >${tab.name}`; + const viewAlias = 'tab/' + encodeFolderName(tab.name || ''); + return this.#renderTab(tabKey, viewAlias, tab.name); }, )} ` @@ -125,6 +226,22 @@ export class UmbBlockWorkspaceViewEditContentNoRouterElement extends UmbLitEleme `; } + #renderTab(tabKey: string | null, viewAlias: string | null, name: string) { + const hint = this._hintMap.get(viewAlias); + const active = this._activeTabKey === tabKey; + return html` this.#setCurrentTabPath(tabKey, viewAlias)} + data-mark="content-tab:${viewAlias ?? 'root'}" + >${hint && !active + ? html`${hint.text}` + : nothing}`; + } + static override styles = [ UmbTextStyles, css` @@ -136,6 +253,9 @@ export class UmbBlockWorkspaceViewEditContentNoRouterElement extends UmbLitEleme padding: calc(var(--uui-size-layout-1)); } + umb-badge { + --uui-badge-inset: 0 0 auto auto; + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit.element.ts index 66d13d6e6b2a..952004cf845b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit.element.ts @@ -1,14 +1,23 @@ import type { UmbBlockWorkspaceElementManagerNames } from '../../block-workspace.context.js'; import { UMB_BLOCK_WORKSPACE_CONTEXT } from '../../block-workspace.context-token.js'; +import type UmbBlockElementManager from '../../block-element-manager.js'; +import type { UmbBlockLayoutBaseModel } from '../../../types.js'; import type { UmbBlockWorkspaceViewEditTabElement } from './block-workspace-view-edit-tab.element.js'; -import { css, html, customElement, state, repeat, property } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, state, repeat, property, nothing } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbContentTypeModel, UmbPropertyTypeContainerMergedModel } from '@umbraco-cms/backoffice/content-type'; import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type'; -import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router'; +import type { + UmbRoute, + UmbRouterSlotChangeEvent, + UmbRouterSlotInitEvent, + PageComponent, +} from '@umbraco-cms/backoffice/router'; import { encodeFolderName } from '@umbraco-cms/backoffice/router'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { ManifestWorkspaceView, UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace'; +import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint'; +import { UmbViewController } from '@umbraco-cms/backoffice/view'; @customElement('umb-block-workspace-view-edit') export class UmbBlockWorkspaceViewEditElement extends UmbLitElement implements UmbWorkspaceViewElement { @@ -21,6 +30,7 @@ export class UmbBlockWorkspaceViewEditElement extends UmbLitElement implements U return; } #managerName?: UmbBlockWorkspaceElementManagerNames; + #blockManager?: UmbBlockElementManager; #blockWorkspace?: typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE; #tabsStructureHelper = new UmbContentTypeContainerStructureHelper(this); @@ -42,6 +52,11 @@ export class UmbBlockWorkspaceViewEditElement extends UmbLitElement implements U @state() private _activePath = ''; + @state() + private _hintMap: Map = new Map(); + + #tabViewContexts: Array = []; + constructor() { super(); @@ -74,6 +89,7 @@ export class UmbBlockWorkspaceViewEditElement extends UmbLitElement implements U async #setStructureManager() { if (!this.#blockWorkspace || !this.#managerName) return; const blockManager = this.#blockWorkspace[this.#managerName]; + this.#blockManager = blockManager; this.#tabsStructureHelper.setStructureManager(blockManager.structure); this.observe( @@ -101,39 +117,41 @@ export class UmbBlockWorkspaceViewEditElement extends UmbLitElement implements U if (!this._tabs || !this.#blockWorkspace) return; const routes: UmbRoute[] = []; + if (this._hasRootGroups || this._hasRootProperties) { + routes.push({ + path: 'root', + component: () => import('./block-workspace-view-edit-tab.element.js'), + setup: (component) => { + (component as UmbBlockWorkspaceViewEditTabElement).managerName = this.#managerName; + (component as UmbBlockWorkspaceViewEditTabElement).containerId = null; + }, + }); + } + if (this._tabs.length > 0) { this._tabs?.forEach((tab) => { const tabName = tab.name ?? ''; + const path = `tab/${encodeFolderName(tabName)}`; routes.push({ - path: `tab/${encodeFolderName(tabName)}`, + path, component: () => import('./block-workspace-view-edit-tab.element.js'), setup: (component) => { (component as UmbBlockWorkspaceViewEditTabElement).managerName = this.#managerName; (component as UmbBlockWorkspaceViewEditTabElement).containerId = tab.ids[0]; + this.#provideViewContext(path, component); }, }); + this.#createViewContext(path, tabName); }); } - if (this._hasRootGroups || this._hasRootProperties) { + if (routes.length !== 0) { routes.push({ + ...routes[0], + unique: 'emptyPathFor_' + routes[0].path, path: '', - component: () => import('./block-workspace-view-edit-tab.element.js'), - setup: (component) => { - (component as UmbBlockWorkspaceViewEditTabElement).managerName = this.#managerName; - (component as UmbBlockWorkspaceViewEditTabElement).containerId = null; - }, }); - } - if (routes.length !== 0) { - if (!this._hasRootGroups) { - routes.push({ - path: '', - pathMatch: 'full', - redirectTo: routes[0]?.path, - }); - } routes.push({ path: `**`, component: async () => (await import('@umbraco-cms/backoffice/router')).UmbRouteNotFoundElement, @@ -143,6 +161,64 @@ export class UmbBlockWorkspaceViewEditElement extends UmbLitElement implements U this._routes = routes; } + #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, so in this case we do accept everything that is not in a tab: [NL] + view.hints.setPathFilter((paths) => { + const firstPath = paths[0]; + // Root-property validation hints can have an empty path (no container), so treat them as root/non-tab hints. + 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, + ); + } + } + + #currentProvidedView?: UmbViewController; + + #provideViewContext(viewAlias: string | null, component: PageComponent) { + 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(component as any); + } + override render() { if (!this._routes || !this._tabs) return; return html` @@ -151,28 +227,18 @@ export class UmbBlockWorkspaceViewEditElement extends UmbLitElement implements U (this._tabs.length > 1 || (this._tabs.length === 1 && (this._hasRootGroups || this._hasRootProperties))) ? html` ${(this._hasRootGroups || this._hasRootProperties) && this._tabs.length > 0 - ? html` - - ` - : ''} + ? this.#renderTab(null, '#general_generic') + : nothing} ${repeat( this._tabs, (tab) => tab.name, - (tab) => { - const path = this._routerPath + '/tab/' + encodeFolderName(tab.name || ''); - return html` - ${this.localize.string(tab.name)} - `; + (tab, index) => { + const path = 'tab/' + encodeFolderName(tab.name || ''); + return this.#renderTab(path, tab.name, index); }, )} ` - : ''} + : nothing} ${hint && !active + ? html`${hint.text}` + : nothing}`; + } + static override readonly styles = [ UmbTextStyles, css` @@ -195,6 +287,9 @@ export class UmbBlockWorkspaceViewEditElement extends UmbLitElement implements U height: 100%; --uui-tab-background: var(--uui-color-surface); } + umb-badge { + --uui-badge-inset: 0 0 auto auto; + } `, ]; }