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 867811498139..6f5b7775c2b5 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 @@ -41,10 +41,10 @@ export class ExampleHintWorkspaceView extends UmbElementMixin(LitElement) { throw new Error('Could not find the workspace'); } - if (workspace.hints.has('exampleHintFromToggleAction')) { - workspace.hints.removeOne('exampleHintFromToggleAction'); + if (workspace.view.hints.has('exampleHintFromToggleAction')) { + workspace.view.hints.removeOne('exampleHintFromToggleAction'); } else { - workspace.hints.addOne({ + workspace.view.hints.addOne({ unique: 'exampleHintFromToggleAction', path: ['Umb.WorkspaceView.Document.Edit'], text: 'Hi', diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context-base.ts index 4661ae1b41f1..9f72381411b3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context-base.ts @@ -7,6 +7,7 @@ import { UmbRequestReloadChildrenOfEntityEvent, UmbRequestReloadStructureForEntityEvent, } from '@umbraco-cms/backoffice/entity-action'; +import { UmbViewContext } from '@umbraco-cms/backoffice/view'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import type { Observable } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; @@ -52,6 +53,8 @@ export abstract class UmbContentTypeWorkspaceContextBase< public readonly structure: UmbContentTypeStructureManager; + public readonly view = new UmbViewContext(this, null); + constructor(host: UmbControllerHost, args: UmbContentTypeWorkspaceContextArgs) { super(host, args); @@ -70,7 +73,9 @@ export abstract class UmbContentTypeWorkspaceContextBase< this.collection = this.structure.ownerContentTypeObservablePart((data) => data?.collection); // Keep current data in sync with the owner content type - This is used for the discard changes feature - this.observe(this.structure.ownerContentType, (data) => this._data.setCurrent(data)); + this.observe(this.structure.ownerContentType, (data) => this._data.setCurrent(data), null); + this.observe(this.name, (name) => this.view.setBrowserTitle(name), null); + // TODO: sometimes the browserTitle for a parent view is set later than the child is updating. We need to fix this as well enable a parent browser title to be updating on the go. [NL] } /** diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context.interface.ts index dc529f79808a..c45e72c363cc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context.interface.ts @@ -1,13 +1,13 @@ import type { UmbContentTypeCompositionModel, UmbContentTypeModel, UmbContentTypeSortModel } from '../types.js'; import type { UmbContentTypeStructureManager } from '../structure/index.js'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; -import type { UmbSubmittableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; +import type { UmbNamableWorkspaceContext, UmbSubmittableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; export interface UmbContentTypeWorkspaceContext - extends UmbSubmittableWorkspaceContext { + extends UmbSubmittableWorkspaceContext, + UmbNamableWorkspaceContext { readonly IS_CONTENT_TYPE_WORKSPACE_CONTEXT: true; - readonly name: Observable; readonly alias: Observable; readonly description: Observable; readonly icon: Observable; @@ -32,7 +32,4 @@ export interface UmbContentTypeWorkspaceContext, @@ -141,8 +145,8 @@ export abstract class UmbContentDetailWorkspaceContextBase< readonly collection: UmbContentCollectionManager; - /* Hints */ - readonly hints = new UmbHintContext(this); + /* View */ + readonly view = new UmbViewContext(this, null); /* Variant Options */ // TODO: Optimize this so it uses either a App Language Context? [NL] @@ -221,7 +225,7 @@ export abstract class UmbContentDetailWorkspaceContextBase< this, this.structure, this.validationContext, - this.hints, + this.view.hints, ); this.variantOptions = mergeObservables( @@ -334,6 +338,17 @@ export abstract class UmbContentDetailWorkspaceContextBase< null, ); + this.observe( + observeMultiple([this.splitView.activeVariantByIndex(0), this.variants]), + ([activeVariant, variants]) => { + const variantName = variants.find( + (v) => v.culture === activeVariant?.culture && v.segment === activeVariant?.segment, + )?.name; + this.view.setBrowserTitle(variantName); + }, + null, + ); + this.observe( this.varies, (varies) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor.element.ts index 45c8d3b817da..5b531ae552ea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor.element.ts @@ -1,22 +1,28 @@ import type { UmbContentWorkspaceViewEditTabElement } from './content-editor-tab.element.js'; import { css, html, customElement, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { encodeFolderName } from '@umbraco-cms/backoffice/router'; +import { + UmbContentTypeContainerStructureHelper, + UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT, +} from '@umbraco-cms/backoffice/content-type'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UMB_VIEW_CONTEXT, UmbViewContext } from '@umbraco-cms/backoffice/view'; +import type { + PageComponent, + UmbRoute, + UmbRouterSlotChangeEvent, + UmbRouterSlotInitEvent, +} from '@umbraco-cms/backoffice/router'; import type { UmbContentTypeModel, UmbContentTypeStructureManager, UmbPropertyTypeContainerMergedModel, } from '@umbraco-cms/backoffice/content-type'; -import { - UmbContentTypeContainerStructureHelper, - UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT, -} from '@umbraco-cms/backoffice/content-type'; -import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router'; -import { encodeFolderName } from '@umbraco-cms/backoffice/router'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace'; + import './content-editor-tab.element.js'; -import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint'; -import { UMB_VIEW_CONTEXT, UmbViewContext } from '@umbraco-cms/backoffice/view'; @customElement('umb-content-workspace-view-edit') export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements UmbWorkspaceViewElement { @@ -43,7 +49,7 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements private _activePath = ''; @state() - private _hintMap: Map = new Map(); + private _hintMap: Map = new Map(); #tabViewContexts: Array = []; @@ -104,9 +110,10 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements component: () => import('./content-editor-tab.element.js'), setup: (component) => { (component as UmbContentWorkspaceViewEditTabElement).containerId = null; + this.#provideViewContext(null, component); }, }); - this.#createViewContext('root'); + this.#createViewContext(null, '#general_generic'); } if (this._tabs.length > 0) { @@ -118,9 +125,10 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements component: () => import('./content-editor-tab.element.js'), setup: (component) => { (component as UmbContentWorkspaceViewEditTabElement).containerId = tab.ownerId ?? tab.ids[0]; + this.#provideViewContext(path, component); }, }); - this.#createViewContext(path); + this.#createViewContext(path, tabName); }); } @@ -140,11 +148,17 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements this._routes = routes; } - #createViewContext(viewAlias: string) { + #createViewContext(viewAlias: string | null, tabName: string) { if (!this.#tabViewContexts.find((context) => context.viewAlias === viewAlias)) { const view = new UmbViewContext(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) => paths[0].includes('tab/') === false); + } + + view.setBrowserTitle(tabName); view.inheritFrom(this.#viewContext); this.observe( @@ -162,13 +176,28 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements } } + #currentProvidedView?: UmbViewContext; + + #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; + view.provideAt(component as any); + } + override render() { if (!this._routes || !this._tabs) return; return html` ${this._routerPath && (this._tabs.length > 1 || (this._tabs.length === 1 && this._hasRootGroups)) ? html` - ${this._hasRootGroups && this._tabs.length > 0 ? this.#renderTab('root', '#general_generic') : nothing} + ${this._hasRootGroups && this._tabs.length > 0 ? this.#renderTab(null, '#general_generic') : nothing} ${repeat( this._tabs, (tab) => tab.name, @@ -194,17 +223,18 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements `; } - #renderTab(path: string, name: string, index = 0) { + #renderTab(path: string | null, name: string, index = 0) { const hint = this._hintMap.get(path); - const fullPath = this._routerPath + '/' + path; + const fullPath = this._routerPath + '/' + (path ? path : 'root'); const active = fullPath === this._activePath || - (!this._hasRootGroups && index === 0 && this._routerPath + '/' === this._activePath); + (!this._hasRootGroups && index === 0 && this._routerPath + '/' === this._activePath) || + (this._hasRootGroups && index === 0 && path === null && this._routerPath + '/' === this._activePath); return html`${hint && !active ? html`${hint.text} { - viewAlias?: string; + viewAlias?: string | null; scaffold?: Partial; } @@ -16,10 +16,15 @@ export class UmbHintController< IncomingHintType extends UmbIncomingHintBase = UmbPartialSome, > extends UmbControllerBase { // - #viewAlias?: string; - getViewAlias(): string | undefined { + #viewAlias: string | null; + getViewAlias(): string | null { return this.#viewAlias; } + #pathFilter?: (path: Array) => boolean; + setPathFilter(filter: (path: Array) => boolean) { + this.#pathFilter = filter; + } + #scaffold = new UmbObjectState>({}); readonly scaffold = this.#scaffold.asObservable(); #inUnprovidingState?: boolean; @@ -43,7 +48,7 @@ export class UmbHintController< constructor(host: UmbControllerHost, args?: UmbHintControllerArgs) { super(host); - this.#viewAlias = args?.viewAlias; + this.#viewAlias = args?.viewAlias ?? null; if (args?.scaffold) { this.#scaffold.setValue(args?.scaffold); } @@ -82,7 +87,7 @@ export class UmbHintController< return this.#hints.asObservablePart(fn); } - descendingHints(viewAlias?: string): Observable | undefined> { + descendingHints(viewAlias?: string | null): Observable | undefined> { if (viewAlias) { return this.#hints.asObservablePart((hints) => { return hints.filter((hint) => hint.path[0] === viewAlias); @@ -92,7 +97,22 @@ export class UmbHintController< } } + /** + * @internal + * @param {(path: Array) => boolean} filter - A filter function to filter the hints by their path. + * @returns {Observable | undefined>} An observable of an array of hints that match the filter. + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + _internal_descendingHintsByFilter(filter: (path: Array) => boolean): Observable | undefined> { + return this.#hints.asObservablePart((hints) => { + return hints.filter((hint) => filter(hint.path)); + }); + } + inherit(): void { + if (this.#viewAlias === null && this.#pathFilter === undefined) { + throw new Error('A Hint Controller needs a view alias or pathFilter to be able to inherit from a parent.'); + } this.consumeContext(UMB_HINT_CONTEXT, (parent) => { this.inheritFrom(parent); }).skipHost(); @@ -101,13 +121,24 @@ export class UmbHintController< inheritFrom(parent: UmbHintController | undefined): void { if (this.#parent === parent) return; + if (this.#viewAlias === null && this.#pathFilter === undefined) { + throw new Error('A Hint Controller needs a view alias or pathFilter to be able to inherit from a parent.'); + } this.#parent = parent; this.observe(this.#parent?.scaffold, (scaffold) => { if (scaffold) { this.#scaffold.update(scaffold as any); } }); - this.observe(parent?.descendingHints(this.#viewAlias), this.#receiveHints, 'observeParentHints'); + if (this.#viewAlias) { + this.observe(parent?.descendingHints(this.#viewAlias), this.#receiveHints, 'observeParentHints'); + } else if (this.#pathFilter) { + this.observe( + parent?._internal_descendingHintsByFilter(this.#pathFilter), + this.#receiveHints, + 'observeParentHints', + ); + } this.observe(this.hints, this.#propagateHints, 'observeLocalMessages'); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/section.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section.context.ts index 510697e14607..85fc30489d53 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/section.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section.context.ts @@ -1,10 +1,11 @@ import type { ManifestSection } from './extensions/section.extension.js'; import { UMB_SECTION_CONTEXT } from './section.context.token.js'; -import { UmbStringState } from '@umbraco-cms/backoffice/observable-api'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbStringState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbViewContext } from '@umbraco-cms/backoffice/view'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbSectionContext extends UmbContextBase { #manifestAlias = new UmbStringState(undefined); @@ -14,17 +15,21 @@ export class UmbSectionContext extends UmbContextBase { public readonly pathname = this.#manifestPathname.asObservable(); public readonly label = this.#manifestLabel.asObservable(); + #viewContext = new UmbViewContext(this, null); #sectionContextExtensionController?: UmbExtensionsApiInitializer; constructor(host: UmbControllerHost) { super(host, UMB_SECTION_CONTEXT); + this.#createSectionContextExtensions(); } public setManifest(manifest?: ManifestSection) { this.#manifestAlias.setValue(manifest?.alias); this.#manifestPathname.setValue(manifest?.meta?.pathname); - this.#manifestLabel.setValue(manifest ? manifest.meta?.label || manifest.name : undefined); + const sectionLabel = manifest ? manifest.meta?.label || manifest.name : undefined; + this.#manifestLabel.setValue(sectionLabel); + this.#viewContext.setBrowserTitle(sectionLabel); } getPathname() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/view/context/view.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/view/context/view.context.ts index 6930c4fa945f..d2833afe1211 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/view/context/view.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/view/context/view.context.ts @@ -1,80 +1,9 @@ -import { UMB_VIEW_CONTEXT } from './view.context-token.js'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbControllerBase, type UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; -import { UmbClassState, mergeObservables } from '@umbraco-cms/backoffice/observable-api'; -import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; -import { UmbHintController, type UmbVariantHint } from '@umbraco-cms/backoffice/hint'; +import { UmbViewController } from './view.controller.js'; +import type { UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; -/** - * - * TODO: - * Include Shortcuts - * - * Browser Title? - * - */ -export class UmbViewContext extends UmbControllerBase { - // - #providerCtrl: any; - #currentProvideHost?: UmbClassInterface; - - public readonly viewAlias: string; - #variantId = new UmbClassState(undefined); - protected readonly variantId = this.#variantId.asObservable(); - - public hints; - - readonly firstHintOfVariant; - - constructor(host: UmbControllerHost, viewAlias: string) { - super(host); - this.viewAlias = viewAlias; - this.hints = new UmbHintController(this, { - viewAlias: viewAlias, - }); - this.firstHintOfVariant = mergeObservables([this.variantId, this.hints.hints], ([variantId, hints]) => { - // Notice, because we in UI have invariant fields on Variants, then we will accept invariant hints on variants. - if (variantId) { - return hints.find((hint) => - hint.variantId ? hint.variantId.equal(variantId!) || hint.variantId.isInvariant() : true, - ); - } else { - return hints[0]; - } - }); - } - - setVariantId(variantId: UmbVariantId | undefined): void { - this.#variantId.setValue(variantId); - this.hints.updateScaffold({ variantId: variantId }); - } - - provideAt(controllerHost: UmbClassInterface): void { - if (this.#currentProvideHost === controllerHost) return; - - this.unprovide(); - - this.#currentProvideHost = controllerHost; - this.#providerCtrl = controllerHost.provideContext(UMB_VIEW_CONTEXT, this); - this.hints.provideAt(controllerHost); - } - - unprovide(): void { - if (this.#providerCtrl) { - this.#providerCtrl.destroy(); - this.#providerCtrl = undefined; - } - this.hints.unprovide(); - } - - inheritFrom(context?: UmbViewContext): void { - this.observe( - context?.variantId, - (variantId) => { - this.setVariantId(variantId); - }, - 'observeParentVariantId', - ); - this.hints.inheritFrom(context?.hints); +export class UmbViewContext extends UmbViewController { + constructor(host: UmbClassInterface, viewAlias: string | null) { + super(host, viewAlias); + this.provideAt(host); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/view/context/view.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/view/context/view.controller.ts new file mode 100644 index 000000000000..af2328240bb0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/view/context/view.controller.ts @@ -0,0 +1,309 @@ +import { UMB_VIEW_CONTEXT } from './view.context-token.js'; +import { UmbClassState, UmbStringState, mergeObservables } from '@umbraco-cms/backoffice/observable-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbHintController } from '@umbraco-cms/backoffice/hint'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; +import type { UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; +import type { UmbContextConsumerController, UmbContextProviderController } from '@umbraco-cms/backoffice/context-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint'; +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; + +/** + * + * TODO: + * Include Shortcuts + * + * The View Context handles the aspects of three Features: + * Browser Titles — Provide a title for this view and it will be set or joint with parent views depending on the inheritance setting. + * Hints — Holds Hints for this view, depending on the inheritance setting it will propagate the hints to be displayed at parent views. + * Shortcuts — Not implemented yet. + * + */ +export class UmbViewController extends UmbControllerBase { + // + #attached = false; + #providerCtrl?: UmbContextProviderController; + #consumeParentCtrl?: UmbContextConsumerController; + #currentProvideHost?: UmbClassInterface; + #localize = new UmbLocalizationController(this); + + // State used to know if the context can be auto activated when attached. + #autoActivate = true; + #active = false; + #hasActiveChild = false; + #inherit?: boolean; + #explicitInheritance?: boolean; + #parentView?: UmbViewController; + #title?: string; + #computedTitle = new UmbStringState(undefined); + readonly computedTitle = this.#computedTitle.asObservable(); + + public readonly viewAlias: string | null; + + #variantId = new UmbClassState(undefined); + protected readonly variantId = this.#variantId.asObservable(); + + public hints; + + readonly firstHintOfVariant; + + constructor(host: UmbControllerHost, viewAlias: string | null) { + super(host); + this.viewAlias = viewAlias; + this.hints = new UmbHintController(this, { + viewAlias: viewAlias, + }); + this.firstHintOfVariant = mergeObservables([this.variantId, this.hints.hints], ([variantId, hints]) => { + // Notice, because we in UI have invariant fields on Variants, then we will accept invariant hints on variants. + if (variantId) { + return hints.find((hint) => + hint.variantId ? hint.variantId.equal(variantId!) || hint.variantId.isInvariant() : true, + ); + } else { + return hints[0]; + } + }); + + this.#consumeParentCtrl = this.consumeContext(UMB_VIEW_CONTEXT, (parentView) => { + // In case of explicit inheritance we do not want to overview the parent view. + if (this.#explicitInheritance) return; + if (this.#active && !this.#hasActiveChild) { + // If we were active we will react as if we got deactivated and then activated again below if state allows. [NL] + this.#propagateActivation(); + } + this.#active = false; + if (parentView) { + this.#parentView = parentView; + } + if (this.#inherit) { + this.#inheritFromParent(); + } + // only activate if we had an incoming parentView, cause if not we are in a disassembling state. [NL] + if (parentView && this.#attached && this.#autoActivate) { + this._internal_activate(); + } + }).skipHost(); + } + + public setVariantId(variantId: UmbVariantId | undefined): void { + this.#variantId.setValue(variantId); + this.hints.updateScaffold({ variantId: variantId }); + } + + public setBrowserTitle(title: string | undefined): void { + if (this.#title === title) return; + this.#title = title; + // TODO: This check should be if its the most child being active, but again think about how the parents in the active chain should work. + this.#computeTitle(); + this.#updateTitle(); + } + + public provideAt(controllerHost: UmbClassInterface): void { + if (this.#currentProvideHost === controllerHost) return; + + this.unprovide(); + + this.#autoActivate = true; + this.#currentProvideHost = controllerHost; + this.#providerCtrl = controllerHost.provideContext(UMB_VIEW_CONTEXT, this); + this.hints.provideAt(controllerHost); + + if (this.#attached && this.#autoActivate) { + this._internal_activate(); + } + } + + public unprovide(): void { + if (this.#providerCtrl) { + this.#providerCtrl.destroy(); + this.#providerCtrl = undefined; + } + this.hints.unprovide(); + + this._internal_deactivate(); + } + + override hostConnected(): void { + this.#attached = true; + super.hostConnected(); + // CHeck that we have a providerController, otherwise this is not provided. [NL] + if (this.#autoActivate) { + this._internal_activate(); + } + } + + override hostDisconnected(): void { + const wasAttached = this.#attached; + const wasActive = this.#active; + this.#attached = false; + this.#active = false; + super.hostDisconnected(); + if (wasAttached === true && wasActive) { + // CHeck that we have a providerController, otherwise this is not provided. [NL] + this.#propagateActivation(); + } + } + + public inherit() { + this.#inherit = true; + } + + public inheritFrom(context?: UmbViewController): void { + this.#inherit = true; + this.#explicitInheritance = true; + this.#consumeParentCtrl?.destroy(); + this.#consumeParentCtrl = undefined; + this.#parentView = context; + this.#inheritFromParent(); + this.#propagateActivation(); + } + + #inheritFromParent(): void { + this.observe( + this.#parentView?.variantId, + (variantId) => { + this.setVariantId(variantId); + }, + 'observeParentVariantId', + ); + this.observe( + this.#parentView?.computedTitle, + () => { + this.#computeTitle(); + // Check for parent view as it is undefined in a disassembling state and we do not want to update the title in that situation. [NL] + if (this.#providerCtrl && this.#parentView && this.#active) { + console.log('ttt', this.viewAlias, this); + this.#updateTitle(); + } + }, + 'observeParentTitle', + ); + this.hints.inheritFrom(this.#parentView?.hints); + } + + #propagateActivation() { + if (!this.#parentView) return; + if (this.#inherit) { + if (this.#active) { + this.#parentView._internal_childActivated(); + } else { + this.#parentView._internal_childDeactivated(); + } + } else { + if (this.#active) { + this.#parentView._internal_deactivate(); + } else { + this.#parentView._internal_activate(); + } + } + } + + /** + * @internal + * Notify that a view context has been activated. + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + public _internal_activate() { + if (!this.#providerCtrl) { + // If we are not provided we should not be activated. [NL] + return; + } + this.#autoActivate = true; + if (this.#active === true) { + return; + } + // If not attached then propagate the activation to the parent. [NL] + if (this.#attached === false) { + if (!this.#parentView) { + throw new Error('Cannot activate a view that is not attached to the DOM.'); + } + this.#propagateActivation(); + } else { + this.#active = true; + this.#propagateActivation(); + this.#updateTitle(); + // TODO: Start shortcuts. [NL] + } + } + + /** + * @internal + * Notify that a child has been activated. + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + public _internal_childActivated() { + if (this.#hasActiveChild) return; + this.#hasActiveChild = true; + this._internal_activate(); + } + + /** + * @internal + * Notify that a child is no longer activated. + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + public _internal_childDeactivated() { + this.#hasActiveChild = false; + if (this.#attached === false) { + if (this.#parentView) { + return; + } else { + throw new Error('Cannot re-activate(_childDeactivated) a view that is not attached to the DOM.'); + } + } + if (this.#autoActivate) { + this._internal_activate(); + } else { + this.#propagateActivation(); + } + } + + /** + * @internal + * Deactivate the view context. + * We cannot conclude that this means the parent should be activated, it can be because of a child being activated. + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + public _internal_deactivate() { + this.#autoActivate = false; + if (!this.#active) return; + this.#active = false; + // TODO: Stop shortcuts. [NL] + // Deactivate parents: + this.#propagateActivation(); + } + + #updateTitle() { + if (!this.#active || this.#hasActiveChild) { + return; + } + const localTitle = this.getComputedTitle(); + document.title = (localTitle ? localTitle + ' | ' : '') + 'Umbraco'; + } + + #computeTitle() { + const titles = []; + if (this.#inherit && this.#parentView) { + titles.push(this.#parentView.getComputedTitle()); + } + if (this.#title) { + titles.push(this.#localize.string(this.#title)); + } + this.#computedTitle.setValue(titles.length > 0 ? titles.join(' | ') : undefined); + } + + public getComputedTitle(): string | undefined { + return this.#computedTitle.getValue(); + } + + override destroy(): void { + this.#inherit = false; + this.#active = false; + this.#autoActivate = false; + (this as any).provideAt = undefined; + this.unprovide(); + super.destroy(); + this.#consumeParentCtrl = undefined; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.context.ts index 73ac95d56386..7efdb5968823 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.context.ts @@ -1,14 +1,12 @@ import type { ManifestWorkspaceView } from '../../types.js'; import { UmbWorkspaceViewContext } from './workspace-view.context.js'; import { UMB_WORKSPACE_EDITOR_CONTEXT } from './workspace-editor.context-token.js'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbBasicState, mergeObservables } from '@umbraco-cms/backoffice/observable-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import { UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api'; -import { UmbHintController } from '@umbraco-cms/backoffice/hint'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbDeepPartialObject } from '@umbraco-cms/backoffice/utils'; -import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint'; import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; export class UmbWorkspaceEditorContext extends UmbContextBase { @@ -26,9 +24,13 @@ export class UmbWorkspaceEditorContext extends UmbContextBase { let contexts = this.#contexts; // remove ones that are no longer contained in the workspaceViews (And thereby make the new array): - const contextsToKeep = contexts.filter( - (view) => !manifests.some((manifest) => manifest.alias === view.manifest.alias), - ); + const contextsToKeep = contexts.filter((view) => { + const keep = manifests.some((manifest) => manifest.alias === view.manifest.alias); + if (!keep) { + view.destroy(); + } + return keep; + }); const hasDiff = contextsToKeep.length !== manifests.length; if (hasDiff) { @@ -40,7 +42,8 @@ export class UmbWorkspaceEditorContext extends UmbContextBase { .forEach((manifest) => { const context = new UmbWorkspaceViewContext(this, manifest); context.setVariantId(this.#variantId); - context.hints.inheritFrom(this.#hints); + context.setBrowserTitle(manifest.meta.label); + context.inherit(); contexts.push(context); }); } @@ -82,13 +85,10 @@ export class UmbWorkspaceEditorContext extends UmbContextBase { #contexts = new Array(); #variantId?: UmbVariantId; - #hints = new UmbHintController(this, {}); constructor(host: UmbControllerHost) { super(host, UMB_WORKSPACE_EDITOR_CONTEXT); - this.#hints.inherit(); - this.#init = new UmbExtensionsManifestInitializer( this, umbExtensionsRegistry, @@ -102,7 +102,6 @@ export class UmbWorkspaceEditorContext extends UmbContextBase { setVariantId(variantId: UmbVariantId | undefined): void { this.#variantId = variantId; - this.#hints.updateScaffold({ variantId }); this.#contexts.forEach((view) => { view.hints.updateScaffold({ variantId }); }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.element.ts index 300a8fdc15b0..46f7f14c468f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.element.ts @@ -110,6 +110,7 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { ); } + #currentProvidedView?: UmbWorkspaceViewContext; #createRoutes() { let newRoutes: UmbRoute[] = []; @@ -120,7 +121,11 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { path: UMB_WORKSPACE_VIEW_PATH_PATTERN.generateLocal({ viewPathname: manifest.meta.pathname }), component: () => createExtensionElement(manifest), setup: (component?: any) => { + if (this.#currentProvidedView !== context) { + this.#currentProvidedView?.unprovide(); + } if (component) { + this.#currentProvidedView = context; context.provideAt(component); component.manifest = manifest; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context.ts index eafe506578e6..bf84734cd84f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context.ts @@ -1,13 +1,14 @@ import type { ManifestWorkspaceView } from '../../types.js'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbViewContext } from '@umbraco-cms/backoffice/view'; +import type { UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; + export class UmbWorkspaceViewContext extends UmbViewContext { public readonly IS_WORKSPACE_VIEW_CONTEXT = true as const; // Note: manifest can change later, but because we currently only use the alias from it, it's not something we need to handle. [NL] public manifest: ManifestWorkspaceView; - constructor(host: UmbControllerHost, manifest: ManifestWorkspaceView) { + constructor(host: UmbClassInterface, manifest: ManifestWorkspaceView) { super(host, manifest.alias); this.manifest = manifest; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-named-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-named-detail-workspace-base.ts index 0cf1573409c9..4d1451005690 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-named-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-named-detail-workspace-base.ts @@ -2,9 +2,10 @@ import type { UmbNamableWorkspaceContext } from '../types.js'; import { UmbNameWriteGuardManager } from '../namable/index.js'; import { UmbEntityDetailWorkspaceContextBase } from './entity-detail-workspace-base.js'; import type { UmbEntityDetailWorkspaceContextArgs, UmbEntityDetailWorkspaceContextCreateArgs } from './types.js'; -import type { UmbNamedEntityModel } from '@umbraco-cms/backoffice/entity'; -import type { UmbDetailRepository } from '@umbraco-cms/backoffice/repository'; +import { UmbViewContext } from '@umbraco-cms/backoffice/view'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbDetailRepository } from '@umbraco-cms/backoffice/repository'; +import type { UmbNamedEntityModel } from '@umbraco-cms/backoffice/entity'; export abstract class UmbEntityNamedDetailWorkspaceContextBase< NamedDetailModelType extends UmbNamedEntityModel = UmbNamedEntityModel, @@ -23,9 +24,18 @@ export abstract class UmbEntityNamedDetailWorkspaceContextBase< public readonly nameWriteGuard = new UmbNameWriteGuardManager(this); + public readonly view = new UmbViewContext(this, null); + constructor(host: UmbControllerHost, args: UmbEntityDetailWorkspaceContextArgs) { super(host, args); this.nameWriteGuard.fallbackToPermitted(); + this.observe( + this.name, + (name) => { + this.view.setBrowserTitle(name); + }, + null, + ); } getName() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts index 52406deee802..e4def2c68eac 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts @@ -220,7 +220,6 @@ export class UmbInputDocumentElement extends UmbFormControlMixin { if (this.#hintedMsgs.has(message.key)) return; - this.hints.addOne({ + this.view.hints.addOne({ unique: message.key, path: [UMB_MEMBER_WORKSPACE_VIEW_MEMBER_ALIAS], text: '!', @@ -158,7 +158,7 @@ export class UmbMemberWorkspaceContext this.#hintedMsgs.forEach((key) => { if (!messages.some((msg) => msg.key === key)) { this.#hintedMsgs.delete(key); - this.hints.removeOne(key); + this.view.hints.removeOne(key); } }); }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace.context.ts index 0e2d78241b1e..1923edb9d01d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace.context.ts @@ -3,19 +3,18 @@ import { UMB_USER_GROUP_DETAIL_REPOSITORY_ALIAS, type UmbUserGroupDetailReposito import { UMB_USER_GROUP_ENTITY_TYPE, UMB_USER_GROUP_ROOT_ENTITY_TYPE } from '../../entity.js'; import { UmbUserGroupWorkspaceEditorElement } from './user-group-workspace-editor.element.js'; import { UMB_USER_GROUP_WORKSPACE_ALIAS } from './constants.js'; -import type { UmbUserPermissionModel } from '@umbraco-cms/backoffice/user-permission'; -import type { UmbRoutableWorkspaceContext, UmbSubmittableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; import { - UmbEntityDetailWorkspaceContextBase, + UmbEntityNamedDetailWorkspaceContextBase, UmbWorkspaceIsNewRedirectController, } from '@umbraco-cms/backoffice/workspace'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbRoutableWorkspaceContext, UmbSubmittableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; +import type { UmbUserPermissionModel } from '@umbraco-cms/backoffice/user-permission'; export class UmbUserGroupWorkspaceContext - extends UmbEntityDetailWorkspaceContextBase + extends UmbEntityNamedDetailWorkspaceContextBase implements UmbSubmittableWorkspaceContext, UmbRoutableWorkspaceContext { - readonly name = this._data.createObservablePartOfCurrent((data) => data?.name || ''); readonly alias = this._data.createObservablePartOfCurrent((data) => data?.alias || ''); readonly aliasCanBeChanged = this._data.createObservablePartOfCurrent((data) => data?.aliasCanBeChanged); readonly icon = this._data.createObservablePartOfCurrent((data) => data?.icon || null); diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/user-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/user-workspace.context.ts index cc7f872c1dc0..8541cf765778 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/user-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/user-workspace.context.ts @@ -1,27 +1,26 @@ import type { UmbUserDetailModel, UmbUserStartNodesModel, UmbUserStateEnum } from '../../types.js'; -import { UMB_USER_ENTITY_TYPE } from '../../entity.js'; import type { UmbUserDetailRepository } from '../../repository/index.js'; import { UMB_USER_DETAIL_REPOSITORY_ALIAS } from '../../repository/index.js'; +import { UMB_USER_ENTITY_TYPE } from '../../entity.js'; import { UmbUserAvatarRepository } from '../../repository/avatar/index.js'; import { UmbUserConfigRepository } from '../../repository/config/index.js'; -import { UMB_USER_WORKSPACE_ALIAS } from './constants.js'; import { UmbUserWorkspaceEditorElement } from './user-workspace-editor.element.js'; -import type { UmbSubmittableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; -import { UmbEntityDetailWorkspaceContextBase } from '@umbraco-cms/backoffice/workspace'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UMB_USER_WORKSPACE_ALIAS } from './constants.js'; +import { UmbEntityNamedDetailWorkspaceContextBase } from '@umbraco-cms/backoffice/workspace'; import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbRepositoryResponseWithAsObservable } from '@umbraco-cms/backoffice/repository'; +import type { UmbSubmittableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; type EntityType = UmbUserDetailModel; export class UmbUserWorkspaceContext - extends UmbEntityDetailWorkspaceContextBase + extends UmbEntityNamedDetailWorkspaceContextBase implements UmbSubmittableWorkspaceContext { public readonly avatarRepository: UmbUserAvatarRepository = new UmbUserAvatarRepository(this); public readonly configRepository = new UmbUserConfigRepository(this); - readonly name = this._data.createObservablePartOfCurrent((x) => x?.name); readonly state = this._data.createObservablePartOfCurrent((x) => x?.state); readonly kind = this._data.createObservablePartOfCurrent((x) => x?.kind); readonly userGroupUniques = this._data.createObservablePartOfCurrent((x) => x?.userGroupUniques || []); @@ -116,14 +115,6 @@ export class UmbUserWorkspaceContext return this.avatarRepository.deleteAvatar(unique); } - getName(): string { - return this._data.getCurrent()?.name || ''; - } - - setName(name: string) { - this._data.updateCurrent({ name }); - } - override destroy(): void { this.avatarRepository.destroy(); super.destroy(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/collection/repository/webhook-collection.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/collection/repository/webhook-collection.server.data-source.ts index f3aecf9d4000..1ce283b8643e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/collection/repository/webhook-collection.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/collection/repository/webhook-collection.server.data-source.ts @@ -41,7 +41,7 @@ export class UmbWebhookCollectionServerDataSource implements UmbWebhookCollectio entityType: UMB_WEBHOOK_ENTITY_TYPE, unique: item.id, url: item.url, - name: item.name, + name: item.name ?? '', description: item.description, enabled: item.enabled, headers: item.headers, diff --git a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/detail/webhook-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/detail/webhook-detail.server.data-source.ts index fedd488ff839..34d72ec12eae 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/detail/webhook-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/detail/webhook-detail.server.data-source.ts @@ -73,7 +73,7 @@ export class UmbWebhookDetailServerDataSource implements UmbDetailDataSource; contentTypes: Array; diff --git a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/workspace/webhook-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/workspace/webhook-workspace.context.ts index d2d2d4623125..8878f584be68 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/workspace/webhook-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/workspace/webhook-workspace.context.ts @@ -5,23 +5,21 @@ import type { UmbWebhookDetailModel } from '../types.js'; import type { UmbWebhookEventModel } from '../../webhook-event/types.js'; import { UmbWebhookWorkspaceEditorElement } from './webhook-workspace-editor.element.js'; import { - type UmbSubmittableWorkspaceContext, + UmbEntityNamedDetailWorkspaceContextBase, UmbWorkspaceIsNewRedirectController, - type UmbRoutableWorkspaceContext, - UmbEntityDetailWorkspaceContextBase, UmbWorkspaceIsNewRedirectControllerAlias, } from '@umbraco-cms/backoffice/workspace'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbSubmittableWorkspaceContext, UmbRoutableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; export class UmbWebhookWorkspaceContext - extends UmbEntityDetailWorkspaceContextBase + extends UmbEntityNamedDetailWorkspaceContextBase implements UmbSubmittableWorkspaceContext, UmbRoutableWorkspaceContext { // Observable states readonly headers = this._data.createObservablePartOfCurrent((data) => data?.headers); readonly enabled = this._data.createObservablePartOfCurrent((data) => data?.enabled); readonly url = this._data.createObservablePartOfCurrent((data) => data?.url); - readonly name = this._data.createObservablePartOfCurrent((data) => data?.name); readonly description = this._data.createObservablePartOfCurrent((data) => data?.description); readonly events = this._data.createObservablePartOfCurrent((data) => data?.events); readonly contentTypes = this._data.createObservablePartOfCurrent((data) => data?.contentTypes); @@ -121,15 +119,6 @@ export class UmbWebhookWorkspaceContext this._data.updateCurrent({ url }); } - /** - * Sets the name - * @param {string} name - The name - * @memberof UmbWebhookWorkspaceContext - */ - setName(name: string) { - this._data.updateCurrent({ name }); - } - /** * Sets the description * @param {string} description - The description