From 207c72f67b44ae96eb7b9bdc97b7b9dbf5372e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 29 Mar 2025 19:05:15 +0100 Subject: [PATCH 01/65] todos --- .../block-grid-manager/block-grid-manager.context-token.ts | 2 +- .../block-grid-entries/block-grid-entries.context-token.ts | 2 +- .../block-grid-entry/block-grid-entry.context-token.ts | 2 +- .../block-list/context/block-list-entries.context-token.ts | 2 +- .../block-list/context/block-list-manager.context-token.ts | 2 +- .../block/block-rte/context/block-rte-entries.context-token.ts | 2 +- .../workspace/block-element-property-dataset.context-token.ts | 1 + .../core/menu/menu-tree-structure-workspace-context-base.ts | 1 + 8 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/block-grid-manager.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/block-grid-manager.context-token.ts index a75bae703e04..e11364291757 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/block-grid-manager.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/block-grid-manager.context-token.ts @@ -1,7 +1,7 @@ import type { UmbBlockGridManagerContext } from './block-grid-manager.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -// TODO: Make discriminator method for this: +// TODO: Make discriminator method for this: (Aim to do this for v.16) [NL] export const UMB_BLOCK_GRID_MANAGER_CONTEXT = new UmbContextToken< UmbBlockGridManagerContext, UmbBlockGridManagerContext diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context-token.ts index 6b05007c4659..8fefaa3546cc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context-token.ts @@ -1,5 +1,5 @@ import type { UmbBlockGridEntriesContext } from './block-grid-entries.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -// TODO: Make discriminator method for this: +// TODO: Make discriminator method for this: (Aim to do this for v.16) [NL] export const UMB_BLOCK_GRID_ENTRIES_CONTEXT = new UmbContextToken('UmbBlockEntriesContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.context-token.ts index b5213a3c324b..9ca8efe52efe 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.context-token.ts @@ -1,5 +1,5 @@ import type { UmbBlockGridEntryContext } from './block-grid-entry.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -// TODO: Make discriminator method for this: +// TODO: Make discriminator method for this: (Aim to do this for v.16) [NL] export const UMB_BLOCK_GRID_ENTRY_CONTEXT = new UmbContextToken('UmbBlockEntryContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context-token.ts index b477dc4685e3..547bd5f8179f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context-token.ts @@ -1,5 +1,5 @@ import type { UmbBlockListEntriesContext } from './block-list-entries.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -// TODO: Make discriminator method for this: +// TODO: Make discriminator method for this: (Aim to do this for v.16) [NL] export const UMB_BLOCK_LIST_ENTRIES_CONTEXT = new UmbContextToken('UmbBlockEntriesContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context-token.ts index bde5ce5b25bf..43f4621c9f0f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context-token.ts @@ -1,7 +1,7 @@ import type { UmbBlockListManagerContext } from './block-list-manager.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -// TODO: Make discriminator method for this: +// TODO: Make discriminator method for this: (Aim to do this for v.16) export const UMB_BLOCK_LIST_MANAGER_CONTEXT = new UmbContextToken< UmbBlockListManagerContext, UmbBlockListManagerContext diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context-token.ts index bda7bbbaa595..df7290295f7f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context-token.ts @@ -1,5 +1,5 @@ import type { UmbBlockRteEntriesContext } from './block-rte-entries.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -// TODO: Make discriminator method for this: +// TODO: Make discriminator method for this: (Aim to do this for v.16) [NL] export const UMB_BLOCK_RTE_ENTRIES_CONTEXT = new UmbContextToken('UmbBlockEntriesContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-property-dataset.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-property-dataset.context-token.ts index db5f957cb234..d3efbb298e5e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-property-dataset.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-property-dataset.context-token.ts @@ -1,6 +1,7 @@ import type { UmbBlockElementPropertyDatasetContext } from './block-element-property-dataset.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +// TODO: Use a discriminator (Aim to do this for v.16) [NL] export const UMB_BLOCK_ELEMENT_PROPERTY_DATASET_CONTEXT = new UmbContextToken( 'UmbPropertyDatasetContext', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-tree-structure-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-tree-structure-workspace-context-base.ts index 59da518fa33a..0881ff1cfa0e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-tree-structure-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-tree-structure-workspace-context-base.ts @@ -22,6 +22,7 @@ export abstract class UmbMenuTreeStructureWorkspaceContextBase extends UmbContex constructor(host: UmbControllerHost, args: UmbMenuTreeStructureWorkspaceContextBaseArgs) { // TODO: set up context token + // TODO: Use UmbWorkspaceContext as Context Alias and then an additional Api Alias. (aim to do this for v.16) [NL] super(host, 'UmbMenuStructureWorkspaceContext'); this.#args = args; From 78b3d76738869185ad1f7336386ed053c4942dc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 29 Mar 2025 20:08:00 +0100 Subject: [PATCH 02/65] navigation context --- ...workspace-view-navigation.context-token.ts | 7 + .../workspace-view-navigation.context.ts | 201 ++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context-token.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context-token.ts new file mode 100644 index 000000000000..a2e8c488d6f3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context-token.ts @@ -0,0 +1,7 @@ +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import type { UmbWorkspaceViewNavigationContext } from './workspace-view-navigation.context'; + +export const UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT = new UmbContextToken< + UmbWorkspaceViewNavigationContext, + UmbWorkspaceViewNavigationContext +>('UmbWworkspaceViewNavigationCcontext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts new file mode 100644 index 000000000000..b80c3928cbbb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts @@ -0,0 +1,201 @@ +import type { ManifestWorkspaceView } from '../../extensions/types.js'; +import { UMB_WORKSPACE_VIEW_PATH_PATTERN } from '../../paths.js'; +import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { createExtensionElement, UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UmbRoute, UmbRouterSlotInitEvent, UmbRouterSlotChangeEvent } from '@umbraco-cms/backoffice/router'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; + +export class UmbWorkspaceViewNavigationContext extends UmbContextBase< + UmbWorkspaceViewNavigationContext, + UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT +> { + @property() + public headline = ''; + + @property({ type: Boolean }) + public hideNavigation = false; + + @property({ type: Boolean }) + public enforceNoFooter = false; + + @property({ attribute: 'back-path' }) + public backPath?: string; + + @property({ type: Boolean }) + public loading = false; + + @state() + private _workspaceViews: Array = []; + + @state() + private _routes?: UmbRoute[]; + + @state() + private _routerPath?: string; + + @state() + private _activePath?: string; + + constructor() { + super(); + + new UmbExtensionsManifestInitializer(this, umbExtensionsRegistry, 'workspaceView', null, (workspaceViews) => { + this._workspaceViews = workspaceViews.map((view) => view.manifest); + this._createRoutes(); + }); + } + + private _createRoutes() { + let newRoutes: UmbRoute[] = []; + + if (this._workspaceViews.length > 0) { + newRoutes = this._workspaceViews.map((manifest) => { + return { + path: UMB_WORKSPACE_VIEW_PATH_PATTERN.generateLocal({ viewPathname: manifest.meta.pathname }), + component: () => createExtensionElement(manifest), + setup: (component) => { + if (component) { + (component as any).manifest = manifest; + } + }, + } as UmbRoute; + }); + + // Duplicate first workspace and use it for the empty path scenario. [NL] + newRoutes.push({ ...newRoutes[0], path: '' }); + + newRoutes.push({ + path: `**`, + component: async () => (await import('@umbraco-cms/backoffice/router')).UmbRouteNotFoundElement, + }); + } + + this._routes = newRoutes; + } + + override render() { + return html` + + ${this.#renderBackButton()} + + ${this.#renderViews()} + + ${this.#renderRoutes()} + + ${when( + !this.enforceNoFooter, + () => html` + + + + + `, + )} + + `; + } + + #renderViews() { + return html` + ${!this.hideNavigation && this._workspaceViews.length > 1 + ? html` + + ${repeat( + this._workspaceViews, + (view) => view.alias, + (view, index) => + // Notice how we use index 0 to determine which workspace that is active with empty path. [NL] + html` + + + ${view.meta.label ? this.localize.string(view.meta.label) : view.name} + + `, + )} + + ` + : nothing} + `; + } + + #renderBackButton() { + if (!this.backPath) return nothing; + return html` + + + + `; + } + + #renderRoutes() { + if (!this._routes || this._routes.length === 0) return nothing; + return html` + { + this._routerPath = event.target.absoluteRouterPath; + }} + @change=${(event: UmbRouterSlotChangeEvent) => { + this._activePath = event.target.localActiveViewPath; + }}> + `; + } + + static override styles = [ + UmbTextStyles, + css` + :host { + display: block; + width: 100%; + height: 100%; + } + + #router-slot { + display: flex; + flex-direction: column; + height: 100%; + } + + .back-button { + margin-right: var(--uui-size-space-4); + } + + uui-input { + width: 100%; + } + + uui-tab-group { + --uui-tab-divider: var(--uui-color-border); + border-left: 1px solid var(--uui-color-border); + border-right: 1px solid var(--uui-color-border); + } + + umb-extension-slot[slot='actions'] { + display: flex; + gap: var(--uui-size-space-2); + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-workspace-editor': UmbWorkspaceEditorElement; + } +} From af40088b41223d49b3f6ed4c19e617632705cb91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 29 Mar 2025 20:08:27 +0100 Subject: [PATCH 03/65] replace raw manifests with view context --- .../components/workspace-editor/index.ts | 2 + .../workspace-editor.element.ts | 52 ++-- ...workspace-view-navigation.context-token.ts | 9 +- .../workspace-view-navigation.context.ts | 233 +++--------------- .../workspace-view.context-token.ts | 4 + .../workspace-view.context.ts | 33 +++ 6 files changed, 114 insertions(+), 219 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context-token.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/index.ts index c372e7e902e8..8899a1aa6c7e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/index.ts @@ -1 +1,3 @@ export * from './workspace-editor.element.js'; +export * from './workspace-view-navigation.context-token.js'; +export * from './workspace-view-navigation.context.js'; 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 845b2248af58..d5e4d3eaa708 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 @@ -1,11 +1,11 @@ -import type { ManifestWorkspaceView } from '../../extensions/types.js'; import { UMB_WORKSPACE_VIEW_PATH_PATTERN } from '../../paths.js'; import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; -import { createExtensionElement, UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbRoute, UmbRouterSlotInitEvent, UmbRouterSlotChangeEvent } from '@umbraco-cms/backoffice/router'; +import { UmbWorkspaceViewNavigationContext } from './workspace-view-navigation.context.js'; +import type { UmbWorkspaceViewContext } from './workspace-view.context.js'; /** * @element umb-workspace-editor @@ -21,6 +21,9 @@ import type { UmbRoute, UmbRouterSlotInitEvent, UmbRouterSlotChangeEvent } from */ @customElement('umb-workspace-editor') export class UmbWorkspaceEditorElement extends UmbLitElement { + // + #navigationContext = new UmbWorkspaceViewNavigationContext(this); + @property() public headline = ''; @@ -37,7 +40,7 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { public loading = false; @state() - private _workspaceViews: Array = []; + private _workspaceViews: Array = []; @state() private _routes?: UmbRoute[]; @@ -51,23 +54,29 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { constructor() { super(); - new UmbExtensionsManifestInitializer(this, umbExtensionsRegistry, 'workspaceView', null, (workspaceViews) => { - this._workspaceViews = workspaceViews.map((view) => view.manifest); - this._createRoutes(); - }); + this.observe( + this.#navigationContext.views, + (views) => { + this._workspaceViews = views; + this._createRoutes(); + }, + 'observeWorkspaceViews', + ); } private _createRoutes() { let newRoutes: UmbRoute[] = []; if (this._workspaceViews.length > 0) { - newRoutes = this._workspaceViews.map((manifest) => { + newRoutes = this._workspaceViews.map((context) => { + const manifest = context.manifest; return { path: UMB_WORKSPACE_VIEW_PATH_PATTERN.generateLocal({ viewPathname: manifest.meta.pathname }), component: () => createExtensionElement(manifest), - setup: (component) => { + setup: (component: any) => { if (component) { - (component as any).manifest = manifest; + component.manifest = manifest; + context.provideAt(component); } }, } as UmbRoute; @@ -114,20 +123,23 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { ${repeat( this._workspaceViews, - (view) => view.alias, - (view, index) => + (view) => view.manifest.alias, + (view, index) => { + const manifest = view.manifest; + const displayName = manifest.meta.label ? this.localize.string(manifest.meta.label) : manifest.name; // Notice how we use index 0 to determine which workspace that is active with empty path. [NL] html` - - ${view.meta.label ? this.localize.string(view.meta.label) : view.name} + data-mark="workspace:view-link:${manifest.alias}"> + + ${displayName} - `, + `; + }, )} ` diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context-token.ts index a2e8c488d6f3..9fcf13c45a22 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context-token.ts @@ -1,7 +1,6 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbWorkspaceViewNavigationContext } from './workspace-view-navigation.context'; +import type { UmbWorkspaceViewNavigationContext } from './workspace-view-navigation.context.js'; -export const UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT = new UmbContextToken< - UmbWorkspaceViewNavigationContext, - UmbWorkspaceViewNavigationContext ->('UmbWworkspaceViewNavigationCcontext'); +export const UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT = new UmbContextToken( + 'UmbWorkspaceViewNavigationContext', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts index b80c3928cbbb..a9d15d98a8d1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts @@ -1,201 +1,46 @@ -import type { ManifestWorkspaceView } from '../../extensions/types.js'; -import { UMB_WORKSPACE_VIEW_PATH_PATTERN } from '../../paths.js'; -import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; -import { createExtensionElement, UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { UmbRoute, UmbRouterSlotInitEvent, UmbRouterSlotChangeEvent } from '@umbraco-cms/backoffice/router'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT } from './workspace-view-navigation.context-token.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbBasicState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbWorkspaceViewContext } from './workspace-view.context.js'; export class UmbWorkspaceViewNavigationContext extends UmbContextBase< UmbWorkspaceViewNavigationContext, - UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT + typeof UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT > { - @property() - public headline = ''; - - @property({ type: Boolean }) - public hideNavigation = false; - - @property({ type: Boolean }) - public enforceNoFooter = false; - - @property({ attribute: 'back-path' }) - public backPath?: string; - - @property({ type: Boolean }) - public loading = false; - - @state() - private _workspaceViews: Array = []; - - @state() - private _routes?: UmbRoute[]; - - @state() - private _routerPath?: string; - - @state() - private _activePath?: string; - - constructor() { - super(); - - new UmbExtensionsManifestInitializer(this, umbExtensionsRegistry, 'workspaceView', null, (workspaceViews) => { - this._workspaceViews = workspaceViews.map((view) => view.manifest); - this._createRoutes(); - }); - } - - private _createRoutes() { - let newRoutes: UmbRoute[] = []; - - if (this._workspaceViews.length > 0) { - newRoutes = this._workspaceViews.map((manifest) => { - return { - path: UMB_WORKSPACE_VIEW_PATH_PATTERN.generateLocal({ viewPathname: manifest.meta.pathname }), - component: () => createExtensionElement(manifest), - setup: (component) => { - if (component) { - (component as any).manifest = manifest; - } - }, - } as UmbRoute; - }); - - // Duplicate first workspace and use it for the empty path scenario. [NL] - newRoutes.push({ ...newRoutes[0], path: '' }); - - newRoutes.push({ - path: `**`, - component: async () => (await import('@umbraco-cms/backoffice/router')).UmbRouteNotFoundElement, - }); - } - - this._routes = newRoutes; - } - - override render() { - return html` - - ${this.#renderBackButton()} - - ${this.#renderViews()} - - ${this.#renderRoutes()} - - ${when( - !this.enforceNoFooter, - () => html` - - - - - `, - )} - - `; - } - - #renderViews() { - return html` - ${!this.hideNavigation && this._workspaceViews.length > 1 - ? html` - - ${repeat( - this._workspaceViews, - (view) => view.alias, - (view, index) => - // Notice how we use index 0 to determine which workspace that is active with empty path. [NL] - html` - - - ${view.meta.label ? this.localize.string(view.meta.label) : view.name} - - `, - )} - - ` - : nothing} - `; - } - - #renderBackButton() { - if (!this.backPath) return nothing; - return html` - - - - `; - } - - #renderRoutes() { - if (!this._routes || this._routes.length === 0) return nothing; - return html` - { - this._routerPath = event.target.absoluteRouterPath; - }} - @change=${(event: UmbRouterSlotChangeEvent) => { - this._activePath = event.target.localActiveViewPath; - }}> - `; - } - - static override styles = [ - UmbTextStyles, - css` - :host { - display: block; - width: 100%; - height: 100%; - } - - #router-slot { - display: flex; - flex-direction: column; - height: 100%; - } - - .back-button { - margin-right: var(--uui-size-space-4); - } - - uui-input { - width: 100%; - } - - uui-tab-group { - --uui-tab-divider: var(--uui-color-border); - border-left: 1px solid var(--uui-color-border); - border-right: 1px solid var(--uui-color-border); - } - - umb-extension-slot[slot='actions'] { - display: flex; - gap: var(--uui-size-space-2); - } - `, - ]; -} - -declare global { - interface HTMLElementTagNameMap { - 'umb-workspace-editor': UmbWorkspaceEditorElement; + // + #views = new UmbBasicState(>[]); + public readonly views = this.#views.asObservable(); + + constructor(host: UmbControllerHost) { + super(host, UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT); + + new UmbExtensionsManifestInitializer( + this, + umbExtensionsRegistry, + 'workspaceView', + null, + (workspaceViews) => { + const oldViews = this.#views.getValue(); + + // remove ones that are no longer contained in the workspaceViews (And thereby make the new array): + const newViews = oldViews.filter( + (view) => !workspaceViews.some((x) => x.manifest.alias === view.manifest.alias), + ); + + // Add ones that are new: + workspaceViews + .filter((view) => !newViews.some((x) => x.manifest.alias === view.manifest.alias)) + .forEach((view) => { + newViews.push(new UmbWorkspaceViewContext(this, view.manifest)); + }); + + this.#views.setValue(newViews); + }, + 'initViewApis', + {}, + ); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context-token.ts new file mode 100644 index 000000000000..48ee4f1f4f56 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context-token.ts @@ -0,0 +1,4 @@ +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import type { UmbWorkspaceViewContext } from './workspace-view.context.js'; + +export const UMB_WORKSPACE_VIEW_CONTEXT = new UmbContextToken('UmbWorkspaceViewContext'); 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 new file mode 100644 index 000000000000..bcd21347f0cb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context.ts @@ -0,0 +1,33 @@ +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbControllerBase, type UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; +import type { ManifestWorkspaceView } from '../../types.js'; +import { UMB_WORKSPACE_VIEW_CONTEXT } from './workspace-view.context-token.js'; + +export class UmbWorkspaceViewContext extends UmbControllerBase { + // + #providerCtrl: any; + #currentProvideHost?: UmbClassInterface; + + manifest: ManifestWorkspaceView; + + constructor(host: UmbControllerHost, manifest: ManifestWorkspaceView) { + super(host); + this.manifest = manifest; + } + + provideAt(controllerHost: UmbClassInterface): void { + if (this.#currentProvideHost === controllerHost) return; + + this.unprovide(); + + this.#currentProvideHost = controllerHost; + this.#providerCtrl = controllerHost.provideContext(UMB_WORKSPACE_VIEW_CONTEXT, this); + } + + unprovide(): void { + if (this.#providerCtrl) { + this.#providerCtrl.destroy(); + this.#providerCtrl = undefined; + } + } +} From 982c6b95c5133bd1aaf641d212b3d3d2c09bd10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 29 Mar 2025 22:33:12 +0100 Subject: [PATCH 04/65] Array State has method --- .../observable-api/states/array-state.test.ts | 7 ++++++ .../libs/observable-api/states/array-state.ts | 25 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.test.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.test.ts index e2a4cdbe588f..8ddc8dc6e45c 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.test.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.test.ts @@ -44,6 +44,13 @@ describe('ArrayState', () => { }); }); + it('has method, return true when key exists', () => { + expect(subject.has('2')).to.be.true; + }); + it('has method, return false when key does not exists', () => { + expect(subject.has('1337')).to.be.true; + }); + it('filter method, removes anything that is not true of the given predicate method', (done) => { const expectedData = [initialData[0], initialData[2]]; diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts index 579efc0e3c17..c838a83e575d 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts @@ -61,6 +61,31 @@ export class UmbArrayState extends UmbDeepState { } } + /** + * @function has + * @param {unknown} unique - The unique value to remove. + * @returns {boolean} true if an entry with the given unique exists. + * @description - Test if a entry with the given unique exists in this Subject. + * @example Example test for entry with id '2' and '3' + * const data = [ + * { id: 1, value: 'foo'}, + * { id: 2, value: 'bar'} + * ]; + * const myState = new UmbArrayState(data, (x) => x.id); + * myState.has(2);// true + * myState.has(3);// false + */ + has(unique: unknown): boolean { + if (this.getUniqueMethod) { + const current = this.getValue(); + if (!current) return false; + return current.some((x) => { + return this.getUniqueMethod(x) === unique; + }); + } + return false; + } + /** * @function remove * @param {unknown[]} uniques - The unique values to remove. From 7365fc7088487884b0050d5cf2e6653e400517b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 29 Mar 2025 22:39:12 +0100 Subject: [PATCH 05/65] rename to hint and much more --- .../examples/workspace-view-hint/README.md | 3 + .../hint-workspace-view.ts | 56 +++++++++++++++++++ .../examples/workspace-view-hint/index.ts | 22 ++++++++ .../workspace-editor.element.ts | 45 +++++++++++++-- .../workspace-view-navigation.context.ts | 17 +++++- .../workspace-view.context.ts | 32 +++++++++++ 6 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/examples/workspace-view-hint/README.md create mode 100644 src/Umbraco.Web.UI.Client/examples/workspace-view-hint/hint-workspace-view.ts create mode 100644 src/Umbraco.Web.UI.Client/examples/workspace-view-hint/index.ts diff --git a/src/Umbraco.Web.UI.Client/examples/workspace-view-hint/README.md b/src/Umbraco.Web.UI.Client/examples/workspace-view-hint/README.md new file mode 100644 index 000000000000..37d60ccfba80 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/workspace-view-hint/README.md @@ -0,0 +1,3 @@ +# Workspace View Badge Example + +This example demonstrates the essence of the Workspace View Navigation Context. And how to append a Status that will be displayed as a badge on the Workspace Views Tab Navigation. 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 new file mode 100644 index 000000000000..6baf94a71088 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/workspace-view-hint/hint-workspace-view.ts @@ -0,0 +1,56 @@ +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_NAVIGATION_CONTEXT } from '@umbraco-cms/backoffice/workspace'; + +@customElement('example-hint-workspace-view') +export class ExampleHintWorkspaceView extends UmbElementMixin(LitElement) { + // + + async onClick() { + const context = await this.getContext(UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT); + const view = await context.getViewContext('example.workspaceView.hint'); + if (!view) { + throw new Error('Could not find the view'); + } + + if (view.hasHint('exampleHintFromToggleAction')) { + view.removeHint('exampleHintFromToggleAction'); + } else { + view.addHint({ + unique: 'exampleHintFromToggleAction', + text: 'Hi', + color: 'invalid', + weight: 100, + }); + } + } + + override render() { + return html` + +

See the hint on this views tab

+

This is toggle on/off via this button:

+ Toggle hint +
+ `; + } + + static override styles = [ + UmbTextStyles, + css` + :host { + display: block; + padding: var(--uui-size-layout-1); + } + `, + ]; +} + +export { ExampleHintWorkspaceView as element }; + +declare global { + interface HTMLElementTagNameMap { + 'example-hint-workspace-view': ExampleHintWorkspaceView; + } +} diff --git a/src/Umbraco.Web.UI.Client/examples/workspace-view-hint/index.ts b/src/Umbraco.Web.UI.Client/examples/workspace-view-hint/index.ts new file mode 100644 index 000000000000..48f5d7a9810f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/workspace-view-hint/index.ts @@ -0,0 +1,22 @@ +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; + +export const manifests: Array = [ + { + type: 'workspaceView', + name: 'Example Badge Workspace View', + alias: 'example.workspaceView.hint', + element: () => import('./hint-workspace-view.js'), + weight: 900, + meta: { + label: 'View with badge', + pathname: 'badge', + icon: 'icon-lab', + }, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: 'Umb.Workspace.Document', + }, + ], + }, +]; 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 d5e4d3eaa708..c24aabfebe51 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 @@ -5,7 +5,8 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbRoute, UmbRouterSlotInitEvent, UmbRouterSlotChangeEvent } from '@umbraco-cms/backoffice/router'; import { UmbWorkspaceViewNavigationContext } from './workspace-view-navigation.context.js'; -import type { UmbWorkspaceViewContext } from './workspace-view.context.js'; +import type { UmbWorkspaceViewNavigationState, UmbWorkspaceViewContext } from './workspace-view.context.js'; +import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; /** * @element umb-workspace-editor @@ -22,7 +23,9 @@ import type { UmbWorkspaceViewContext } from './workspace-view.context.js'; @customElement('umb-workspace-editor') export class UmbWorkspaceEditorElement extends UmbLitElement { // + // TODO: Concider making the NavigationContext host on Workspace level instead, but that would be breaking as well. #navigationContext = new UmbWorkspaceViewNavigationContext(this); + #workspaceViewStateObservers: Array = []; @property() public headline = ''; @@ -42,6 +45,9 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { @state() private _workspaceViews: Array = []; + @state() + private _hintMap: Map = new Map(); + @state() private _routes?: UmbRoute[]; @@ -57,7 +63,19 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { this.observe( this.#navigationContext.views, (views) => { + this.#workspaceViewStateObservers.forEach((observer) => observer.destroy()); + this._hintMap = new Map(); this._workspaceViews = views; + this.#workspaceViewStateObservers = views.map((view, index) => + this.observe( + view.hint, + (state) => { + this._hintMap.set(view.manifest.alias, state); + this.requestUpdate('_states'); + }, + 'umbObserveState_' + index, + ), + ); this._createRoutes(); }, 'observeWorkspaceViews', @@ -127,15 +145,22 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { (view, index) => { const manifest = view.manifest; const displayName = manifest.meta.label ? this.localize.string(manifest.meta.label) : manifest.name; + const hint = this._hintMap.get(manifest.alias); // Notice how we use index 0 to determine which workspace that is active with empty path. [NL] - html` + return html` - +
+ ${hint + ? html`${hint.text}` + : nothing} +
${displayName}
`; @@ -207,6 +232,18 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { border-right: 1px solid var(--uui-color-border); } + div[slot='icon'] { + position: relative; + } + + uui-badge { + position: absolute; + font-size: var(--uui-type-small-size); + top: -0.5em; + right: auto; + left: calc(50% + 0.8em); + } + umb-extension-slot[slot='actions'] { display: flex; gap: var(--uui-size-space-2); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts index a9d15d98a8d1..470649bb6e4b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts @@ -11,13 +11,14 @@ export class UmbWorkspaceViewNavigationContext extends UmbContextBase< typeof UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT > { // + #init: Promise; #views = new UmbBasicState(>[]); public readonly views = this.#views.asObservable(); constructor(host: UmbControllerHost) { super(host, UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT); - new UmbExtensionsManifestInitializer( + this.#init = new UmbExtensionsManifestInitializer( this, umbExtensionsRegistry, 'workspaceView', @@ -30,17 +31,27 @@ export class UmbWorkspaceViewNavigationContext extends UmbContextBase< (view) => !workspaceViews.some((x) => x.manifest.alias === view.manifest.alias), ); + let hasDif = newViews.length !== oldViews.length; + // Add ones that are new: workspaceViews .filter((view) => !newViews.some((x) => x.manifest.alias === view.manifest.alias)) .forEach((view) => { newViews.push(new UmbWorkspaceViewContext(this, view.manifest)); + hasDif = true; }); - this.#views.setValue(newViews); + if (hasDif) { + this.#views.setValue(newViews); + } }, 'initViewApis', {}, - ); + ).asPromise(); + } + + async getViewContext(alias: string): Promise { + await this.#init; + return this.#views.getValue().find((view) => view.manifest.alias === alias); } } 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 bcd21347f0cb..9f14e8095b63 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 @@ -2,6 +2,15 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbControllerBase, type UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; import type { ManifestWorkspaceView } from '../../types.js'; import { UMB_WORKSPACE_VIEW_CONTEXT } from './workspace-view.context-token.js'; +import type { UUIInterfaceColor } from '@umbraco-ui/uui'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; + +export type UmbWorkspaceViewNavigationState = { + unique: string | symbol; + text: string; + weight: number; + color?: UUIInterfaceColor; +}; export class UmbWorkspaceViewContext extends UmbControllerBase { // @@ -10,9 +19,15 @@ export class UmbWorkspaceViewContext extends UmbControllerBase { manifest: ManifestWorkspaceView; + #hints = new UmbArrayState([], (x) => x.unique); + readonly hints = this.#hints.asObservable(); + readonly hint = this.#hints.asObservablePart((x) => x[0]); + constructor(host: UmbControllerHost, manifest: ManifestWorkspaceView) { super(host); this.manifest = manifest; + + this.#hints.sortBy((a, b) => (b.weight || 0) - (a.weight || 0)); } provideAt(controllerHost: UmbClassInterface): void { @@ -30,4 +45,21 @@ export class UmbWorkspaceViewContext extends UmbControllerBase { this.#providerCtrl = undefined; } } + + hasHint(unique: string | symbol): boolean { + return this.#hints.has(unique); + } + + addHint(state: Partial): string | symbol { + let newState = { ...state } as UmbWorkspaceViewNavigationState; + newState.unique ??= Symbol(); + newState.weight ??= 0; + newState.text ??= '!'; + this.#hints.appendOne(newState); + return newState.unique; + } + + removeHint(unique: string | symbol): void { + this.#hints.removeOne(unique); + } } From d482a21537e974ff99b810e90e2673c9a26ed4b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 29 Mar 2025 22:58:22 +0100 Subject: [PATCH 06/65] Notes for later --- .../components/workspace-editor/workspace-editor.element.ts | 4 ++++ 1 file changed, 4 insertions(+) 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 c24aabfebe51..233d64f310b0 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 @@ -24,6 +24,10 @@ import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-a export class UmbWorkspaceEditorElement extends UmbLitElement { // // TODO: Concider making the NavigationContext host on Workspace level instead, but that would be breaking as well. + // TODO: Or make another broader context for the Workspace that can host hints across cultures. — Cause if you like an Extension Type Context that is for each culture, then such would be able to host hints for each culture. + // But then again Hints should be able to be replicated when opening a Document, meaning the responsible for them is a Workspace Context. This will then set hints, and in this case it should be able to append a culture or segment, or make it wider than such. + // Then these should been observe here in the workspace-editor(here), for the current culture and segment? and also for the invariant culture. (not invariant/default segment) + // This would mean that the WorkspaceEditor would have a WorkspaceNavigationContext, and that would be able to host hints for each culture and segment. And then this one should not have a WorkspaceViewNavigationContext, the WorkspaceViewContext should also go away but a WorkspaceViewNavigationContext should exist and this should support begin provided at multiple elements cause we need to think about Split View. #navigationContext = new UmbWorkspaceViewNavigationContext(this); #workspaceViewStateObservers: Array = []; From b492203948e175324b567889a779ddc8a863a75b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 29 Mar 2025 22:58:59 +0100 Subject: [PATCH 07/65] correcting one word --- .../components/workspace-editor/workspace-editor.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 233d64f310b0..63b76fbcd11a 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 @@ -27,7 +27,7 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { // TODO: Or make another broader context for the Workspace that can host hints across cultures. — Cause if you like an Extension Type Context that is for each culture, then such would be able to host hints for each culture. // But then again Hints should be able to be replicated when opening a Document, meaning the responsible for them is a Workspace Context. This will then set hints, and in this case it should be able to append a culture or segment, or make it wider than such. // Then these should been observe here in the workspace-editor(here), for the current culture and segment? and also for the invariant culture. (not invariant/default segment) - // This would mean that the WorkspaceEditor would have a WorkspaceNavigationContext, and that would be able to host hints for each culture and segment. And then this one should not have a WorkspaceViewNavigationContext, the WorkspaceViewContext should also go away but a WorkspaceViewNavigationContext should exist and this should support begin provided at multiple elements cause we need to think about Split View. + // This would mean that the Workspace would have a WorkspaceNavigationContext, and that would be able to host hints for each culture and segment. And then this one should not have a WorkspaceViewNavigationContext, the WorkspaceViewContext should also go away but a WorkspaceViewNavigationContext should exist and this should support begin provided at multiple elements cause we need to think about Split View. #navigationContext = new UmbWorkspaceViewNavigationContext(this); #workspaceViewStateObservers: Array = []; From 7724575a838ebdc8429f8733c6fd8d7e7e7672fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 29 Mar 2025 23:12:08 +0100 Subject: [PATCH 08/65] more notes --- .../components/workspace-editor/workspace-editor.element.ts | 1 + 1 file changed, 1 insertion(+) 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 63b76fbcd11a..b5aee39c3965 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 @@ -28,6 +28,7 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { // But then again Hints should be able to be replicated when opening a Document, meaning the responsible for them is a Workspace Context. This will then set hints, and in this case it should be able to append a culture or segment, or make it wider than such. // Then these should been observe here in the workspace-editor(here), for the current culture and segment? and also for the invariant culture. (not invariant/default segment) // This would mean that the Workspace would have a WorkspaceNavigationContext, and that would be able to host hints for each culture and segment. And then this one should not have a WorkspaceViewNavigationContext, the WorkspaceViewContext should also go away but a WorkspaceViewNavigationContext should exist and this should support begin provided at multiple elements cause we need to think about Split View. + // It then also means that this element should be able to get a Variant ID, I think via a property, and once that is set then we consume the WorkspaceViewNavigationContext and listens for hints for that variant. #navigationContext = new UmbWorkspaceViewNavigationContext(this); #workspaceViewStateObservers: Array = []; From 5d2aab75435688f933b8814c3844ee424f8b21e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 29 Mar 2025 23:14:21 +0100 Subject: [PATCH 09/65] update JS Docs --- .../workspace-editor/workspace-editor.element.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 b5aee39c3965..a3b08964aaa6 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 @@ -11,13 +11,12 @@ import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-a /** * @element umb-workspace-editor * @description - * @slot icon - Slot for icon * @slot header - Slot for workspace header - * @slot name - Slot for name - * @slot footer - Slot for workspace footer + * @slot action-menu - Slot for workspace header + * @slot footer-info - Slot for workspace footer * @slot actions - Slot for workspace footer actions * @slot - slot for main content - * @class UmbWorkspaceEditor + * @class UmbWorkspaceEditorElement * @augments {UmbLitElement} */ @customElement('umb-workspace-editor') From e2d8c13b6b231fbda2d8460c181620f5a5e37b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 6 May 2025 18:39:41 +0200 Subject: [PATCH 10/65] update tests for getHasOne --- .../src/libs/observable-api/states/array-state.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.test.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.test.ts index 8ddc8dc6e45c..3d0d9afaa57e 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.test.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.test.ts @@ -44,11 +44,11 @@ describe('ArrayState', () => { }); }); - it('has method, return true when key exists', () => { - expect(subject.has('2')).to.be.true; + it('getHasOne method, return true when key exists', () => { + expect(subject.getHasOne('2')).to.be.true; }); - it('has method, return false when key does not exists', () => { - expect(subject.has('1337')).to.be.true; + it('getHasOne method, return false when key does not exists', () => { + expect(subject.getHasOne('1337')).to.be.true; }); it('filter method, removes anything that is not true of the given predicate method', (done) => { From 55c8a086c5def7cd2cc8c2da63346b4da847d51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 6 May 2025 18:46:16 +0200 Subject: [PATCH 11/65] fix context api usage --- .../examples/workspace-view-hint/hint-workspace-view.ts | 3 +++ 1 file changed, 3 insertions(+) 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 6baf94a71088..491c2f9a8715 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 @@ -9,6 +9,9 @@ export class ExampleHintWorkspaceView extends UmbElementMixin(LitElement) { async onClick() { const context = await this.getContext(UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT); + if (!context) { + throw new Error('Could not find the context'); + } const view = await context.getViewContext('example.workspaceView.hint'); if (!view) { throw new Error('Could not find the view'); From 62013f2e797a5cbfd61470b985d51de7c79f937b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 6 May 2025 18:46:42 +0200 Subject: [PATCH 12/65] update code for v.16 --- .../workspace-view-navigation.context-token.ts | 2 +- .../workspace-view-navigation.context.ts | 9 +++------ .../workspace-editor/workspace-view.context.ts | 10 +++++----- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context-token.ts index 9fcf13c45a22..d30580ef9884 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context-token.ts @@ -1,5 +1,5 @@ -import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import type { UmbWorkspaceViewNavigationContext } from './workspace-view-navigation.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; export const UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT = new UmbContextToken( 'UmbWorkspaceViewNavigationContext', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts index 470649bb6e4b..ecadb42e5279 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts @@ -1,15 +1,12 @@ -import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbWorkspaceViewContext } from './workspace-view.context.js'; import { UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT } from './workspace-view-navigation.context-token.js'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbBasicState } from '@umbraco-cms/backoffice/observable-api'; -import { UmbWorkspaceViewContext } from './workspace-view.context.js'; -export class UmbWorkspaceViewNavigationContext extends UmbContextBase< - UmbWorkspaceViewNavigationContext, - typeof UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT -> { +export class UmbWorkspaceViewNavigationContext extends UmbContextBase { // #init: Promise; #views = new UmbBasicState(>[]); 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 9f14e8095b63..e85b73fe3336 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,8 +1,8 @@ -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbControllerBase, type UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; import type { ManifestWorkspaceView } from '../../types.js'; import { UMB_WORKSPACE_VIEW_CONTEXT } from './workspace-view.context-token.js'; -import type { UUIInterfaceColor } from '@umbraco-ui/uui'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbControllerBase, type UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; +import type { UUIInterfaceColor } from '@umbraco-cms/backoffice/external/uui'; import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; export type UmbWorkspaceViewNavigationState = { @@ -47,11 +47,11 @@ export class UmbWorkspaceViewContext extends UmbControllerBase { } hasHint(unique: string | symbol): boolean { - return this.#hints.has(unique); + return this.#hints.getHasOne(unique); } addHint(state: Partial): string | symbol { - let newState = { ...state } as UmbWorkspaceViewNavigationState; + const newState = { ...state } as UmbWorkspaceViewNavigationState; newState.unique ??= Symbol(); newState.weight ??= 0; newState.text ??= '!'; From dd83e8420d6b763a97892a96c464efe2b50ca2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 6 May 2025 19:49:21 +0200 Subject: [PATCH 13/65] correct test --- .../src/libs/observable-api/states/array-state.test.ts | 2 +- .../components/workspace-editor/workspace-view.context-token.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.test.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.test.ts index 3d0d9afaa57e..d9d5d1ad4f88 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.test.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.test.ts @@ -48,7 +48,7 @@ describe('ArrayState', () => { expect(subject.getHasOne('2')).to.be.true; }); it('getHasOne method, return false when key does not exists', () => { - expect(subject.getHasOne('1337')).to.be.true; + expect(subject.getHasOne('1337')).to.be.false; }); it('filter method, removes anything that is not true of the given predicate method', (done) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context-token.ts index 48ee4f1f4f56..284bb8312177 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context-token.ts @@ -1,4 +1,4 @@ -import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import type { UmbWorkspaceViewContext } from './workspace-view.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; export const UMB_WORKSPACE_VIEW_CONTEXT = new UmbContextToken('UmbWorkspaceViewContext'); From 4642e0e32b09b5b1d6cfd8fe29121c283d5029a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 6 May 2025 19:52:36 +0200 Subject: [PATCH 14/65] export UMB_WORKSPACE_VIEW_CONTEXT --- .../packages/core/workspace/components/workspace-editor/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/index.ts index 8899a1aa6c7e..2ab5af77f9b4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/index.ts @@ -1,3 +1,4 @@ export * from './workspace-editor.element.js'; export * from './workspace-view-navigation.context-token.js'; export * from './workspace-view-navigation.context.js'; +export * from './workspace-view.context-token.js'; From 5f59f4b0cffbdca5e9b862468932ca3cba2c0f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 6 May 2025 20:14:06 +0200 Subject: [PATCH 15/65] minor corrections --- .../workspace-editor/workspace-editor.element.ts | 10 +++++----- .../workspace-view-navigation.context.ts | 3 +++ 2 files changed, 8 insertions(+), 5 deletions(-) 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 d0f80709e519..4fc191598c64 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 @@ -1,11 +1,11 @@ import { UMB_WORKSPACE_VIEW_PATH_PATTERN } from '../../paths.js'; +import { UmbWorkspaceViewNavigationContext } from './workspace-view-navigation.context.js'; +import type { UmbWorkspaceViewNavigationState, UmbWorkspaceViewContext } from './workspace-view.context.js'; import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbRoute, UmbRouterSlotInitEvent, UmbRouterSlotChangeEvent } from '@umbraco-cms/backoffice/router'; -import { UmbWorkspaceViewNavigationContext } from './workspace-view-navigation.context.js'; -import type { UmbWorkspaceViewNavigationState, UmbWorkspaceViewContext } from './workspace-view.context.js'; import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; /** @@ -29,7 +29,7 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { // This would mean that the Workspace would have a WorkspaceNavigationContext, and that would be able to host hints for each culture and segment. And then this one should not have a WorkspaceViewNavigationContext, the WorkspaceViewContext should also go away but a WorkspaceViewNavigationContext should exist and this should support begin provided at multiple elements cause we need to think about Split View. // It then also means that this element should be able to get a Variant ID, I think via a property, and once that is set then we consume the WorkspaceViewNavigationContext and listens for hints for that variant. #navigationContext = new UmbWorkspaceViewNavigationContext(this); - #workspaceViewStateObservers: Array = []; + #workspaceViewHintObservers: Array = []; @property() public headline = ''; @@ -67,10 +67,10 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { this.observe( this.#navigationContext.views, (views) => { - this.#workspaceViewStateObservers.forEach((observer) => observer.destroy()); + this.#workspaceViewHintObservers.forEach((observer) => observer.destroy()); this._hintMap = new Map(); this._workspaceViews = views; - this.#workspaceViewStateObservers = views.map((view, index) => + this.#workspaceViewHintObservers = views.map((view, index) => this.observe( view.hint, (state) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts index ecadb42e5279..9ed5cb38181a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts @@ -9,6 +9,9 @@ import { UmbBasicState } from '@umbraco-cms/backoffice/observable-api'; export class UmbWorkspaceViewNavigationContext extends UmbContextBase { // #init: Promise; + /** + * State holding the permitted Workspace Views as a Workspace View Context + */ #views = new UmbBasicState(>[]); public readonly views = this.#views.asObservable(); From 1ba948e6becd2ae4d26aac01b2b219f796c7a75f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 6 May 2025 20:16:39 +0200 Subject: [PATCH 16/65] rename to _hintMap --- .../components/workspace-editor/workspace-editor.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4fc191598c64..65e2ec165966 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 @@ -75,7 +75,7 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { view.hint, (state) => { this._hintMap.set(view.manifest.alias, state); - this.requestUpdate('_states'); + this.requestUpdate('_hintMap'); }, 'umbObserveState_' + index, ), From 97533a4d488a7ee71768b5901026399326a83020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 8 May 2025 08:58:21 +0200 Subject: [PATCH 17/65] refactor part 1 --- .../hint-workspace-view.ts | 8 +- .../content-detail-workspace-base.ts | 8 +- .../core/utils/hint-manager/hint-manager.ts | 159 ++++++++++++++++++ .../packages/core/utils/hint-manager/index.ts | 1 + .../src/packages/core/utils/index.ts | 1 + .../workspace-editor.element.ts | 99 +++++++---- .../workspace-view-navigation.context.ts | 29 ++-- .../workspace-view.context.ts | 52 +++--- .../workspace-split-view.context.ts | 35 ++-- .../workspace-split-view.element.ts | 15 +- .../core/workspace/controllers/index.ts | 3 +- .../workspace-view-hint-manager.controller.ts | 12 ++ 12 files changed, 328 insertions(+), 94 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/utils/hint-manager/hint-manager.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/utils/hint-manager/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-view-hint-manager.controller.ts 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 491c2f9a8715..8fedcae1e466 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 @@ -2,6 +2,7 @@ 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_NAVIGATION_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; @customElement('example-hint-workspace-view') export class ExampleHintWorkspaceView extends UmbElementMixin(LitElement) { @@ -17,14 +18,15 @@ export class ExampleHintWorkspaceView extends UmbElementMixin(LitElement) { throw new Error('Could not find the view'); } - if (view.hasHint('exampleHintFromToggleAction')) { - view.removeHint('exampleHintFromToggleAction'); + if (view.hints.has('exampleHintFromToggleAction')) { + view.hints.removeOne('exampleHintFromToggleAction'); } else { - view.addHint({ + view.hints.addOne({ unique: 'exampleHintFromToggleAction', text: 'Hi', color: 'invalid', weight: 100, + variantId: new UmbVariantId('en-US'), }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts index bbfa248cd6a5..4a3adf8dacff 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts @@ -11,6 +11,7 @@ import type { UmbDetailRepository, UmbDetailRepositoryConstructor } from '@umbra import { UmbEntityDetailWorkspaceContextBase, UmbWorkspaceSplitViewManager, + UmbWorkspaceViewHintManager, type UmbEntityDetailWorkspaceContextArgs, type UmbEntityDetailWorkspaceContextCreateArgs, } from '@umbraco-cms/backoffice/workspace'; @@ -135,6 +136,9 @@ export abstract class UmbContentDetailWorkspaceContextBase< /* Split View */ readonly splitView = new UmbWorkspaceSplitViewManager(); + /* Hints */ + readonly hints = new UmbWorkspaceViewHintManager(this); + /* Variant Options */ // TODO: Optimize this so it uses either a App Language Context? [NL] #languageRepository = new UmbLanguageCollectionRepository(this); @@ -818,8 +822,8 @@ export abstract class UmbContentDetailWorkspaceContextBase< variantIds = result?.selection.map((x) => UmbVariantId.FromString(x)) ?? []; } else { - /* If there are multiple variants but no modal token is set - we will save the variants that would have been preselected in the modal. + /* If there are multiple variants but no modal token is set + we will save the variants that would have been preselected in the modal. These are based on the variants that have been edited */ variantIds = selected.map((x) => UmbVariantId.FromString(x)); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/hint-manager/hint-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/hint-manager/hint-manager.ts new file mode 100644 index 000000000000..b5b1fac704f9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/hint-manager/hint-manager.ts @@ -0,0 +1,159 @@ +import type { UmbPartialSome } from '../type/index.js'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UUIInterfaceColor } from '@umbraco-cms/backoffice/external/uui'; +import { UmbArrayState, type Observable } from '@umbraco-cms/backoffice/observable-api'; + +export interface UmbIncomingHintBase { + unique?: string | symbol; + text: string; + weight?: number; + color?: UUIInterfaceColor; +} + +export interface UmbHint extends UmbIncomingHintBase { + unique: string | symbol; + weight: number; +} + +export class UmbHintManager< + HintType extends UmbHint = UmbHint, + IncomingHintType extends UmbIncomingHintBase = UmbPartialSome, +> extends UmbControllerBase { + // + #scaffold?: Partial; + + protected readonly _hints = new UmbArrayState([], (x) => x.unique); + public readonly hints = this._hints.asObservable(); + public readonly firstHint = this._hints.asObservablePart((x) => x[0]); + //public readonly hasHints = this._hints.asObservablePart((x) => x.length > 0); + + constructor(host: UmbControllerBase, args?: { scaffold?: Partial }) { + super(host); + + this.#scaffold = args?.scaffold; + + this._hints.sortBy((a, b) => (b.weight || 0) - (a.weight || 0)); + } + + asObservablePart(fn: (hints: HintType[]) => R): Observable { + return this._hints.asObservablePart(fn); + } + + #parent?: UmbHintManager; + #parentHints: HintType[] = []; + #localHints: HintType[] = []; + bindWith(parent: UmbHintManager): void { + if (this.#parent) { + this.#parentHints = []; + this.#localHints = []; + } + this.#parent = parent; + this.observe( + parent.hints, + (hints) => { + this._hints.mute(); + + this.#parentHints = hints as unknown as HintType[]; + + // Remove the local hints that does not exist in the parent anymore: + const toRemove = this.#parentHints.filter((msg) => !hints.find((m) => m.unique === msg.unique)); + this._hints.remove(toRemove.map((msg) => msg.unique)); + this._hints.append(this.#parentHints); + this.#localHints = this._hints.getValue(); + this._hints.unmute(); + }, + 'observeParentHints', + ); + this.observe( + this.hints, + (hints) => { + if (!this.#parent) return; + + this.#parent!.initiateChange(); + + // Remove the parent messages that does not exist locally anymore: + const toRemove = this.#localHints.filter((locals) => !hints.find((m) => m.unique === locals.unique)); + this.#parent!.remove(toRemove.map((x) => x.unique)); + this.#parent!.add(hints); + this.#parent!.finishChange(); + }, + 'observeLocalHints', + ); + } + + initiateChange() { + this._hints.mute(); + } + finishChange() { + this._hints.unmute(); + } + + /** + * Add a new hint + * @param {HintType} hint - The hint to add + * @returns {HintType['unique']} Unique value of the hint + */ + addOne(hint: IncomingHintType): string | symbol { + const newHint = { ...this.#scaffold, ...hint } as unknown as HintType; + newHint.unique ??= Symbol(); + newHint.weight ??= 0; + newHint.text ??= '!'; + this._hints.appendOne(newHint); + return hint.unique!; + } + + /** + * Add multiple rules + * @param {HintType[]} hints - Array of hints to add + */ + add(hints: IncomingHintType[]) { + this._hints.mute(); + hints.forEach((hint) => this.addOne(hint)); + this._hints.unmute(); + } + + /** + * Remove a hint + * @param {HintType['unique']} unique Unique value of the hint to remove + */ + removeOne(unique: HintType['unique']) { + this._hints.removeOne(unique); + } + + /** + * Remove multiple hints + * @param {HintType['unique'][]} uniques Array of unique values to remove + */ + remove(uniques: HintType['unique'][]) { + this._hints.remove(uniques); + } + + /** + * Check if a hint exists + * @param {HintType['unique']} unique Unique value of the hint to check + * @returns {boolean} True if the hint exists, false otherwise + */ + has(unique: HintType['unique']): boolean { + return this._hints.getHasOne(unique); + } + + /** + * Get all hints + * @returns {HintType[]} Array of hints + */ + getAll(): HintType[] { + return this._hints.getValue(); + } + + /** + * Clear all hints + */ + clear(): void { + this._hints.setValue([]); + } + + override destroy() { + this._hints.destroy(); + super.destroy(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/hint-manager/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/hint-manager/index.ts new file mode 100644 index 000000000000..07fecfdf2416 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/hint-manager/index.ts @@ -0,0 +1 @@ +export * from './hint-manager.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts index aa191db2e730..d35aea7061a6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts @@ -7,6 +7,7 @@ export * from './download/blob-download.function.js'; export * from './get-guid-from-udi.function.js'; export * from './get-processed-image-url.function.js'; export * from './guard-manager/index.js'; +export * from './hint-manager/index.js'; export * from './math/math.js'; export * from './media/image-size.function.js'; export * from './object/deep-merge.function.js'; 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 65e2ec165966..828570ba3192 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 @@ -1,12 +1,14 @@ import { UMB_WORKSPACE_VIEW_PATH_PATTERN } from '../../paths.js'; +import type { UmbWorkspaceHint } from '../../controllers/workspace-view-hint-manager.controller.js'; import { UmbWorkspaceViewNavigationContext } from './workspace-view-navigation.context.js'; -import type { UmbWorkspaceViewNavigationState, UmbWorkspaceViewContext } from './workspace-view.context.js'; +import type { UmbWorkspaceViewContext } from './workspace-view.context.js'; import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbRoute, UmbRouterSlotInitEvent, UmbRouterSlotChangeEvent } from '@umbraco-cms/backoffice/router'; import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; /** * @element umb-workspace-editor @@ -28,6 +30,12 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { // Then these should been observe here in the workspace-editor(here), for the current culture and segment? and also for the invariant culture. (not invariant/default segment) // This would mean that the Workspace would have a WorkspaceNavigationContext, and that would be able to host hints for each culture and segment. And then this one should not have a WorkspaceViewNavigationContext, the WorkspaceViewContext should also go away but a WorkspaceViewNavigationContext should exist and this should support begin provided at multiple elements cause we need to think about Split View. // It then also means that this element should be able to get a Variant ID, I think via a property, and once that is set then we consume the WorkspaceViewNavigationContext and listens for hints for that variant. + /* + --- UPDATE 06/05/2025 --- + Well, because we use individually, we should make sure this works independently of the workspace context. + So the question goes how can we sync this upward and downward for just specific variant ids. Or sync all, but only show by a filter.. + I think all hints should support the concept of variant id, but it should be optional if we like to filter by it. + */ #navigationContext = new UmbWorkspaceViewNavigationContext(this); #workspaceViewHintObservers: Array = []; @@ -46,11 +54,21 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { @property({ type: Boolean }) public loading = false; + @property({ attribute: false }) + public get variantId(): UmbVariantId | undefined { + return this._variantId; + } + public set variantId(value: UmbVariantId | undefined) { + this._variantId = value; + this.#observeWorkspaceViewHints(); + } + private _variantId?: UmbVariantId | undefined; + @state() private _workspaceViews: Array = []; @state() - private _hintMap: Map = new Map(); + private _hintMap: Map = new Map(); @state() private _routes?: UmbRoute[]; @@ -63,30 +81,37 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { constructor() { super(); - this.observe( this.#navigationContext.views, (views) => { - this.#workspaceViewHintObservers.forEach((observer) => observer.destroy()); - this._hintMap = new Map(); this._workspaceViews = views; - this.#workspaceViewHintObservers = views.map((view, index) => - this.observe( - view.hint, - (state) => { - this._hintMap.set(view.manifest.alias, state); - this.requestUpdate('_hintMap'); - }, - 'umbObserveState_' + index, - ), - ); - this._createRoutes(); + this.#observeWorkspaceViewHints(); + this.#createRoutes(); }, - 'observeWorkspaceViews', + null, ); } - private _createRoutes() { + #observeWorkspaceViewHints() { + this.#workspaceViewHintObservers.forEach((observer) => observer.destroy()); + this._hintMap = new Map(); + this.#workspaceViewHintObservers = this._workspaceViews.map((view, index) => + this.observe( + view.hintOfVariant(this._variantId), + (hint) => { + if (hint) { + this._hintMap.set(view.manifest.alias, hint); + } else { + this._hintMap.delete(view.manifest.alias); + } + this.requestUpdate('_hintMap'); + }, + 'umbObserveState_' + index, + ), + ); + } + + #createRoutes() { let newRoutes: UmbRoute[] = []; if (this._workspaceViews.length > 0) { @@ -117,24 +142,26 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { } override render() { - return html` - - ${this.#renderBackButton()} - - - ${this.#renderViews()} ${this.#renderRoutes()} - - ${when( - !this.enforceNoFooter, - () => html` - - - - - `, - )} - - `; + return this._routes + ? html` + + ${this.#renderBackButton()} + + + ${this.#renderViews()} ${this.#renderRoutes()} + + ${when( + !this.enforceNoFooter, + () => html` + + + + + `, + )} + + ` + : nothing; } #renderViews() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts index 9ed5cb38181a..a8aa5020df77 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts @@ -5,6 +5,7 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbBasicState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbHintManager } from '@umbraco-cms/backoffice/utils'; export class UmbWorkspaceViewNavigationContext extends UmbContextBase { // @@ -15,6 +16,11 @@ export class UmbWorkspaceViewNavigationContext extends UmbContextBase { #views = new UmbBasicState(>[]); public readonly views = this.#views.asObservable(); + /** HERE THIS IS THE PLACE TO CONTINUE!!!!!!! */ + + // TODO: We still need to sync these up to the workspace context... + #hints = new UmbHintManager(this); + constructor(host: UmbControllerHost) { super(host, UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT); @@ -27,21 +33,24 @@ export class UmbWorkspaceViewNavigationContext extends UmbContextBase { const oldViews = this.#views.getValue(); // remove ones that are no longer contained in the workspaceViews (And thereby make the new array): - const newViews = oldViews.filter( + const viewsToKeep = oldViews.filter( (view) => !workspaceViews.some((x) => x.manifest.alias === view.manifest.alias), ); - let hasDif = newViews.length !== oldViews.length; + const diff = viewsToKeep.length !== workspaceViews.length; + + if (diff) { + const newViews = [...viewsToKeep]; - // Add ones that are new: - workspaceViews - .filter((view) => !newViews.some((x) => x.manifest.alias === view.manifest.alias)) - .forEach((view) => { - newViews.push(new UmbWorkspaceViewContext(this, view.manifest)); - hasDif = true; - }); + // Add ones that are new: + workspaceViews + .filter((view) => !viewsToKeep.some((x) => x.manifest.alias === view.manifest.alias)) + .forEach((view) => { + const context = new UmbWorkspaceViewContext(this, view.manifest); + context.hints.bindWith(this.#hints); + newViews.push(context); + }); - if (hasDif) { this.#views.setValue(newViews); } }, 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 e85b73fe3336..3973e936b3f3 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,33 +1,29 @@ import type { ManifestWorkspaceView } from '../../types.js'; +import type { UmbWorkspaceHint } from '../../controllers/workspace-view-hint-manager.controller.js'; import { UMB_WORKSPACE_VIEW_CONTEXT } from './workspace-view.context-token.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbControllerBase, type UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; -import type { UUIInterfaceColor } from '@umbraco-cms/backoffice/external/uui'; -import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import type { Observable } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbHintManager } from '@umbraco-cms/backoffice/utils'; -export type UmbWorkspaceViewNavigationState = { - unique: string | symbol; - text: string; - weight: number; - color?: UUIInterfaceColor; -}; +export interface UmbWorkspaceViewHint extends UmbWorkspaceHint { + viewAlias: string; +} export class UmbWorkspaceViewContext extends UmbControllerBase { // #providerCtrl: any; #currentProvideHost?: UmbClassInterface; - manifest: ManifestWorkspaceView; + public manifest: ManifestWorkspaceView; - #hints = new UmbArrayState([], (x) => x.unique); - readonly hints = this.#hints.asObservable(); - readonly hint = this.#hints.asObservablePart((x) => x[0]); + public hints; constructor(host: UmbControllerHost, manifest: ManifestWorkspaceView) { super(host); this.manifest = manifest; - - this.#hints.sortBy((a, b) => (b.weight || 0) - (a.weight || 0)); + this.hints = new UmbHintManager(this, { scaffold: { viewAlias: manifest.alias } }); } provideAt(controllerHost: UmbClassInterface): void { @@ -46,20 +42,18 @@ export class UmbWorkspaceViewContext extends UmbControllerBase { } } - hasHint(unique: string | symbol): boolean { - return this.#hints.getHasOne(unique); - } - - addHint(state: Partial): string | symbol { - const newState = { ...state } as UmbWorkspaceViewNavigationState; - newState.unique ??= Symbol(); - newState.weight ??= 0; - newState.text ??= '!'; - this.#hints.appendOne(newState); - return newState.unique; - } - - removeHint(unique: string | symbol): void { - this.#hints.removeOne(unique); + /** + * observe hint of an optional variant, the variant is optional, in that case all variantIds are accepted. + * @param {UmbVariantId} variantId - the variantId to match against the hint. + * @returns {Observable} the first hint that matches the variantId or undefined if no hint is found. + */ + hintOfVariant(variantId?: UmbVariantId): Observable { + const viewAlias = this.manifest.alias; + if (variantId) { + return this.hints.asObservablePart((x) => + x.find((hint) => hint.viewAlias === viewAlias && (hint.variantId ? hint.variantId.compare(variantId) : true)), + ); + } + return this.hints.asObservablePart((x) => x.find((hint) => hint.viewAlias === viewAlias)); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view.context.ts index 6ff5ac73e408..eb40274c91e0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view.context.ts @@ -2,7 +2,7 @@ import { UMB_VARIANT_WORKSPACE_CONTEXT } from '../../contexts/index.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; -import { UmbBooleanState, UmbNumberState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbBooleanState, UmbClassState, UmbNumberState } from '@umbraco-cms/backoffice/observable-api'; import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import type { UmbPropertyDatasetContext } from '@umbraco-cms/backoffice/property'; import { UMB_MARK_ATTRIBUTE_NAME } from '@umbraco-cms/backoffice/const'; @@ -24,21 +24,25 @@ export class UmbWorkspaceSplitViewContext extends UmbContextBase { #isNew = new UmbBooleanState(undefined); isNew = this.#isNew.asObservable(); - //#variantId = new UmbClassState(undefined); - //variantId = this.#variantId.asObservable(); + #variantId = new UmbClassState(undefined); + variantId = this.#variantId.asObservable(); constructor(host: UmbControllerHost) { super(host, UMB_WORKSPACE_SPLIT_VIEW_CONTEXT); this.consumeContext(UMB_VARIANT_WORKSPACE_CONTEXT, (context) => { this.#workspaceContext = context; - this._observeVariant(); + this.#observeVariant(); this.#observeIsNew(); }); - this.observe(this.index, () => { - this._observeVariant(); - }); + this.observe( + this.index, + () => { + this.#observeVariant(); + }, + null, + ); } #observeIsNew() { @@ -51,7 +55,7 @@ export class UmbWorkspaceSplitViewContext extends UmbContextBase { ); } - private _observeVariant() { + #observeVariant() { if (!this.#workspaceContext) return; const index = this.#index.getValue(); @@ -67,6 +71,7 @@ export class UmbWorkspaceSplitViewContext extends UmbContextBase { this.#datasetContext?.destroy(); const variantId = UmbVariantId.Create(activeVariantInfo); + this.#variantId.setValue(variantId); const validationContext = this.#workspaceContext?.getVariantValidationContext(variantId); if (validationContext) { @@ -109,11 +114,17 @@ export class UmbWorkspaceSplitViewContext extends UmbContextBase { * concept this class could have methods to set and get the culture and segment of the active variant? just by using the index. */ - /* - public destroy(): void { - + public override destroy(): void { + this.#isNew.destroy(); + this.#variantId.destroy(); + this.#index.destroy(); + this.#variantVariantValidationContext?.unprovide(); + this.#datasetContext?.destroy(); + this.#workspaceContext = undefined; + this.#variantVariantValidationContext = undefined; + this.#datasetContext = undefined; + super.destroy(); } - */ } export const UMB_WORKSPACE_SPLIT_VIEW_CONTEXT = new UmbContextToken( diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view.element.ts index 4deb1346658d..32e1c33fb069 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view.element.ts @@ -14,6 +14,7 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; // import local components import './workspace-split-view-variant-selector.element.js'; +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; /** * @@ -43,6 +44,9 @@ export class UmbWorkspaceSplitViewElement extends UmbLitElement { @state() private _isNew = false; + @state() + private _variantId?: UmbVariantId; + splitViewContext = new UmbWorkspaceSplitViewContext(this); #onVariantSelectorSlotChanged(e: Event) { @@ -57,7 +61,15 @@ export class UmbWorkspaceSplitViewElement extends UmbLitElement { (isNew) => { this._isNew = isNew ?? false; }, - 'umbObserveIsNew', + null, + ); + + this.observe( + this.splitViewContext.variantId, + (variantId) => { + this._variantId = variantId; + }, + null, ); } @@ -66,6 +78,7 @@ export class UmbWorkspaceSplitViewElement extends UmbLitElement { ${when( diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/index.ts index fce8ba5e8744..f29a5d018290 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/index.ts @@ -1,3 +1,4 @@ export * from './workspace-is-new-redirect.controller.js'; -export * from './workspace-split-view-manager.controller.js'; export * from './workspace-route-manager.controller.js'; +export * from './workspace-split-view-manager.controller.js'; +export * from './workspace-view-hint-manager.controller.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-view-hint-manager.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-view-hint-manager.controller.ts new file mode 100644 index 000000000000..66536f7faedf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-view-hint-manager.controller.ts @@ -0,0 +1,12 @@ +import type { UmbVariantId } from '../../variant/variant-id.class.js'; +import { UmbHintManager, type UmbHint } from '@umbraco-cms/backoffice/utils'; + +export interface UmbWorkspaceHint extends UmbHint { + variantId?: UmbVariantId; +} + +/** + * @class UmbWorkspaceViewHintManager + * @description - Class managing the hints of views in a workspace. + */ +export class UmbWorkspaceViewHintManager extends UmbHintManager {} From 61ceb57ea4cc883a1ac4540f0cee4b67f2c9d580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 11 Aug 2025 14:24:35 +0200 Subject: [PATCH 18/65] update version number in comment --- .../block-grid-manager/block-grid-manager.context-token.ts | 2 +- .../block-grid-entries/block-grid-entries.context-token.ts | 2 +- .../block-grid-entry/block-grid-entry.context-token.ts | 2 +- .../block-list/context/block-list-entries.context-token.ts | 2 +- .../block/block-rte/context/block-rte-entries.context-token.ts | 2 +- .../workspace/block-element-property-dataset.context-token.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/block-grid-manager.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/block-grid-manager.context-token.ts index e11364291757..fc68fde79397 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/block-grid-manager.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/block-grid-manager.context-token.ts @@ -1,7 +1,7 @@ import type { UmbBlockGridManagerContext } from './block-grid-manager.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -// TODO: Make discriminator method for this: (Aim to do this for v.16) [NL] +// TODO: Make discriminator method for this: (Aim to do this for v.17) [NL] export const UMB_BLOCK_GRID_MANAGER_CONTEXT = new UmbContextToken< UmbBlockGridManagerContext, UmbBlockGridManagerContext diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context-token.ts index 8fefaa3546cc..13754ae34be2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context-token.ts @@ -1,5 +1,5 @@ import type { UmbBlockGridEntriesContext } from './block-grid-entries.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -// TODO: Make discriminator method for this: (Aim to do this for v.16) [NL] +// TODO: Make discriminator method for this: (Aim to do this for v.17) [NL] export const UMB_BLOCK_GRID_ENTRIES_CONTEXT = new UmbContextToken('UmbBlockEntriesContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.context-token.ts index 9ca8efe52efe..3c0bc679bf8d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.context-token.ts @@ -1,5 +1,5 @@ import type { UmbBlockGridEntryContext } from './block-grid-entry.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -// TODO: Make discriminator method for this: (Aim to do this for v.16) [NL] +// TODO: Make discriminator method for this: (Aim to do this for v.17) [NL] export const UMB_BLOCK_GRID_ENTRY_CONTEXT = new UmbContextToken('UmbBlockEntryContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context-token.ts index 547bd5f8179f..f81b388cdfac 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context-token.ts @@ -1,5 +1,5 @@ import type { UmbBlockListEntriesContext } from './block-list-entries.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -// TODO: Make discriminator method for this: (Aim to do this for v.16) [NL] +// TODO: Make discriminator method for this: (Aim to do this for v.17) [NL] export const UMB_BLOCK_LIST_ENTRIES_CONTEXT = new UmbContextToken('UmbBlockEntriesContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context-token.ts index df7290295f7f..296c2c89ae01 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context-token.ts @@ -1,5 +1,5 @@ import type { UmbBlockRteEntriesContext } from './block-rte-entries.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -// TODO: Make discriminator method for this: (Aim to do this for v.16) [NL] +// TODO: Make discriminator method for this: (Aim to do this for v.17) [NL] export const UMB_BLOCK_RTE_ENTRIES_CONTEXT = new UmbContextToken('UmbBlockEntriesContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-property-dataset.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-property-dataset.context-token.ts index d3efbb298e5e..e7e3169c386a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-property-dataset.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-property-dataset.context-token.ts @@ -1,7 +1,7 @@ import type { UmbBlockElementPropertyDatasetContext } from './block-element-property-dataset.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -// TODO: Use a discriminator (Aim to do this for v.16) [NL] +// TODO: Use a discriminator (Aim to do this for v.17) [NL] export const UMB_BLOCK_ELEMENT_PROPERTY_DATASET_CONTEXT = new UmbContextToken( 'UmbPropertyDatasetContext', ); From ff46b18648b9586966d15a5cbd8d8a35baf5cfbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 14 Aug 2025 13:08:18 +0200 Subject: [PATCH 19/65] clear method for array states --- .../content/workspace/content-detail-workspace-base.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts index 02b257c65a15..a65be13ae5c2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts @@ -11,7 +11,6 @@ import type { UmbDetailRepository, UmbDetailRepositoryConstructor } from '@umbra import { UmbEntityDetailWorkspaceContextBase, UmbWorkspaceSplitViewManager, - UmbWorkspaceViewHintManager, type UmbEntityDetailWorkspaceContextArgs, type UmbEntityDetailWorkspaceContextCreateArgs, type UmbSaveableWorkspaceContext, @@ -55,6 +54,7 @@ import { type UmbPropertyTypePresetModelTypeModel, } from '@umbraco-cms/backoffice/property'; import { UmbSegmentCollectionRepository, type UmbSegmentCollectionItemModel } from '@umbraco-cms/backoffice/segment'; +import { UmbHintController } from '@umbraco-cms/backoffice/hint'; export interface UmbContentDetailWorkspaceContextArgs< DetailModelType extends UmbContentDetailModel, @@ -140,7 +140,7 @@ export abstract class UmbContentDetailWorkspaceContextBase< readonly splitView = new UmbWorkspaceSplitViewManager(); /* Hints */ - readonly hints = new UmbWorkspaceViewHintManager(this); + readonly hints = new UmbHintController(this); /* Variant Options */ // TODO: Optimize this so it uses either a App Language Context? [NL] From fa5067c37f4302b2e17338c1af7296e28d21a2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 14 Aug 2025 13:08:38 +0200 Subject: [PATCH 20/65] declare hint import map --- src/Umbraco.Web.UI.Client/package.json | 1 + src/Umbraco.Web.UI.Client/tsconfig.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 640d3feac830..e1de28bec945 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -52,6 +52,7 @@ "./extension-registry": "./dist-cms/packages/core/extension-registry/index.js", "./health-check": "./dist-cms/packages/health-check/index.js", "./help": "./dist-cms/packages/help/index.js", + "./hint": "./dist-cms/packages/core/hint/index.js", "./http-client": "./dist-cms/packages/core/http-client/index.js", "./icon": "./dist-cms/packages/core/icon-registry/index.js", "./id": "./dist-cms/packages/core/id/index.js", diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index 23592d2c5b0d..577c436413e7 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -14,7 +14,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js "moduleDetection": "force", "verbatimModuleSyntax": true, "target": "es2022", - "lib": ["es2022", "dom", "dom.iterable", "WebWorker"], + "lib": ["es2022", "dom", "dom.iterable"], "outDir": "./types", "allowSyntheticDefaultImports": true, "experimentalDecorators": true, @@ -81,6 +81,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js "@umbraco-cms/backoffice/extension-registry": ["./src/packages/core/extension-registry/index.ts"], "@umbraco-cms/backoffice/health-check": ["./src/packages/health-check/index.ts"], "@umbraco-cms/backoffice/help": ["./src/packages/help/index.ts"], + "@umbraco-cms/backoffice/hint": ["./src/packages/core/hint/index.ts"], "@umbraco-cms/backoffice/http-client": ["./src/packages/core/http-client/index.ts"], "@umbraco-cms/backoffice/icon": ["./src/packages/core/icon-registry/index.ts"], "@umbraco-cms/backoffice/id": ["./src/packages/core/id/index.ts"], From 47f03e285b36a647cb986b521db2b36b3b02c7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 14 Aug 2025 13:08:55 +0200 Subject: [PATCH 21/65] mega refactor --- .../hint-workspace-view.ts | 10 +- .../libs/observable-api/states/array-state.ts | 21 +- .../core/hint/context/hint.context-token.ts | 4 + .../core/hint/context/hint.controller.ts | 246 ++++++++++++++++++ .../src/packages/core/hint/context/index.ts | 1 + .../src/packages/core/hint/index.ts | 1 + .../core/utils/hint-manager/hint-manager.ts | 159 ----------- .../packages/core/utils/hint-manager/index.ts | 1 - .../src/packages/core/utils/index.ts | 1 - .../validation/context/validation.context.ts | 1 - .../components/workspace-editor/index.ts | 4 +- ...rkspace-editor-navigation.context-token.ts | 6 + ...=> workspace-editor-navigation.context.ts} | 23 +- .../workspace-editor.element.ts | 5 +- ...workspace-view-navigation.context-token.ts | 6 - .../workspace-view.context.ts | 49 ++-- .../workspace-view-hint-manager.controller.ts | 12 - 17 files changed, 334 insertions(+), 216 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.context-token.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/hint/context/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/hint/index.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/utils/hint-manager/hint-manager.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/utils/hint-manager/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context-token.ts rename src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/{workspace-view-navigation.context.ts => workspace-editor-navigation.context.ts} (73%) delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context-token.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-view-hint-manager.controller.ts 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 8fedcae1e466..6932749ba286 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,7 +1,7 @@ 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_NAVIGATION_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { UMB_WORKSPACE_VIEW_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; @customElement('example-hint-workspace-view') @@ -9,15 +9,22 @@ export class ExampleHintWorkspaceView extends UmbElementMixin(LitElement) { // async onClick() { + /* const context = await this.getContext(UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT); if (!context) { throw new Error('Could not find the context'); } const view = await context.getViewContext('example.workspaceView.hint'); + */ + const view = await this.getContext(UMB_WORKSPACE_VIEW_CONTEXT); if (!view) { throw new Error('Could not find the view'); } + this.observe(view.hints.hints, (hints) => { + console.log('Local Hints:', hints, this); + }); + if (view.hints.has('exampleHintFromToggleAction')) { view.hints.removeOne('exampleHintFromToggleAction'); } else { @@ -26,7 +33,6 @@ export class ExampleHintWorkspaceView extends UmbElementMixin(LitElement) { text: 'Hi', color: 'invalid', weight: 100, - variantId: new UmbVariantId('en-US'), }); } } diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts index a057b8bb1596..26cf9261abf2 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts @@ -48,10 +48,10 @@ export class UmbArrayState extends UmbDeepState { * @param {T} data - The next data for this state to hold. * @description - Set the data of this state, if sortBy has been defined for this state the data will be sorted before set. If data is different than current this will trigger observations to update. * @example Example change the data of a state - * const myState = new UmbArrayState('Good morning'); - * // myState.value is equal 'Good morning'. - * myState.setValue('Goodnight') - * // myState.value is equal 'Goodnight'. + * const myState = new UmbArrayState(['Good morning']); + * // myState.value is equal ['Good morning']. + * myState.setValue(['Goodnight']) + * // myState.value is equal ['Goodnight']. */ override setValue(value: T[]) { if (value && this.#sortMethod) { @@ -61,6 +61,19 @@ export class UmbArrayState extends UmbDeepState { } } + /** + * @function clear + * @description - Set the data of this state to an empty array. + * @example Example clearing the data of a state + * const myState = new UmbArrayState(['Good morning']); + * // myState.value is equal ['Good morning']. + * myState.clear() + * // myState.value is equal []. + */ + clear() { + super.setValue([]); + } + /** * @function getHasOne * @param {U} unique - the unique value to compare with. diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.context-token.ts new file mode 100644 index 000000000000..d95030626cff --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.context-token.ts @@ -0,0 +1,4 @@ +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import type { UmbHintController } from './hint.controller.js'; + +export const UMB_HINT_CONTEXT = new UmbContextToken('UmbHintContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts new file mode 100644 index 000000000000..969955f255ea --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts @@ -0,0 +1,246 @@ +import type { UmbPartialSome } from '../../utils/type/index.js'; +import { UmbContextBase, UmbControllerBase, type UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UUIInterfaceColor } from '@umbraco-cms/backoffice/external/uui'; +import { UmbArrayState, type Observable } from '@umbraco-cms/backoffice/observable-api'; +import { UMB_HINT_CONTEXT } from './hint.context-token.js'; +import { ReplaceStartOfPath } from '../../validation/utils/replace-start-of-path.function.js'; +import type { UmbContextProviderController } from '@umbraco-cms/backoffice/context-api'; + +export interface UmbIncomingHintBase { + unique?: string | symbol; + text: string; + weight?: number; + color?: UUIInterfaceColor; +} + +export interface UmbHint extends UmbIncomingHintBase { + unique: string | symbol; + path: Array; + weight: number; +} + +export class UmbHintController< + HintType extends UmbHint = UmbHint, + IncomingHintType extends UmbIncomingHintBase = UmbPartialSome, +> extends UmbControllerBase { + // + #viewAlias?: string; + #scaffold: Partial; + #inUnprovidingState?: boolean; + + #parent?: UmbHintController; + #parentHints?: Array; + + readonly #hints = new UmbArrayState([], (x) => x.unique); + public readonly hints = this.#hints.asObservable(); + public readonly firstHint = this.#hints.asObservablePart((x) => x[0]); + // Consider using weight to determine the visibility distance. [NL] + //public readonly hasHints = this._hints.asObservablePart((x) => x.length > 0); + + updateScaffold(updates: Partial) { + this.#scaffold = { ...this.#scaffold, ...updates }; + } + + constructor(host: UmbControllerHost, args?: { viewAlias?: string; scaffold?: Partial }) { + super(host); + + this.#viewAlias = args?.viewAlias; + this.#scaffold = args?.scaffold ?? {}; + + this.#hints.sortBy((a, b) => (b.weight || 0) - (a.weight || 0)); + } + + #providerCtrl?: UmbContextProviderController; + #currentProvideHost?: UmbClassInterface; + /** + * Provide this validation context to a specific controller host. + * This can be used to Host a validation context in a Workspace, but provide it on a certain scope, like a specific Workspace View. + * @param {UmbClassInterface} controllerHost - The controller host to provide this validation context to. + */ + provideAt(controllerHost: UmbClassInterface): void { + if (this.#currentProvideHost === controllerHost) return; + + this.unprovide(); + + this.#currentProvideHost = controllerHost; + this.#providerCtrl = controllerHost.provideContext(UMB_HINT_CONTEXT, this as any); + } + + unprovide(): void { + if (this.#providerCtrl) { + // We need to set this in Unprovide state, so this context can be provided again later. + this.#inUnprovidingState = true; + this.#providerCtrl.destroy(); + this.#providerCtrl = undefined; + this.#inUnprovidingState = false; + this.#currentProvideHost = undefined; + } + } + + asObservablePart(fn: (hints: HintType[]) => R): Observable { + return this.#hints.asObservablePart(fn); + } + + descendingHints(viewAlias?: string): Observable | undefined> { + if (viewAlias) { + return this.#hints.asObservablePart((hints) => { + return hints.filter((hint) => hint.path[0] === viewAlias); + }); + } else { + return this.hints; + } + } + + inherit(): void { + this.consumeContext(UMB_HINT_CONTEXT, (parent) => { + this.inheritFrom(parent); + }).skipHost(); + // Notice skipHost ^^, this is because we do not want it to consume it self, as this would be a match for this consumption, instead we will look at the parent and above. [NL] + } + + inheritFrom(parent: UmbHintController | undefined): void { + this.#parent = parent; + this.observe( + parent?.descendingHints(this.#viewAlias), + (hints) => { + if (!hints) { + this.#hints.clear(); + return; + } + this.initiateChange(); + if (this.#parentHints) { + // Remove the local messages that does not exist in the parent anymore: + const toRemove = this.#parentHints.filter((msg) => !hints.find((m) => m.unique === msg.unique)); + this.remove(toRemove.map((msg) => msg.unique)); + } + this.#parentHints = hints; + + hints.forEach((hint) => { + // Remove first entry of hint.path: + const path = hint.path.slice(1); + if (path === undefined) { + throw new Error( + 'Path was not transformed correctly and can therefor not be transfered to the local validation context messages.', + ); + } + this.#hints.appendOne({ ...hint, path } as HintType); + }); + + this.finishChange(); + }, + 'observeParentHints', + ); + + this.observe(this.hints, this.#transferHints, 'observeLocalMessages'); + } + + #transferHints = (hints: Array) => { + if (!this.#parent) return; + + this.#parent!.initiateChange(); + + hints.forEach((hint) => { + // replace this.#baseDataPath (if it starts with it) with $ in the path, so it becomes relative to the parent context + const path = hint.path; + if (path === undefined) { + throw new Error('Path was not transformed correctly and can therefor not be synced with parent messages.'); + } + // Notice addOne appends the parent viewAlias to the path: + this.#parent!.addOne(hint); + }); + + this.#parent!.finishChange(); + }; + + initiateChange() { + this.#hints.mute(); + } + finishChange() { + this.#hints.unmute(); + } + + /** + * Add a new hint + * @param {HintType} hint - The hint to add + * @returns {HintType['unique']} Unique value of the hint + */ + addOne(hint: IncomingHintType): string | symbol { + const newHint = { ...this.#scaffold, ...hint } as unknown as HintType; + newHint.unique ??= Symbol(); + newHint.weight ??= 0; + newHint.text ??= '!'; + if (newHint.path) { + if (this.#viewAlias && newHint.path[0] !== this.#viewAlias) { + newHint.path = [this.#viewAlias, ...newHint.path]; + } + } else { + newHint.path = []; + } + this.#hints.appendOne(newHint); + return hint.unique!; + } + + /** + * Add multiple rules + * @param {HintType[]} hints - Array of hints to add + */ + add(hints: IncomingHintType[]) { + this.#hints.mute(); + hints.forEach((hint) => this.addOne(hint)); + this.#hints.unmute(); + } + + /** + * Remove a hint + * @param {HintType['unique']} unique Unique value of the hint to remove + */ + removeOne(unique: HintType['unique']) { + this.#hints.removeOne(unique); + } + + /** + * Remove multiple hints + * @param {HintType['unique'][]} uniques Array of unique values to remove + */ + remove(uniques: HintType['unique'][]) { + this.#hints.remove(uniques); + } + + /** + * Check if a hint exists + * @param {HintType['unique']} unique Unique value of the hint to check + * @returns {boolean} True if the hint exists, false otherwise + */ + has(unique: HintType['unique']): boolean { + return this.#hints.getHasOne(unique); + } + + /** + * Get all hints + * @returns {HintType[]} Array of hints + */ + getAll(): HintType[] { + return this.#hints.getValue(); + } + + /** + * Clear all hints + */ + clear(): void { + this.#hints.setValue([]); + } + + override destroy(): void { + super.destroy(); + if (this.#inUnprovidingState === true) { + return; + } + this.unprovide(); + this.#parentHints = undefined; + this.#parent = undefined; + + this.#hints.destroy(); + super.destroy(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/index.ts new file mode 100644 index 000000000000..1568e218f3fe --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/index.ts @@ -0,0 +1 @@ +export * from './hint.controller.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/index.ts new file mode 100644 index 000000000000..00c55032bc33 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/index.ts @@ -0,0 +1 @@ +export * from './context/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/hint-manager/hint-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/hint-manager/hint-manager.ts deleted file mode 100644 index b5b1fac704f9..000000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/hint-manager/hint-manager.ts +++ /dev/null @@ -1,159 +0,0 @@ -import type { UmbPartialSome } from '../type/index.js'; -import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; -import type { UUIInterfaceColor } from '@umbraco-cms/backoffice/external/uui'; -import { UmbArrayState, type Observable } from '@umbraco-cms/backoffice/observable-api'; - -export interface UmbIncomingHintBase { - unique?: string | symbol; - text: string; - weight?: number; - color?: UUIInterfaceColor; -} - -export interface UmbHint extends UmbIncomingHintBase { - unique: string | symbol; - weight: number; -} - -export class UmbHintManager< - HintType extends UmbHint = UmbHint, - IncomingHintType extends UmbIncomingHintBase = UmbPartialSome, -> extends UmbControllerBase { - // - #scaffold?: Partial; - - protected readonly _hints = new UmbArrayState([], (x) => x.unique); - public readonly hints = this._hints.asObservable(); - public readonly firstHint = this._hints.asObservablePart((x) => x[0]); - //public readonly hasHints = this._hints.asObservablePart((x) => x.length > 0); - - constructor(host: UmbControllerBase, args?: { scaffold?: Partial }) { - super(host); - - this.#scaffold = args?.scaffold; - - this._hints.sortBy((a, b) => (b.weight || 0) - (a.weight || 0)); - } - - asObservablePart(fn: (hints: HintType[]) => R): Observable { - return this._hints.asObservablePart(fn); - } - - #parent?: UmbHintManager; - #parentHints: HintType[] = []; - #localHints: HintType[] = []; - bindWith(parent: UmbHintManager): void { - if (this.#parent) { - this.#parentHints = []; - this.#localHints = []; - } - this.#parent = parent; - this.observe( - parent.hints, - (hints) => { - this._hints.mute(); - - this.#parentHints = hints as unknown as HintType[]; - - // Remove the local hints that does not exist in the parent anymore: - const toRemove = this.#parentHints.filter((msg) => !hints.find((m) => m.unique === msg.unique)); - this._hints.remove(toRemove.map((msg) => msg.unique)); - this._hints.append(this.#parentHints); - this.#localHints = this._hints.getValue(); - this._hints.unmute(); - }, - 'observeParentHints', - ); - this.observe( - this.hints, - (hints) => { - if (!this.#parent) return; - - this.#parent!.initiateChange(); - - // Remove the parent messages that does not exist locally anymore: - const toRemove = this.#localHints.filter((locals) => !hints.find((m) => m.unique === locals.unique)); - this.#parent!.remove(toRemove.map((x) => x.unique)); - this.#parent!.add(hints); - this.#parent!.finishChange(); - }, - 'observeLocalHints', - ); - } - - initiateChange() { - this._hints.mute(); - } - finishChange() { - this._hints.unmute(); - } - - /** - * Add a new hint - * @param {HintType} hint - The hint to add - * @returns {HintType['unique']} Unique value of the hint - */ - addOne(hint: IncomingHintType): string | symbol { - const newHint = { ...this.#scaffold, ...hint } as unknown as HintType; - newHint.unique ??= Symbol(); - newHint.weight ??= 0; - newHint.text ??= '!'; - this._hints.appendOne(newHint); - return hint.unique!; - } - - /** - * Add multiple rules - * @param {HintType[]} hints - Array of hints to add - */ - add(hints: IncomingHintType[]) { - this._hints.mute(); - hints.forEach((hint) => this.addOne(hint)); - this._hints.unmute(); - } - - /** - * Remove a hint - * @param {HintType['unique']} unique Unique value of the hint to remove - */ - removeOne(unique: HintType['unique']) { - this._hints.removeOne(unique); - } - - /** - * Remove multiple hints - * @param {HintType['unique'][]} uniques Array of unique values to remove - */ - remove(uniques: HintType['unique'][]) { - this._hints.remove(uniques); - } - - /** - * Check if a hint exists - * @param {HintType['unique']} unique Unique value of the hint to check - * @returns {boolean} True if the hint exists, false otherwise - */ - has(unique: HintType['unique']): boolean { - return this._hints.getHasOne(unique); - } - - /** - * Get all hints - * @returns {HintType[]} Array of hints - */ - getAll(): HintType[] { - return this._hints.getValue(); - } - - /** - * Clear all hints - */ - clear(): void { - this._hints.setValue([]); - } - - override destroy() { - this._hints.destroy(); - super.destroy(); - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/hint-manager/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/hint-manager/index.ts deleted file mode 100644 index 07fecfdf2416..000000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/hint-manager/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './hint-manager.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts index a8b8f8874bef..da5a0ec8c6a4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts @@ -8,7 +8,6 @@ export * from './download/blob-download.function.js'; export * from './get-guid-from-udi.function.js'; export * from './get-processed-image-url.function.js'; export * from './guard-manager/index.js'; -export * from './hint-manager/index.js'; export * from './math/math.js'; export * from './media/image-size.function.js'; export * from './object/deep-merge.function.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context.ts index 6deed36696b7..8bd9647dd8d7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context.ts @@ -9,7 +9,6 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; */ export class UmbValidationContext extends UmbValidationController { constructor(host: UmbControllerHost) { - // This is overridden to avoid setting a controllerAlias, this might make sense, but currently i want to leave it out. [NL] super(host); this.provideContext(UMB_VALIDATION_CONTEXT, this); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/index.ts index 2ab5af77f9b4..0f8feb20811a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/index.ts @@ -1,4 +1,4 @@ export * from './workspace-editor.element.js'; -export * from './workspace-view-navigation.context-token.js'; -export * from './workspace-view-navigation.context.js'; +export * from './workspace-editor-navigation.context-token.js'; +export * from './workspace-editor-navigation.context.js'; export * from './workspace-view.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context-token.ts new file mode 100644 index 000000000000..eaa35b7f6c24 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context-token.ts @@ -0,0 +1,6 @@ +import type { UmbWorkspaceEditorNavigationContext } from './workspace-editor-navigation.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_WORKSPACE_EDITOR_NAVIGATION_CONTEXT = new UmbContextToken( + 'UmbWorkspaceViewNavigationContext', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts similarity index 73% rename from src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts index a8aa5020df77..8d82ed96146d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts @@ -1,13 +1,14 @@ -import { UmbWorkspaceViewContext } from './workspace-view.context.js'; -import { UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT } from './workspace-view-navigation.context-token.js'; +import { type UmbVariantHint, UmbWorkspaceViewContext } from './workspace-view.context.js'; +import { UMB_WORKSPACE_EDITOR_NAVIGATION_CONTEXT } from './workspace-editor-navigation.context-token.js'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbBasicState } from '@umbraco-cms/backoffice/observable-api'; -import { UmbHintManager } from '@umbraco-cms/backoffice/utils'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbHintController } from '@umbraco-cms/backoffice/hint'; -export class UmbWorkspaceViewNavigationContext extends UmbContextBase { +export class UmbWorkspaceEditorNavigationContext extends UmbContextBase { // #init: Promise; /** @@ -16,13 +17,12 @@ export class UmbWorkspaceViewNavigationContext extends UmbContextBase { #views = new UmbBasicState(>[]); public readonly views = this.#views.asObservable(); - /** HERE THIS IS THE PLACE TO CONTINUE!!!!!!! */ - - // TODO: We still need to sync these up to the workspace context... - #hints = new UmbHintManager(this); + #hints = new UmbHintController(this, {}); constructor(host: UmbControllerHost) { - super(host, UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT); + super(host, UMB_WORKSPACE_EDITOR_NAVIGATION_CONTEXT); + + this.#hints.inherit(); this.#init = new UmbExtensionsManifestInitializer( this, @@ -47,7 +47,6 @@ export class UmbWorkspaceViewNavigationContext extends UmbContextBase { .filter((view) => !viewsToKeep.some((x) => x.manifest.alias === view.manifest.alias)) .forEach((view) => { const context = new UmbWorkspaceViewContext(this, view.manifest); - context.hints.bindWith(this.#hints); newViews.push(context); }); @@ -59,6 +58,10 @@ export class UmbWorkspaceViewNavigationContext extends UmbContextBase { ).asPromise(); } + setVariantId(variantId: UmbVariantId | undefined): void { + this.#hints.updateScaffold({ variantId }); + } + async getViewContext(alias: string): Promise { await this.#init; return this.#views.getValue().find((view) => view.manifest.alias === alias); 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 b9f7195d6944..1e930c5e17ee 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 @@ -1,6 +1,6 @@ import { UMB_WORKSPACE_VIEW_PATH_PATTERN } from '../../paths.js'; import type { UmbWorkspaceHint } from '../../controllers/workspace-view-hint-manager.controller.js'; -import { UmbWorkspaceViewNavigationContext } from './workspace-view-navigation.context.js'; +import { UmbWorkspaceEditorNavigationContext } from './workspace-editor-navigation.context.js'; import type { UmbWorkspaceViewContext } from './workspace-view.context.js'; import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api'; @@ -36,7 +36,7 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { So the question goes how can we sync this upward and downward for just specific variant ids. Or sync all, but only show by a filter.. I think all hints should support the concept of variant id, but it should be optional if we like to filter by it. */ - #navigationContext = new UmbWorkspaceViewNavigationContext(this); + #navigationContext = new UmbWorkspaceEditorNavigationContext(this); #workspaceViewHintObservers: Array = []; @property() @@ -60,6 +60,7 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { } public set variantId(value: UmbVariantId | undefined) { this._variantId = value; + this.#navigationContext.setVariantId(value); this.#observeWorkspaceViewHints(); } private _variantId?: UmbVariantId | undefined; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context-token.ts deleted file mode 100644 index d30580ef9884..000000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view-navigation.context-token.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { UmbWorkspaceViewNavigationContext } from './workspace-view-navigation.context.js'; -import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; - -export const UMB_WORKSPACE_VIEW_NAVIGATION_CONTEXT = new UmbContextToken( - 'UmbWorkspaceViewNavigationContext', -); 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 3973e936b3f3..4251103d664d 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,29 +1,51 @@ import type { ManifestWorkspaceView } from '../../types.js'; -import type { UmbWorkspaceHint } from '../../controllers/workspace-view-hint-manager.controller.js'; import { UMB_WORKSPACE_VIEW_CONTEXT } from './workspace-view.context-token.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbControllerBase, type UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; import type { Observable } from '@umbraco-cms/backoffice/observable-api'; import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; -import { UmbHintManager } from '@umbraco-cms/backoffice/utils'; +import { UmbHintController, type UmbHint } from '@umbraco-cms/backoffice/hint'; -export interface UmbWorkspaceViewHint extends UmbWorkspaceHint { - viewAlias: string; +export interface UmbVariantHint extends UmbHint { + variantId?: UmbVariantId; } +/** + * + * NOTES + * TODO: + * The Workspace View Context is also a View Context... + * + * But shortcut should properly be it's own Context. + * Consider leaving out Browser Title for now? + * Could Hints be its own context? And can the navgiational element know about hints that the specific manager isnt available for — no need to spin up workspace view contexts virtually... + * + * + * TODO: Enable changing Icon and Weight at runtime.. + * + */ export class UmbWorkspaceViewContext extends UmbControllerBase { // #providerCtrl: any; #currentProvideHost?: UmbClassInterface; + #variantId?: UmbVariantId; + public manifest: ManifestWorkspaceView; public hints; - constructor(host: UmbControllerHost, manifest: ManifestWorkspaceView) { + constructor(host: UmbControllerHost, manifest: ManifestWorkspaceView, variantId?: UmbVariantId) { super(host); this.manifest = manifest; - this.hints = new UmbHintManager(this, { scaffold: { viewAlias: manifest.alias } }); + this.#variantId = variantId; + this.hints = new UmbHintController(this, { + viewAlias: manifest.alias, + scaffold: { + variantId: variantId, + }, + }); + this.hints.inherit(); } provideAt(controllerHost: UmbClassInterface): void { @@ -42,18 +64,13 @@ export class UmbWorkspaceViewContext extends UmbControllerBase { } } - /** - * observe hint of an optional variant, the variant is optional, in that case all variantIds are accepted. - * @param {UmbVariantId} variantId - the variantId to match against the hint. - * @returns {Observable} the first hint that matches the variantId or undefined if no hint is found. - */ - hintOfVariant(variantId?: UmbVariantId): Observable { - const viewAlias = this.manifest.alias; - if (variantId) { + firstHintOfVariant(): Observable { + if (this.#variantId) { return this.hints.asObservablePart((x) => - x.find((hint) => hint.viewAlias === viewAlias && (hint.variantId ? hint.variantId.compare(variantId) : true)), + x.find((hint) => (hint.variantId ? hint.variantId.equal(this.#variantId!) : true)), ); + } else { + return this.hints.firstHint; } - return this.hints.asObservablePart((x) => x.find((hint) => hint.viewAlias === viewAlias)); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-view-hint-manager.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-view-hint-manager.controller.ts deleted file mode 100644 index 66536f7faedf..000000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-view-hint-manager.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { UmbVariantId } from '../../variant/variant-id.class.js'; -import { UmbHintManager, type UmbHint } from '@umbraco-cms/backoffice/utils'; - -export interface UmbWorkspaceHint extends UmbHint { - variantId?: UmbVariantId; -} - -/** - * @class UmbWorkspaceViewHintManager - * @description - Class managing the hints of views in a workspace. - */ -export class UmbWorkspaceViewHintManager extends UmbHintManager {} From 42f6863f88e93f467e6ac93ccd5870d5fc14fe56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 14 Aug 2025 14:23:12 +0200 Subject: [PATCH 22/65] final corrections for working POC --- .../core/hint/context/hint.controller.ts | 26 ++++++++++++------- .../workspace-editor-navigation.context.ts | 1 + .../workspace-editor.element.ts | 12 ++++++--- .../workspace-view.context.ts | 1 - .../core/workspace/controllers/index.ts | 1 - 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts index 969955f255ea..1b68bd9469fd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts @@ -26,6 +26,9 @@ export class UmbHintController< > extends UmbControllerBase { // #viewAlias?: string; + getViewAlias(): string | undefined { + return this.#viewAlias; + } #scaffold: Partial; #inUnprovidingState?: boolean; @@ -111,8 +114,8 @@ export class UmbHintController< this.initiateChange(); if (this.#parentHints) { // Remove the local messages that does not exist in the parent anymore: - const toRemove = this.#parentHints.filter((msg) => !hints.find((m) => m.unique === msg.unique)); - this.remove(toRemove.map((msg) => msg.unique)); + const toRemove = this.#parentHints.filter((hint) => !hints.find((m) => m.unique === hint.unique)); + this.remove(toRemove.map((hint) => hint.unique)); } this.#parentHints = hints; @@ -140,14 +143,15 @@ export class UmbHintController< this.#parent!.initiateChange(); + console.log('transferHints'); + + const parentViewAlias = this.#parent.getViewAlias(); + hints.forEach((hint) => { - // replace this.#baseDataPath (if it starts with it) with $ in the path, so it becomes relative to the parent context - const path = hint.path; - if (path === undefined) { - throw new Error('Path was not transformed correctly and can therefor not be synced with parent messages.'); - } + console.log(hint); + const newPath = parentViewAlias ? [parentViewAlias, ...hint.path] : hint.path; // Notice addOne appends the parent viewAlias to the path: - this.#parent!.addOne(hint); + this.#parent!.addOne({ ...hint, path: newPath }); }); this.#parent!.finishChange(); @@ -175,7 +179,11 @@ export class UmbHintController< newHint.path = [this.#viewAlias, ...newHint.path]; } } else { - newHint.path = []; + if (this.#viewAlias) { + newHint.path = [this.#viewAlias]; + } else { + newHint.path = []; + } } this.#hints.appendOne(newHint); return hint.unique!; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts index 8d82ed96146d..e4c0b89c3374 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts @@ -47,6 +47,7 @@ export class UmbWorkspaceEditorNavigationContext extends UmbContextBase { .filter((view) => !viewsToKeep.some((x) => x.manifest.alias === view.manifest.alias)) .forEach((view) => { const context = new UmbWorkspaceViewContext(this, view.manifest); + context.hints.inheritFrom(this.#hints); newViews.push(context); }); 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 1e930c5e17ee..ccef8bc84264 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 @@ -1,7 +1,6 @@ import { UMB_WORKSPACE_VIEW_PATH_PATTERN } from '../../paths.js'; -import type { UmbWorkspaceHint } from '../../controllers/workspace-view-hint-manager.controller.js'; import { UmbWorkspaceEditorNavigationContext } from './workspace-editor-navigation.context.js'; -import type { UmbWorkspaceViewContext } from './workspace-view.context.js'; +import type { UmbVariantHint, UmbWorkspaceViewContext } from './workspace-view.context.js'; import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -59,6 +58,9 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { return this._variantId; } public set variantId(value: UmbVariantId | undefined) { + if (value && this._variantId?.equal(value)) { + return; + } this._variantId = value; this.#navigationContext.setVariantId(value); this.#observeWorkspaceViewHints(); @@ -69,7 +71,7 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { private _workspaceViews: Array = []; @state() - private _hintMap: Map = new Map(); + private _hintMap: Map = new Map(); @state() private _routes?: UmbRoute[]; @@ -98,13 +100,15 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { this._hintMap = new Map(); this.#workspaceViewHintObservers = this._workspaceViews.map((view, index) => this.observe( - view.hintOfVariant(this._variantId), + view.firstHintOfVariant(), (hint) => { + console.log('hint found', view.manifest.alias, hint); if (hint) { this._hintMap.set(view.manifest.alias, hint); } else { this._hintMap.delete(view.manifest.alias); } + console.log('hint map', this._hintMap); this.requestUpdate('_hintMap'); }, 'umbObserveState_' + index, 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 4251103d664d..865369e0c0aa 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 @@ -45,7 +45,6 @@ export class UmbWorkspaceViewContext extends UmbControllerBase { variantId: variantId, }, }); - this.hints.inherit(); } provideAt(controllerHost: UmbClassInterface): void { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/index.ts index f29a5d018290..3efe05161974 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/index.ts @@ -1,4 +1,3 @@ export * from './workspace-is-new-redirect.controller.js'; export * from './workspace-route-manager.controller.js'; export * from './workspace-split-view-manager.controller.js'; -export * from './workspace-view-hint-manager.controller.js'; From 0408fa05f1ed5ff9c1d6bc8284e8b965984fc7fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 14 Aug 2025 14:35:35 +0200 Subject: [PATCH 23/65] clean up path logic --- .../packages/core/hint/context/hint.controller.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts index 1b68bd9469fd..347acdd15bcc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts @@ -150,7 +150,6 @@ export class UmbHintController< hints.forEach((hint) => { console.log(hint); const newPath = parentViewAlias ? [parentViewAlias, ...hint.path] : hint.path; - // Notice addOne appends the parent viewAlias to the path: this.#parent!.addOne({ ...hint, path: newPath }); }); @@ -174,17 +173,7 @@ export class UmbHintController< newHint.unique ??= Symbol(); newHint.weight ??= 0; newHint.text ??= '!'; - if (newHint.path) { - if (this.#viewAlias && newHint.path[0] !== this.#viewAlias) { - newHint.path = [this.#viewAlias, ...newHint.path]; - } - } else { - if (this.#viewAlias) { - newHint.path = [this.#viewAlias]; - } else { - newHint.path = []; - } - } + newHint.path ??= []; this.#hints.appendOne(newHint); return hint.unique!; } From 536069aa656133a955d3feb694ac354547f90848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 15 Aug 2025 06:10:49 +0200 Subject: [PATCH 24/65] implement scaffold --- .../core/hint/context/hint.controller.ts | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts index 347acdd15bcc..7afbcec80676 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts @@ -1,10 +1,9 @@ import type { UmbPartialSome } from '../../utils/type/index.js'; -import { UmbContextBase, UmbControllerBase, type UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; +import { UmbControllerBase, type UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UUIInterfaceColor } from '@umbraco-cms/backoffice/external/uui'; -import { UmbArrayState, type Observable } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState, UmbObjectState, type Observable } from '@umbraco-cms/backoffice/observable-api'; import { UMB_HINT_CONTEXT } from './hint.context-token.js'; -import { ReplaceStartOfPath } from '../../validation/utils/replace-start-of-path.function.js'; import type { UmbContextProviderController } from '@umbraco-cms/backoffice/context-api'; export interface UmbIncomingHintBase { @@ -29,7 +28,8 @@ export class UmbHintController< getViewAlias(): string | undefined { return this.#viewAlias; } - #scaffold: Partial; + #scaffold = new UmbObjectState>({}); + readonly scaffold = this.#scaffold.asObservable(); #inUnprovidingState?: boolean; #parent?: UmbHintController; @@ -42,14 +42,19 @@ export class UmbHintController< //public readonly hasHints = this._hints.asObservablePart((x) => x.length > 0); updateScaffold(updates: Partial) { - this.#scaffold = { ...this.#scaffold, ...updates }; + this.#scaffold.update(updates); + } + getScaffold(): Partial { + return this.#scaffold.getValue(); } constructor(host: UmbControllerHost, args?: { viewAlias?: string; scaffold?: Partial }) { super(host); this.#viewAlias = args?.viewAlias; - this.#scaffold = args?.scaffold ?? {}; + if (args?.scaffold) { + this.#scaffold.setValue(args?.scaffold); + } this.#hints.sortBy((a, b) => (b.weight || 0) - (a.weight || 0)); } @@ -104,6 +109,11 @@ export class UmbHintController< inheritFrom(parent: UmbHintController | undefined): void { this.#parent = parent; + this.observe(this.#parent?.scaffold, (scaffold) => { + if (scaffold) { + this.#scaffold.update(scaffold as any); + } + }); this.observe( parent?.descendingHints(this.#viewAlias), (hints) => { @@ -143,13 +153,14 @@ export class UmbHintController< this.#parent!.initiateChange(); - console.log('transferHints'); - const parentViewAlias = this.#parent.getViewAlias(); hints.forEach((hint) => { - console.log(hint); - const newPath = parentViewAlias ? [parentViewAlias, ...hint.path] : hint.path; + let newPath = hint.path; + // If the hint path does not already contain the parent view alias as the first entry, we add it. (This will usually happen, but some Hint Contexts does not have a view alias as they) + if (parentViewAlias && newPath[0] !== parentViewAlias) { + newPath = [parentViewAlias, ...hint.path]; + } this.#parent!.addOne({ ...hint, path: newPath }); }); @@ -169,7 +180,11 @@ export class UmbHintController< * @returns {HintType['unique']} Unique value of the hint */ addOne(hint: IncomingHintType): string | symbol { - const newHint = { ...this.#scaffold, ...hint } as unknown as HintType; + /** + * TODO: + * Works, but the Hint does not stay when navigating away from a variant and back... + */ + const newHint = { ...this.#scaffold.getValue(), ...hint } as unknown as HintType; newHint.unique ??= Symbol(); newHint.weight ??= 0; newHint.text ??= '!'; From bebc5ec0ba2a3bc8ed7f8d2db5efeb9b2c9a4c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 15 Aug 2025 07:02:24 +0200 Subject: [PATCH 25/65] propagation and inheritance from view to workspace --- .../hint-workspace-view.ts | 4 ---- .../content-detail-workspace-base.ts | 4 ++-- .../core/hint/context/hint.context-token.ts | 2 +- .../core/hint/context/hints.context.ts | 10 +++++++++ ...hint.controller.ts => hints.controller.ts} | 21 ++++++++----------- .../src/packages/core/hint/context/index.ts | 3 ++- .../workspace-editor-navigation.context.ts | 7 ++++++- .../workspace-editor.element.ts | 4 +--- .../workspace-view.context.ts | 2 ++ 9 files changed, 33 insertions(+), 24 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.context.ts rename src/Umbraco.Web.UI.Client/src/packages/core/hint/context/{hint.controller.ts => hints.controller.ts} (93%) 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 6932749ba286..103b17506c9a 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 @@ -21,10 +21,6 @@ export class ExampleHintWorkspaceView extends UmbElementMixin(LitElement) { throw new Error('Could not find the view'); } - this.observe(view.hints.hints, (hints) => { - console.log('Local Hints:', hints, this); - }); - if (view.hints.has('exampleHintFromToggleAction')) { view.hints.removeOne('exampleHintFromToggleAction'); } else { diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts index a65be13ae5c2..435585b43d26 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts @@ -54,7 +54,7 @@ import { type UmbPropertyTypePresetModelTypeModel, } from '@umbraco-cms/backoffice/property'; import { UmbSegmentCollectionRepository, type UmbSegmentCollectionItemModel } from '@umbraco-cms/backoffice/segment'; -import { UmbHintController } from '@umbraco-cms/backoffice/hint'; +import { UmbHintContext } from '@umbraco-cms/backoffice/hint'; export interface UmbContentDetailWorkspaceContextArgs< DetailModelType extends UmbContentDetailModel, @@ -140,7 +140,7 @@ export abstract class UmbContentDetailWorkspaceContextBase< readonly splitView = new UmbWorkspaceSplitViewManager(); /* Hints */ - readonly hints = new UmbHintController(this); + readonly hints = new UmbHintContext(this); /* Variant Options */ // TODO: Optimize this so it uses either a App Language Context? [NL] diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.context-token.ts index d95030626cff..df7bbd2a2eaa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.context-token.ts @@ -1,4 +1,4 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbHintController } from './hint.controller.js'; +import type { UmbHintController } from './hints.controller.js'; export const UMB_HINT_CONTEXT = new UmbContextToken('UmbHintContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.context.ts new file mode 100644 index 000000000000..0da0c3dfbff1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.context.ts @@ -0,0 +1,10 @@ +import { UMB_HINT_CONTEXT } from './hint.context-token.js'; +import { UmbHintController } from './hints.controller.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbHintContext extends UmbHintController { + constructor(host: UmbControllerHost) { + super(host); + this.provideContext(UMB_HINT_CONTEXT, this); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts similarity index 93% rename from src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts index 7afbcec80676..0708c7406225 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts @@ -118,8 +118,8 @@ export class UmbHintController< parent?.descendingHints(this.#viewAlias), (hints) => { if (!hints) { - this.#hints.clear(); - return; + console.warn('TODO: Does this case happen, maybe its only in destruction?'); + hints = []; } this.initiateChange(); if (this.#parentHints) { @@ -130,14 +130,11 @@ export class UmbHintController< this.#parentHints = hints; hints.forEach((hint) => { - // Remove first entry of hint.path: - const path = hint.path.slice(1); - if (path === undefined) { - throw new Error( - 'Path was not transformed correctly and can therefor not be transfered to the local validation context messages.', - ); + // Remove first entry of hint.path, if it matches viewAlias. + if (this.#viewAlias && hint.path[0] === this.#viewAlias) { + hint = { ...hint, path: hint.path.slice(1) }; } - this.#hints.appendOne({ ...hint, path } as HintType); + this.#hints.appendOne(hint as HintType); }); this.finishChange(); @@ -153,13 +150,13 @@ export class UmbHintController< this.#parent!.initiateChange(); - const parentViewAlias = this.#parent.getViewAlias(); + const viewAlias = this.getViewAlias(); hints.forEach((hint) => { let newPath = hint.path; // If the hint path does not already contain the parent view alias as the first entry, we add it. (This will usually happen, but some Hint Contexts does not have a view alias as they) - if (parentViewAlias && newPath[0] !== parentViewAlias) { - newPath = [parentViewAlias, ...hint.path]; + if (viewAlias && newPath[0] !== viewAlias) { + newPath = [viewAlias, ...hint.path]; } this.#parent!.addOne({ ...hint, path: newPath }); }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/index.ts index 1568e218f3fe..09ac45710dff 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/index.ts @@ -1 +1,2 @@ -export * from './hint.controller.js'; +export * from './hints.controller.js'; +export * from './hints.context.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts index e4c0b89c3374..82a98ffea679 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts @@ -17,6 +17,7 @@ export class UmbWorkspaceEditorNavigationContext extends UmbContextBase { #views = new UmbBasicState(>[]); public readonly views = this.#views.asObservable(); + #variantId?: UmbVariantId; #hints = new UmbHintController(this, {}); constructor(host: UmbControllerHost) { @@ -46,7 +47,7 @@ export class UmbWorkspaceEditorNavigationContext extends UmbContextBase { workspaceViews .filter((view) => !viewsToKeep.some((x) => x.manifest.alias === view.manifest.alias)) .forEach((view) => { - const context = new UmbWorkspaceViewContext(this, view.manifest); + const context = new UmbWorkspaceViewContext(this, view.manifest, this.#variantId); context.hints.inheritFrom(this.#hints); newViews.push(context); }); @@ -60,7 +61,11 @@ export class UmbWorkspaceEditorNavigationContext extends UmbContextBase { } setVariantId(variantId: UmbVariantId | undefined): void { + this.#variantId = variantId; this.#hints.updateScaffold({ variantId }); + this.#views.getValue().forEach((view) => { + view.hints.updateScaffold({ variantId }); + }); } async getViewContext(alias: string): Promise { 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 ccef8bc84264..0082351b4992 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 @@ -102,13 +102,11 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { this.observe( view.firstHintOfVariant(), (hint) => { - console.log('hint found', view.manifest.alias, hint); if (hint) { this._hintMap.set(view.manifest.alias, hint); } else { this._hintMap.delete(view.manifest.alias); } - console.log('hint map', this._hintMap); this.requestUpdate('_hintMap'); }, 'umbObserveState_' + index, @@ -127,8 +125,8 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { component: () => createExtensionElement(manifest), setup: (component: any) => { if (component) { - component.manifest = manifest; 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 865369e0c0aa..27dcacfd5cfe 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 @@ -54,6 +54,7 @@ export class UmbWorkspaceViewContext extends UmbControllerBase { this.#currentProvideHost = controllerHost; this.#providerCtrl = controllerHost.provideContext(UMB_WORKSPACE_VIEW_CONTEXT, this); + this.hints.provideAt(controllerHost); } unprovide(): void { @@ -61,6 +62,7 @@ export class UmbWorkspaceViewContext extends UmbControllerBase { this.#providerCtrl.destroy(); this.#providerCtrl = undefined; } + this.hints.unprovide(); } firstHintOfVariant(): Observable { From 9b6a242f5d99d5c7582d62c0cc3f8c0929f5f491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 15 Aug 2025 07:39:11 +0200 Subject: [PATCH 26/65] separate types from classes --- .../views/edit/content-editor.element.ts | 4 ++++ .../core/hint/context/hints.controller.ts | 15 +-------------- .../src/packages/core/hint/index.ts | 1 + .../src/packages/core/hint/types.ts | 19 +++++++++++++++++++ .../workspace-view.context.ts | 6 +----- 5 files changed, 26 insertions(+), 19 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/hint/types.ts 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 bac7610e6498..b567b731c5ad 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 @@ -15,6 +15,7 @@ import { encodeFolderName } from '@umbraco-cms/backoffice/router'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace'; import './content-editor-tab.element.js'; +import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint'; @customElement('umb-content-workspace-view-edit') export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements UmbWorkspaceViewElement { @@ -39,6 +40,9 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements @state() private _activePath = ''; + @state() + private _hintMap: Map = new Map(); + #structureManager?: UmbContentTypeStructureManager; private _tabsStructureHelper = new UmbContentTypeContainerStructureHelper(this); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts index 0708c7406225..f475c7c2b6b5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts @@ -1,23 +1,10 @@ import type { UmbPartialSome } from '../../utils/type/index.js'; import { UmbControllerBase, type UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { UUIInterfaceColor } from '@umbraco-cms/backoffice/external/uui'; import { UmbArrayState, UmbObjectState, type Observable } from '@umbraco-cms/backoffice/observable-api'; import { UMB_HINT_CONTEXT } from './hint.context-token.js'; import type { UmbContextProviderController } from '@umbraco-cms/backoffice/context-api'; - -export interface UmbIncomingHintBase { - unique?: string | symbol; - text: string; - weight?: number; - color?: UUIInterfaceColor; -} - -export interface UmbHint extends UmbIncomingHintBase { - unique: string | symbol; - path: Array; - weight: number; -} +import type { UmbHint, UmbIncomingHintBase } from '../types.js'; export class UmbHintController< HintType extends UmbHint = UmbHint, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/index.ts index 00c55032bc33..66e7bbbc857f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/index.ts @@ -1 +1,2 @@ export * from './context/index.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/types.ts new file mode 100644 index 000000000000..6eef3a7529e2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/types.ts @@ -0,0 +1,19 @@ +import type { UUIInterfaceColor } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; + +export interface UmbIncomingHintBase { + unique?: string | symbol; + text: string; + weight?: number; + color?: UUIInterfaceColor; +} + +export interface UmbHint extends UmbIncomingHintBase { + unique: string | symbol; + path: Array; + weight: number; +} + +export interface UmbVariantHint extends UmbHint { + variantId?: UmbVariantId; +} 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 27dcacfd5cfe..66c08923a9ad 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 @@ -4,11 +4,7 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbControllerBase, type UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; import type { Observable } from '@umbraco-cms/backoffice/observable-api'; import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; -import { UmbHintController, type UmbHint } from '@umbraco-cms/backoffice/hint'; - -export interface UmbVariantHint extends UmbHint { - variantId?: UmbVariantId; -} +import { UmbHintController, type UmbVariantHint } from '@umbraco-cms/backoffice/hint'; /** * From 843b889a6708461d550ec6b1a50f37ed795aea86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 15 Aug 2025 08:16:40 +0200 Subject: [PATCH 27/65] refactor to view context --- src/Umbraco.Web.UI.Client/package.json | 1 + .../content-workspace.context-token.ts | 3 +- .../views/edit/content-editor.element.ts | 14 +++- .../src/packages/core/view/context/index.ts | 2 + .../core/view/context/view.context-token.ts | 4 ++ .../core/view/context/view.context.ts | 68 +++++++++++++++++++ .../src/packages/core/view/index.ts | 1 + .../workspace-editor-navigation.context.ts | 4 +- .../workspace-editor.element.ts | 3 +- .../workspace-view.context-token.ts | 7 +- .../workspace-view.context.ts | 67 ++---------------- src/Umbraco.Web.UI.Client/tsconfig.json | 1 + 12 files changed, 104 insertions(+), 71 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/view/context/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/view/context/view.context-token.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/view/context/view.context.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/view/index.ts diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index e1de28bec945..1d64e64d11f0 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -117,6 +117,7 @@ "./utils": "./dist-cms/packages/core/utils/index.js", "./validation": "./dist-cms/packages/core/validation/index.js", "./variant": "./dist-cms/packages/core/variant/index.js", + "./view": "./dist-cms/packages/core/view/index.js", "./webhook": "./dist-cms/packages/webhook/index.js", "./workspace": "./dist-cms/packages/core/workspace/index.js", "./external/backend-api": "./dist-cms/packages/core/backend-api/index.js", diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-workspace.context-token.ts index cf2c295263ee..cd80d5326604 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-workspace.context-token.ts @@ -7,5 +7,6 @@ export const UMB_CONTENT_WORKSPACE_CONTEXT = new UmbContextToken< >( 'UmbWorkspaceContext', undefined, - (context): context is UmbContentWorkspaceContext => (context as any).IS_CONTENT_WORKSPACE_CONTEXT, + (context): context is UmbContentWorkspaceContext => + (context as UmbContentWorkspaceContext).IS_CONTENT_WORKSPACE_CONTEXT, ); 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 b567b731c5ad..f7e749991586 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 @@ -16,6 +16,8 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace'; import './content-editor-tab.element.js'; import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint'; +import { UMB_HINT_CONTEXT } from 'src/packages/core/hint/context/hint.context-token.js'; +import type { UmbViewContext } from '@umbraco-cms/backoffice/view'; @customElement('umb-content-workspace-view-edit') export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements UmbWorkspaceViewElement { @@ -43,6 +45,8 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements @state() private _hintMap: Map = new Map(); + #viewContexts: Array = []; + #structureManager?: UmbContentTypeStructureManager; private _tabsStructureHelper = new UmbContentTypeContainerStructureHelper(this); @@ -50,13 +54,17 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements constructor() { super(); + this.consumeContext(UMB_HINT_CONTEXT, (context) => { + // Parse on as inhiretFrom for all the view contexts. + }); + this._tabsStructureHelper.setIsRoot(true); this._tabsStructureHelper.setContainerChildType('Tab'); this.observe( this._tabsStructureHelper.mergedContainers, (tabs) => { this._tabs = tabs; - this._createRoutes(); + this.#createRoutes(); }, null, ); @@ -77,13 +85,13 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements await this.#structureManager.hasRootContainers('Group'), (hasRootGroups) => { this._hasRootGroups = hasRootGroups; - this._createRoutes(); + this.#createRoutes(); }, '_observeGroups', ); } - private _createRoutes() { + #createRoutes() { if (!this._tabs || !this.#structureManager) return; const routes: UmbRoute[] = []; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/view/context/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/view/context/index.ts new file mode 100644 index 000000000000..3fff04337fea --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/view/context/index.ts @@ -0,0 +1,2 @@ +export * from './view.context.js'; +export * from './view.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/view/context/view.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/view/context/view.context-token.ts new file mode 100644 index 000000000000..efbb4b8c53f3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/view/context/view.context-token.ts @@ -0,0 +1,4 @@ +import type { UmbViewContext } from './view.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_VIEW_CONTEXT = new UmbContextToken('UmbViewContext'); 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 new file mode 100644 index 000000000000..20a30053141a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/view/context/view.context.ts @@ -0,0 +1,68 @@ +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbControllerBase, type UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; +import type { Observable } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbHintController, type UmbVariantHint } from '@umbraco-cms/backoffice/hint'; +import { UMB_VIEW_CONTEXT } from './view.context-token'; + +/** + * + * TODO: + * Include Shortcuts + * + * Browser Title? + * + */ +export class UmbViewContext extends UmbControllerBase { + // + #providerCtrl: any; + #currentProvideHost?: UmbClassInterface; + + #variantId?: UmbVariantId; + + public hints; + + constructor(host: UmbControllerHost, viewAlias: string, variantId?: UmbVariantId) { + super(host); + this.#variantId = variantId; + this.hints = new UmbHintController(this, { + viewAlias: viewAlias, + scaffold: { + 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 { + // TODO: Do you want to inherit the variantId as well? Then I think VariantId needs to become a state. + this.hints.inheritFrom(context.hints); + } + + firstHintOfVariant(): Observable { + if (this.#variantId) { + return this.hints.asObservablePart((x) => + x.find((hint) => (hint.variantId ? hint.variantId.equal(this.#variantId!) : true)), + ); + } else { + return this.hints.firstHint; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/view/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/view/index.ts new file mode 100644 index 000000000000..00c55032bc33 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/view/index.ts @@ -0,0 +1 @@ +export * from './context/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts index 82a98ffea679..5b63b065db1c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts @@ -1,4 +1,4 @@ -import { type UmbVariantHint, UmbWorkspaceViewContext } from './workspace-view.context.js'; +import { UmbWorkspaceViewContext } from './workspace-view.context.js'; import { UMB_WORKSPACE_EDITOR_NAVIGATION_CONTEXT } from './workspace-editor-navigation.context-token.js'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; @@ -6,7 +6,7 @@ import { UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extens import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbBasicState } from '@umbraco-cms/backoffice/observable-api'; import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; -import { UmbHintController } from '@umbraco-cms/backoffice/hint'; +import { UmbHintController, type UmbVariantHint } from '@umbraco-cms/backoffice/hint'; export class UmbWorkspaceEditorNavigationContext extends UmbContextBase { // 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 0082351b4992..2737e4002e64 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 @@ -1,6 +1,6 @@ import { UMB_WORKSPACE_VIEW_PATH_PATTERN } from '../../paths.js'; import { UmbWorkspaceEditorNavigationContext } from './workspace-editor-navigation.context.js'; -import type { UmbVariantHint, UmbWorkspaceViewContext } from './workspace-view.context.js'; +import type { UmbWorkspaceViewContext } from './workspace-view.context.js'; import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -8,6 +8,7 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbRoute, UmbRouterSlotInitEvent, UmbRouterSlotChangeEvent } from '@umbraco-cms/backoffice/router'; import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint'; /** * @element umb-workspace-editor diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context-token.ts index 284bb8312177..1013b1fdb85d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context-token.ts @@ -1,4 +1,9 @@ +import type { UmbViewContext } from '@umbraco-cms/backoffice/view'; import type { UmbWorkspaceViewContext } from './workspace-view.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -export const UMB_WORKSPACE_VIEW_CONTEXT = new UmbContextToken('UmbWorkspaceViewContext'); +export const UMB_WORKSPACE_VIEW_CONTEXT = new UmbContextToken( + 'UmbViewContext', + undefined, + (context): context is UmbWorkspaceViewContext => (context as UmbWorkspaceViewContext).IS_WORKSPACE_VIEW_CONTEXT, +); 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 66c08923a9ad..1322e809a72d 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,73 +1,14 @@ import type { ManifestWorkspaceView } from '../../types.js'; -import { UMB_WORKSPACE_VIEW_CONTEXT } from './workspace-view.context-token.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbControllerBase, type UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; -import type { Observable } from '@umbraco-cms/backoffice/observable-api'; import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; -import { UmbHintController, type UmbVariantHint } from '@umbraco-cms/backoffice/hint'; - -/** - * - * NOTES - * TODO: - * The Workspace View Context is also a View Context... - * - * But shortcut should properly be it's own Context. - * Consider leaving out Browser Title for now? - * Could Hints be its own context? And can the navgiational element know about hints that the specific manager isnt available for — no need to spin up workspace view contexts virtually... - * - * - * TODO: Enable changing Icon and Weight at runtime.. - * - */ -export class UmbWorkspaceViewContext extends UmbControllerBase { - // - #providerCtrl: any; - #currentProvideHost?: UmbClassInterface; - - #variantId?: UmbVariantId; +import { UmbViewContext } from '@umbraco-cms/backoffice/view'; +export class UmbWorkspaceViewContext extends UmbViewContext { + public readonly IS_WORKSPACE_VIEW_CONTEXT = true as const; public manifest: ManifestWorkspaceView; - public hints; - constructor(host: UmbControllerHost, manifest: ManifestWorkspaceView, variantId?: UmbVariantId) { - super(host); + super(host, manifest.alias, variantId); this.manifest = manifest; - this.#variantId = variantId; - this.hints = new UmbHintController(this, { - viewAlias: manifest.alias, - scaffold: { - variantId: variantId, - }, - }); - } - - provideAt(controllerHost: UmbClassInterface): void { - if (this.#currentProvideHost === controllerHost) return; - - this.unprovide(); - - this.#currentProvideHost = controllerHost; - this.#providerCtrl = controllerHost.provideContext(UMB_WORKSPACE_VIEW_CONTEXT, this); - this.hints.provideAt(controllerHost); - } - - unprovide(): void { - if (this.#providerCtrl) { - this.#providerCtrl.destroy(); - this.#providerCtrl = undefined; - } - this.hints.unprovide(); - } - - firstHintOfVariant(): Observable { - if (this.#variantId) { - return this.hints.asObservablePart((x) => - x.find((hint) => (hint.variantId ? hint.variantId.equal(this.#variantId!) : true)), - ); - } else { - return this.hints.firstHint; - } } } diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index 577c436413e7..39d6826191c2 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -146,6 +146,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js "@umbraco-cms/backoffice/utils": ["./src/packages/core/utils/index.ts"], "@umbraco-cms/backoffice/validation": ["./src/packages/core/validation/index.ts"], "@umbraco-cms/backoffice/variant": ["./src/packages/core/variant/index.ts"], + "@umbraco-cms/backoffice/view": ["./src/packages/core/view/index.ts"], "@umbraco-cms/backoffice/webhook": ["./src/packages/webhook/index.ts"], "@umbraco-cms/backoffice/workspace": ["./src/packages/core/workspace/index.ts"], "@umbraco-cms/backoffice/external/backend-api": ["./src/packages/core/backend-api/index.ts"], From 26cd5199fb9d05da8dd3876e6d0f3e84e8f3f9ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 15 Aug 2025 08:24:05 +0200 Subject: [PATCH 28/65] rename editor navigation context to editor context --- .../core/workspace/components/workspace-editor/index.ts | 4 ++-- .../workspace-editor-navigation.context-token.ts | 6 ------ .../workspace-editor/workspace-editor.context-token.ts | 4 ++++ ...or-navigation.context.ts => workspace-editor.context.ts} | 6 +++--- .../components/workspace-editor/workspace-editor.element.ts | 4 ++-- 5 files changed, 11 insertions(+), 13 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context-token.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.context-token.ts rename src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/{workspace-editor-navigation.context.ts => workspace-editor.context.ts} (91%) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/index.ts index 0f8feb20811a..8c72e0ff13cb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/index.ts @@ -1,4 +1,4 @@ export * from './workspace-editor.element.js'; -export * from './workspace-editor-navigation.context-token.js'; -export * from './workspace-editor-navigation.context.js'; +export * from './workspace-editor.context-token.js'; +export * from './workspace-editor.context.js'; export * from './workspace-view.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context-token.ts deleted file mode 100644 index eaa35b7f6c24..000000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context-token.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { UmbWorkspaceEditorNavigationContext } from './workspace-editor-navigation.context.js'; -import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; - -export const UMB_WORKSPACE_EDITOR_NAVIGATION_CONTEXT = new UmbContextToken( - 'UmbWorkspaceViewNavigationContext', -); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.context-token.ts new file mode 100644 index 000000000000..fb6638a93eea --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.context-token.ts @@ -0,0 +1,4 @@ +import type { UmbWorkspaceEditorContext } from './workspace-editor.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_WORKSPACE_EDITOR_CONTEXT = new UmbContextToken('UmbWorkspaceViewContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.context.ts similarity index 91% rename from src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.context.ts index 5b63b065db1c..b6a3e46badd0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor-navigation.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.context.ts @@ -1,5 +1,5 @@ import { UmbWorkspaceViewContext } from './workspace-view.context.js'; -import { UMB_WORKSPACE_EDITOR_NAVIGATION_CONTEXT } from './workspace-editor-navigation.context-token.js'; +import { UMB_WORKSPACE_EDITOR_CONTEXT } from './workspace-editor.context-token.js'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api'; @@ -8,7 +8,7 @@ import { UmbBasicState } from '@umbraco-cms/backoffice/observable-api'; import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UmbHintController, type UmbVariantHint } from '@umbraco-cms/backoffice/hint'; -export class UmbWorkspaceEditorNavigationContext extends UmbContextBase { +export class UmbWorkspaceEditorContext extends UmbContextBase { // #init: Promise; /** @@ -21,7 +21,7 @@ export class UmbWorkspaceEditorNavigationContext extends UmbContextBase { #hints = new UmbHintController(this, {}); constructor(host: UmbControllerHost) { - super(host, UMB_WORKSPACE_EDITOR_NAVIGATION_CONTEXT); + super(host, UMB_WORKSPACE_EDITOR_CONTEXT); this.#hints.inherit(); 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 2737e4002e64..7c1a196b92ef 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 @@ -1,5 +1,5 @@ import { UMB_WORKSPACE_VIEW_PATH_PATTERN } from '../../paths.js'; -import { UmbWorkspaceEditorNavigationContext } from './workspace-editor-navigation.context.js'; +import { UmbWorkspaceEditorContext } from './workspace-editor.context.js'; import type { UmbWorkspaceViewContext } from './workspace-view.context.js'; import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api'; @@ -36,7 +36,7 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { So the question goes how can we sync this upward and downward for just specific variant ids. Or sync all, but only show by a filter.. I think all hints should support the concept of variant id, but it should be optional if we like to filter by it. */ - #navigationContext = new UmbWorkspaceEditorNavigationContext(this); + #navigationContext = new UmbWorkspaceEditorContext(this); #workspaceViewHintObservers: Array = []; @property() From 4b16be5e5dd95d9afbb2c255a8274a8ab37ca1cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 15 Aug 2025 08:26:32 +0200 Subject: [PATCH 29/65] propagate removals --- .../src/packages/core/hint/context/hints.controller.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts index f475c7c2b6b5..b15d7ab77dc8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts @@ -148,6 +148,12 @@ export class UmbHintController< this.#parent!.addOne({ ...hint, path: newPath }); }); + // Remove hints that are not in the local hints anymore: + const toRemove = this.#parentHints?.filter((hint) => !hints.find((m) => m.unique === hint.unique)); + if (toRemove) { + this.#parent!.remove(toRemove.map((hint) => hint.unique)); + } + this.#parent!.finishChange(); }; From 0758e5f99f66455dab7dfe757af9ce87f413d30c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 15 Aug 2025 08:30:02 +0200 Subject: [PATCH 30/65] clean up notes --- .../workspace-editor/workspace-editor.element.ts | 12 ------------ 1 file changed, 12 deletions(-) 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 7c1a196b92ef..6be6a8af8e08 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 @@ -24,18 +24,6 @@ import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint'; @customElement('umb-workspace-editor') export class UmbWorkspaceEditorElement extends UmbLitElement { // - // TODO: Concider making the NavigationContext host on Workspace level instead, but that would be breaking as well. - // TODO: Or make another broader context for the Workspace that can host hints across cultures. — Cause if you like an Extension Type Context that is for each culture, then such would be able to host hints for each culture. - // But then again Hints should be able to be replicated when opening a Document, meaning the responsible for them is a Workspace Context. This will then set hints, and in this case it should be able to append a culture or segment, or make it wider than such. - // Then these should been observe here in the workspace-editor(here), for the current culture and segment? and also for the invariant culture. (not invariant/default segment) - // This would mean that the Workspace would have a WorkspaceNavigationContext, and that would be able to host hints for each culture and segment. And then this one should not have a WorkspaceViewNavigationContext, the WorkspaceViewContext should also go away but a WorkspaceViewNavigationContext should exist and this should support begin provided at multiple elements cause we need to think about Split View. - // It then also means that this element should be able to get a Variant ID, I think via a property, and once that is set then we consume the WorkspaceViewNavigationContext and listens for hints for that variant. - /* - --- UPDATE 06/05/2025 --- - Well, because we use individually, we should make sure this works independently of the workspace context. - So the question goes how can we sync this upward and downward for just specific variant ids. Or sync all, but only show by a filter.. - I think all hints should support the concept of variant id, but it should be optional if we like to filter by it. - */ #navigationContext = new UmbWorkspaceEditorContext(this); #workspaceViewHintObservers: Array = []; From 613aae7061e76620660cacbd012966152b8c78b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 15 Aug 2025 11:38:52 +0200 Subject: [PATCH 31/65] Hints for Content Tabs --- .../hint-workspace-view.ts | 21 ++++++ .../views/edit/content-editor.element.ts | 75 +++++++++++++------ .../core/view/context/view.context.ts | 32 ++++---- .../workspace-editor.element.ts | 9 ++- 4 files changed, 97 insertions(+), 40 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 103b17506c9a..24a4d2ce0334 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 @@ -3,6 +3,7 @@ import { css, html, customElement, LitElement } from '@umbraco-cms/backoffice/ex 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') export class ExampleHintWorkspaceView extends UmbElementMixin(LitElement) { @@ -16,6 +17,8 @@ export class ExampleHintWorkspaceView extends UmbElementMixin(LitElement) { } const view = await context.getViewContext('example.workspaceView.hint'); */ + + /* const view = await this.getContext(UMB_WORKSPACE_VIEW_CONTEXT); if (!view) { throw new Error('Could not find the view'); @@ -31,6 +34,24 @@ export class ExampleHintWorkspaceView extends UmbElementMixin(LitElement) { weight: 100, }); } + */ + + const workspace = await this.getContext(UMB_DOCUMENT_WORKSPACE_CONTEXT); + if (!workspace) { + throw new Error('Could not find the workspace'); + } + + if (workspace.hints.has('exampleHintFromToggleAction')) { + workspace.hints.removeOne('exampleHintFromToggleAction'); + } else { + workspace.hints.addOne({ + unique: 'exampleHintFromToggleAction', + path: ['Umb.WorkspaceView.Document.Edit', 'root'], + text: 'Hi', + color: 'invalid', + weight: 100, + }); + } } override render() { 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 f7e749991586..5e0aaf9e6c4d 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,5 +1,5 @@ import type { UmbContentWorkspaceViewEditTabElement } from './content-editor-tab.element.js'; -import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbContentTypeModel, @@ -16,8 +16,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace'; import './content-editor-tab.element.js'; import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint'; -import { UMB_HINT_CONTEXT } from 'src/packages/core/hint/context/hint.context-token.js'; -import type { UmbViewContext } from '@umbraco-cms/backoffice/view'; +import { UMB_VIEW_CONTEXT, UmbViewContext } from '@umbraco-cms/backoffice/view'; @customElement('umb-content-workspace-view-edit') export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements UmbWorkspaceViewElement { @@ -26,6 +25,7 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements @state() private _hasRootProperties = false; */ + #viewContext?: UmbViewContext; @state() private _hasRootGroups = false; @@ -54,8 +54,9 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements constructor() { super(); - this.consumeContext(UMB_HINT_CONTEXT, (context) => { - // Parse on as inhiretFrom for all the view contexts. + this.consumeContext(UMB_VIEW_CONTEXT, (context) => { + // TODO: Parse on as inhiretFrom for all the view contexts. + this.#viewContext = context; }); this._tabsStructureHelper.setIsRoot(true); @@ -103,11 +104,13 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements (component as UmbContentWorkspaceViewEditTabElement).containerId = null; }, }); + this.#createViewContext('root'); } if (this._tabs.length > 0) { this._tabs?.forEach((tab) => { const tabName = tab.name ?? ''; + const path = `tab/${encodeFolderName(tabName)}`; routes.push({ path: `tab/${encodeFolderName(tabName)}`, component: () => import('./content-editor-tab.element.js'), @@ -115,6 +118,7 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements (component as UmbContentWorkspaceViewEditTabElement).containerId = tab.id; }, }); + this.#createViewContext(path); }); } @@ -134,35 +138,45 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements this._routes = routes; } + #createViewContext(viewAlias: string) { + if (!this.#viewContexts.find((context) => context.viewAlias === viewAlias)) { + const view = new UmbViewContext(this, viewAlias); + this.#viewContexts.push(view); + + view.inheritFrom(this.#viewContext); + + this.observe( + view.firstHintOfVariant, + (hint) => { + if (hint) { + this._hintMap.set(view.viewAlias, hint); + } else { + this._hintMap.delete(view.viewAlias); + } + this.requestUpdate('_hintMap'); + }, + 'umbObserveState_' + viewAlias, + ); + } + } + 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 - ? html` - - ` - : ''} + ${this._hasRootGroups && this._tabs.length > 0 ? this.#renderTab('root', '#general_generic') : nothing} ${repeat( this._tabs, (tab) => tab.name, (tab, index) => { - const path = this._routerPath + '/tab/' + encodeFolderName(tab.name || ''); - return html``; + const path = 'tab/' + encodeFolderName(tab.name || ''); + return this.#renderTab(path, tab.name, index); }, )} ` - : ''} + : nothing} ${hint && !active + ? html`${hint.text}` + : nothing}`; + } + static override styles = [ UmbTextStyles, css` 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 20a30053141a..9dc44bd4e4dc 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,6 +1,6 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbControllerBase, type UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; -import type { Observable } from '@umbraco-cms/backoffice/observable-api'; +import { UmbClassState, mergeObservables, type Observable } from '@umbraco-cms/backoffice/observable-api'; import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UmbHintController, type UmbVariantHint } from '@umbraco-cms/backoffice/hint'; import { UMB_VIEW_CONTEXT } from './view.context-token'; @@ -18,19 +18,31 @@ export class UmbViewContext extends UmbControllerBase { #providerCtrl: any; #currentProvideHost?: UmbClassInterface; - #variantId?: UmbVariantId; + public readonly viewAlias: string; + #variantId = new UmbClassState(undefined); + protected readonly _variantId = this.#variantId.asObservable(); public hints; + readonly firstHintOfVariant; + constructor(host: UmbControllerHost, viewAlias: string, variantId?: UmbVariantId) { super(host); - this.#variantId = variantId; + this.viewAlias = viewAlias; + this.#variantId.setValue(variantId); this.hints = new UmbHintController(this, { viewAlias: viewAlias, scaffold: { variantId: variantId, }, }); + this.firstHintOfVariant = mergeObservables([this._variantId, this.hints.hints], ([variantId, hints]) => { + if (variantId) { + return hints.find((hint) => (hint.variantId ? hint.variantId.equal(variantId!) : true)); + } else { + return hints[0]; + } + }); } provideAt(controllerHost: UmbClassInterface): void { @@ -51,18 +63,8 @@ export class UmbViewContext extends UmbControllerBase { this.hints.unprovide(); } - inheritFrom(context: UmbViewContext): void { + inheritFrom(context?: UmbViewContext): void { // TODO: Do you want to inherit the variantId as well? Then I think VariantId needs to become a state. - this.hints.inheritFrom(context.hints); - } - - firstHintOfVariant(): Observable { - if (this.#variantId) { - return this.hints.asObservablePart((x) => - x.find((hint) => (hint.variantId ? hint.variantId.equal(this.#variantId!) : true)), - ); - } else { - return this.hints.firstHint; - } + this.hints.inheritFrom(context?.hints); } } 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 6be6a8af8e08..7f3b3240b5bd 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 @@ -89,7 +89,7 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { this._hintMap = new Map(); this.#workspaceViewHintObservers = this._workspaceViews.map((view, index) => this.observe( - view.firstHintOfVariant(), + view.firstHintOfVariant, (hint) => { if (hint) { this._hintMap.set(view.manifest.alias, hint); @@ -170,16 +170,17 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { const manifest = view.manifest; const displayName = manifest.meta.label ? this.localize.string(manifest.meta.label) : manifest.name; const hint = this._hintMap.get(manifest.alias); + const active = + 'view/' + manifest.meta.pathname === this._activePath || (index === 0 && this._activePath === ''); // Notice how we use index 0 to determine which workspace that is active with empty path. [NL] return html`
- ${hint + ${hint && !active ? html`${hint.text}` From 1d48d6ab08c5aff71c8878fced3e4ec6011accf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 15 Aug 2025 19:49:15 +0200 Subject: [PATCH 32/65] use const path --- .../content/workspace/views/edit/content-editor.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5e0aaf9e6c4d..4bd0d79f639d 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 @@ -112,7 +112,7 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements const tabName = tab.name ?? ''; const path = `tab/${encodeFolderName(tabName)}`; routes.push({ - path: `tab/${encodeFolderName(tabName)}`, + path, component: () => import('./content-editor-tab.element.js'), setup: (component) => { (component as UmbContentWorkspaceViewEditTabElement).containerId = tab.id; From 421af86866aef03f20c299439fd0cac7531b0937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 15 Aug 2025 19:49:32 +0200 Subject: [PATCH 33/65] handle gone parent --- .../src/packages/core/hint/context/hints.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts index b15d7ab77dc8..06b9304ee7c7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts @@ -105,7 +105,7 @@ export class UmbHintController< parent?.descendingHints(this.#viewAlias), (hints) => { if (!hints) { - console.warn('TODO: Does this case happen, maybe its only in destruction?'); + // Parent properly lost, so lets assume the parent hints are empty: [NL] hints = []; } this.initiateChange(); From 2ce17d320eb673b07d00fb3d2bfe70b8bcf09f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 15 Aug 2025 21:14:29 +0200 Subject: [PATCH 34/65] added comments on something to be looked at --- .../block-list-editor/property-editor-ui-block-list.element.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts index baeba6c44f0b..943d79c51dd6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts @@ -216,6 +216,7 @@ export class UmbPropertyEditorUIBlockListElement this.#gotPropertyContext(context); }); + // TODO: Why is this logic not part of the Block Grid and RTE Editors? [NL] // Observe Blocks and clean up validation messages for content/settings that are not in the block list anymore: this.observe( this.#managerContext.layouts, @@ -224,6 +225,7 @@ export class UmbPropertyEditorUIBlockListElement const contentKeys = layouts.map((x) => x.contentKey); this.#validationContext.messages.getMessagesOfPathAndDescendant('$.contentData').forEach((message) => { // get the KEY from this string: $.contentData[?(@.key == 'KEY')] + // TODO: Investigate if this is missing a part to just get the [] part of the path. Cause couldn't there be a sub path inside of this. [NL] const key = extractJsonQueryProps(message.path).key; if (key && contentKeys.indexOf(key) === -1) { validationMessagesToRemove.push(message.key); @@ -232,6 +234,7 @@ export class UmbPropertyEditorUIBlockListElement const settingsKeys = layouts.map((x) => x.settingsKey).filter((x) => x !== undefined) as string[]; this.#validationContext.messages.getMessagesOfPathAndDescendant('$.settingsData').forEach((message) => { + // TODO: Investigate if this is missing a part to just get the [] part of the path. Cause couldn't there be a sub path inside of this. [NL] // get the key from this string: $.settingsData[?(@.key == 'KEY')] const key = extractJsonQueryProps(message.path).key; if (key && settingsKeys.indexOf(key) === -1) { From 09367685f621ab148f7594a951ac07e3ba978aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 15 Aug 2025 22:03:40 +0200 Subject: [PATCH 35/65] hints context types --- .../src/packages/core/hint/context/hints.context.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.context.ts index 0da0c3dfbff1..bff7a08342e8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.context.ts @@ -1,10 +1,15 @@ +import type { UmbPartialSome } from '@umbraco-cms/backoffice/utils'; +import type { UmbHint, UmbIncomingHintBase } from '../types.js'; import { UMB_HINT_CONTEXT } from './hint.context-token.js'; import { UmbHintController } from './hints.controller.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbHintContext extends UmbHintController { +export class UmbHintContext< + HintType extends UmbHint = UmbHint, + IncomingHintType extends UmbIncomingHintBase = UmbPartialSome, +> extends UmbHintController { constructor(host: UmbControllerHost) { super(host); - this.provideContext(UMB_HINT_CONTEXT, this); + this.provideContext(UMB_HINT_CONTEXT, this as unknown as UmbHintContext); } } From 4c15a854a352f6d25d5b22a6ed39beaa148e77d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 15 Aug 2025 22:51:58 +0200 Subject: [PATCH 36/65] contentTypeMergedContainers --- .../content-type-structure-manager.class.ts | 64 ++++++++++ .../packages/content/content-type/types.ts | 8 ++ .../content-detail-workspace-base.ts | 12 +- .../content-validation-to-hints.manager.ts | 111 ++++++++++++++++++ .../views/edit/content-editor.element.ts | 10 +- .../core/hint/context/hints.controller.ts | 1 + 6 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index d7708d7d87c1..b44c07bb955c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -1,6 +1,7 @@ import type { UmbContentTypeModel, UmbPropertyContainerTypes, + UmbPropertyTypeContainerMergedModel, UmbPropertyTypeContainerModel, UmbPropertyTypeModel, } from '../types.js'; @@ -28,6 +29,7 @@ import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-ap import { umbExtensionsRegistry, type ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; import { UmbError } from '@umbraco-cms/backoffice/resources'; +import { encodeFolderName } from '@umbraco-cms/backoffice/router'; type UmbPropertyTypeUnique = UmbPropertyTypeModel['unique']; @@ -866,4 +868,66 @@ export class UmbContentTypeStructureManager< this.#contentTypes.destroy(); super.destroy(); } + + // TODO: Make use of this for the structure helpers. + readonly contentTypeMergedContainers = createObservablePart( + this.#contentTypeContainers, + (containers: UmbPropertyTypeContainerModel[]): UmbPropertyTypeContainerMergedModel[] => { + // Lookup map for containers + const containerByIdCache = new Map(); + for (const c of containers) { + containerByIdCache.set(c.id, c); + } + + // Cache to avoid recomputing parent chains + const chainCache = new Map>(); + + // Map to merge duplicates + const mergedMap = new Map(); + + for (const container of containers) { + const path = getContainerChainKey(container, containerByIdCache, chainCache); + const key = path?.join('|') ?? ''; + if (!mergedMap.has(key)) { + // Store the first occurrence + mergedMap.set(key, { + ids: [container.id], + path: path, + type: container.type, + name: container.name, + sortOrder: container.sortOrder, // Heavily assuming the first is the owner content type container, this could maybe turn out not always to be the case? + }); + } else { + // existing already then just add the id: + mergedMap.get(key)?.ids.push(container.id); + } + } + + return Array.from(mergedMap.values()); + }, + ); +} + +// Get a unique key for a container including all parent type/name pairs +function getContainerChainKey( + container: UmbPropertyTypeContainerModel, + containerById: Map, + chainCache: Map>, +): Array { + if (chainCache.has(container.id)) { + return chainCache.get(container.id)!; + } + + // Notice this is made compatible with the path for the URL of the tab, making the match simpler in the other end. [NL] + let path = [`${container.type.toLowerCase()}/${encodeFolderName(container.name)}`]; + if (container.parent && containerById.has(container.parent.id)) { + const parent = containerById.get(container.parent.id)!; + path = [...getContainerChainKey(parent, containerById, chainCache), ...path]; + } else if (!container.parent && container.type === 'Group') { + // Append root to the containers with no parent. + path.unshift(`root`); + } + + chainCache.set(container.id, [...path]); + return path; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/types.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/types.ts index 4364ef2e5be7..1ada9548f446 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/types.ts @@ -13,6 +13,14 @@ export interface UmbPropertyTypeContainerModel { type: UmbPropertyContainerTypes; sortOrder: number; } + +export interface UmbPropertyTypeContainerMergedModel { + ids: Array; + path: Array; + name: string; + type: UmbPropertyContainerTypes; + sortOrder: number; +} /** * * @deprecated diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts index 435585b43d26..33c869b6854c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts @@ -54,7 +54,8 @@ import { type UmbPropertyTypePresetModelTypeModel, } from '@umbraco-cms/backoffice/property'; import { UmbSegmentCollectionRepository, type UmbSegmentCollectionItemModel } from '@umbraco-cms/backoffice/segment'; -import { UmbHintContext } from '@umbraco-cms/backoffice/hint'; +import { UmbHintContext, type UmbVariantHint } from '@umbraco-cms/backoffice/hint'; +import { UmbContentValidationToHintsManager } from './content-validation-to-hints.manager.js'; export interface UmbContentDetailWorkspaceContextArgs< DetailModelType extends UmbContentDetailModel, @@ -140,7 +141,7 @@ export abstract class UmbContentDetailWorkspaceContextBase< readonly splitView = new UmbWorkspaceSplitViewManager(); /* Hints */ - readonly hints = new UmbHintContext(this); + readonly hints = new UmbHintContext(this); /* Variant Options */ // TODO: Optimize this so it uses either a App Language Context? [NL] @@ -209,6 +210,13 @@ export abstract class UmbContentDetailWorkspaceContextBase< x ? x.variesByCulture || x.variesBySegment : undefined, ); + new UmbContentValidationToHintsManager( + this, + this.structure, + this.validationContext, + this.hints, + ); + this.variantOptions = mergeObservables( [this.variesByCulture, this.variesBySegment, this.variants, this.languages, this._segments], ([variesByCulture, variesBySegment, variants, languages, segments]) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts new file mode 100644 index 000000000000..72fefa195d82 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts @@ -0,0 +1,111 @@ +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { + UmbContentTypeModel, + UmbContentTypeStructureManager, + UmbPropertyTypeContainerMergedModel, +} from '@umbraco-cms/backoffice/content-type'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbHintController, UmbVariantHint } from '@umbraco-cms/backoffice/hint'; +import { extractJsonQueryProps, type UmbValidationContext } from '@umbraco-cms/backoffice/validation'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; + +/* + * @internal + * @module UmbContentValidationToHintsManager + * @description + * This manager observes the content type structure and validation messages, converting them into hints. + * It is designed to be used in a content workspace to provide real-time feedback on content validation. + */ +export class UmbContentValidationToHintsManager< + ContentTypeDetailModelType extends UmbContentTypeModel = UmbContentTypeModel, +> extends UmbControllerBase { + /*workspace.hints.addOne({ + unique: 'exampleHintFromToggleAction', + path: ['Umb.WorkspaceView.Document.Edit', 'root'], + text: 'Hi', + color: 'invalid', + weight: 100, + }); + + TODO: + * Maintaine structural awareness of all Properties. + * Observe validation messages for all Properties, and turn them into Hints as fitting. + */ + + #hintedMsgs: Set = new Set(); + + #containers: Array = []; + + constructor( + host: UmbControllerHost, + structure: UmbContentTypeStructureManager, + validation: UmbValidationContext, + hints: UmbHintController, + ) { + super(host); + + this.observe(structure.contentTypeMergedContainers, (merged) => { + //console.log(merged); + this.#containers = merged; + }); + + this.observe(validation.messages.messagesOfPathAndDescendant('$.values'), (messages) => { + messages.forEach((message) => { + if (this.#hintedMsgs.has(message.key)) return; + + // Get the value between [ and ] of message.path: + const query = getValueBetweenBrackets(message.path); + if (!query) return; + const queryProps = extractJsonQueryProps(query); + + const alias = queryProps.alias; + const variantId = UmbVariantId.CreateFromPartial(queryProps); + + structure.getPropertyStructureByAlias(alias).then((property) => { + if (!property) return; + + let path: Array = ['root']; + if (property.container) { + const container = this.#containers.find((c) => c.ids.includes(property.container!.id)); + if (container) { + path = container.path; + } else { + throw new Error( + `Could not find the declared container of id "${property.container.id}" for property with alias: "${property.alias}"`, + ); + } + } + + hints.addOne({ + unique: message.key, + path: ['Umb.WorkspaceView.Document.Edit', ...path], + text: '!', + /*label: message.body,*/ + color: 'invalid', + weight: 1000, + variantId, + }); + console.log('add one for', hints.getAll()); + this.#hintedMsgs.add(message.key); + }); + }); + this.#hintedMsgs.forEach((key) => { + if (!messages.some((msg) => msg.key === key)) { + console.log('remove one', key); + this.#hintedMsgs.delete(key); + hints.removeOne(key); + } + }); + }); + } +} + +function getValueBetweenBrackets(path: string): string | null { + const start = path.indexOf('['); + if (start === -1) return null; + + const end = path.indexOf(']', start + 1); + if (end === -1) return null; + + return path.substring(start + 1, end); +} 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 4bd0d79f639d..c983d1f63afd 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 @@ -45,7 +45,7 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements @state() private _hintMap: Map = new Map(); - #viewContexts: Array = []; + #tabViewContexts: Array = []; #structureManager?: UmbContentTypeStructureManager; @@ -55,8 +55,10 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements super(); this.consumeContext(UMB_VIEW_CONTEXT, (context) => { - // TODO: Parse on as inhiretFrom for all the view contexts. this.#viewContext = context; + this.#tabViewContexts.forEach((view) => { + view.inheritFrom(this.#viewContext); + }); }); this._tabsStructureHelper.setIsRoot(true); @@ -139,9 +141,9 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements } #createViewContext(viewAlias: string) { - if (!this.#viewContexts.find((context) => context.viewAlias === viewAlias)) { + if (!this.#tabViewContexts.find((context) => context.viewAlias === viewAlias)) { const view = new UmbViewContext(this, viewAlias); - this.#viewContexts.push(view); + this.#tabViewContexts.push(view); view.inheritFrom(this.#viewContext); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts index 06b9304ee7c7..473996e260da 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts @@ -95,6 +95,7 @@ export class UmbHintController< } inheritFrom(parent: UmbHintController | undefined): void { + if (this.#parent === parent) return; this.#parent = parent; this.observe(this.#parent?.scaffold, (scaffold) => { if (scaffold) { From d6395d013ae3db82af47254141edbb8c964d04f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 19 Aug 2025 10:04:55 +0200 Subject: [PATCH 37/65] lint fixes --- .../content-type-structure-manager.class.ts | 6 +++++ .../content-detail-workspace-base.ts | 2 +- .../content-validation-to-hints.manager.ts | 4 +++ .../core/hint/context/hint.context-token.ts | 2 +- .../core/hint/context/hints.context.ts | 2 +- .../core/hint/context/hints.controller.ts | 4 +-- .../core/view/context/view.context.ts | 27 ++++++++++++------- .../workspace-editor.context.ts | 2 +- .../workspace-view.context-token.ts | 2 +- 9 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index b44c07bb955c..077b66c2c440 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -909,6 +909,12 @@ export class UmbContentTypeStructureManager< } // Get a unique key for a container including all parent type/name pairs +/** + * + * @param container + * @param containerById + * @param chainCache + */ function getContainerChainKey( container: UmbPropertyTypeContainerModel, containerById: Map, diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts index 33c869b6854c..46a6270889ec 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts @@ -6,6 +6,7 @@ import type { UmbContentPropertyDatasetContext } from '../property-dataset-conte import type { UmbContentValidationRepository } from '../repository/content-validation-repository.interface.js'; import type { UmbContentWorkspaceContext } from './content-workspace-context.interface.js'; import { UmbContentDetailValidationPathTranslator } from './content-detail-validation-path-translator.js'; +import { UmbContentValidationToHintsManager } from './content-validation-to-hints.manager.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbDetailRepository, UmbDetailRepositoryConstructor } from '@umbraco-cms/backoffice/repository'; import { @@ -55,7 +56,6 @@ import { } from '@umbraco-cms/backoffice/property'; import { UmbSegmentCollectionRepository, type UmbSegmentCollectionItemModel } from '@umbraco-cms/backoffice/segment'; import { UmbHintContext, type UmbVariantHint } from '@umbraco-cms/backoffice/hint'; -import { UmbContentValidationToHintsManager } from './content-validation-to-hints.manager.js'; export interface UmbContentDetailWorkspaceContextArgs< DetailModelType extends UmbContentDetailModel, diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts index 72fefa195d82..e8d29fa41aa3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts @@ -100,6 +100,10 @@ export class UmbContentValidationToHintsManager< } } +/** + * + * @param path {string} The path string to extract the value from. + */ function getValueBetweenBrackets(path: string): string | null { const start = path.indexOf('['); if (start === -1) return null; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.context-token.ts index df7bbd2a2eaa..5649e78018c1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hint.context-token.ts @@ -1,4 +1,4 @@ -import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import type { UmbHintController } from './hints.controller.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; export const UMB_HINT_CONTEXT = new UmbContextToken('UmbHintContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.context.ts index bff7a08342e8..87025e2717f2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.context.ts @@ -1,7 +1,7 @@ -import type { UmbPartialSome } from '@umbraco-cms/backoffice/utils'; import type { UmbHint, UmbIncomingHintBase } from '../types.js'; import { UMB_HINT_CONTEXT } from './hint.context-token.js'; import { UmbHintController } from './hints.controller.js'; +import type { UmbPartialSome } from '@umbraco-cms/backoffice/utils'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbHintContext< diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts index 473996e260da..0c9f9fd36d3a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts @@ -1,10 +1,10 @@ import type { UmbPartialSome } from '../../utils/type/index.js'; +import type { UmbHint, UmbIncomingHintBase } from '../types.js'; +import { UMB_HINT_CONTEXT } from './hint.context-token.js'; import { UmbControllerBase, type UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbArrayState, UmbObjectState, type Observable } from '@umbraco-cms/backoffice/observable-api'; -import { UMB_HINT_CONTEXT } from './hint.context-token.js'; import type { UmbContextProviderController } from '@umbraco-cms/backoffice/context-api'; -import type { UmbHint, UmbIncomingHintBase } from '../types.js'; export class UmbHintController< HintType extends UmbHint = UmbHint, 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 9dc44bd4e4dc..6c8a85bc126a 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,9 +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, type Observable } from '@umbraco-cms/backoffice/observable-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 { UMB_VIEW_CONTEXT } from './view.context-token'; /** * @@ -20,23 +20,19 @@ export class UmbViewContext extends UmbControllerBase { public readonly viewAlias: string; #variantId = new UmbClassState(undefined); - protected readonly _variantId = this.#variantId.asObservable(); + protected readonly variantId = this.#variantId.asObservable(); public hints; readonly firstHintOfVariant; - constructor(host: UmbControllerHost, viewAlias: string, variantId?: UmbVariantId) { + constructor(host: UmbControllerHost, viewAlias: string) { super(host); this.viewAlias = viewAlias; - this.#variantId.setValue(variantId); this.hints = new UmbHintController(this, { viewAlias: viewAlias, - scaffold: { - variantId: variantId, - }, }); - this.firstHintOfVariant = mergeObservables([this._variantId, this.hints.hints], ([variantId, hints]) => { + this.firstHintOfVariant = mergeObservables([this.variantId, this.hints.hints], ([variantId, hints]) => { if (variantId) { return hints.find((hint) => (hint.variantId ? hint.variantId.equal(variantId!) : true)); } else { @@ -45,6 +41,11 @@ export class UmbViewContext extends UmbControllerBase { }); } + setVariantId(variantId: UmbVariantId | undefined): void { + this.#variantId.setValue(variantId); + this.hints.updateScaffold({ variantId: variantId }); + } + provideAt(controllerHost: UmbClassInterface): void { if (this.#currentProvideHost === controllerHost) return; @@ -64,7 +65,13 @@ export class UmbViewContext extends UmbControllerBase { } inheritFrom(context?: UmbViewContext): void { - // TODO: Do you want to inherit the variantId as well? Then I think VariantId needs to become a state. + this.observe( + context?.variantId, + (variantId) => { + this.setVariantId(variantId); + }, + 'observeParentVariantId', + ); this.hints.inheritFrom(context?.hints); } } 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 b6a3e46badd0..b76c62b7c45b 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 @@ -5,7 +5,7 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbBasicState } from '@umbraco-cms/backoffice/observable-api'; -import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UmbHintController, type UmbVariantHint } from '@umbraco-cms/backoffice/hint'; export class UmbWorkspaceEditorContext extends UmbContextBase { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context-token.ts index 1013b1fdb85d..fa8d595d8083 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context-token.ts @@ -1,5 +1,5 @@ -import type { UmbViewContext } from '@umbraco-cms/backoffice/view'; import type { UmbWorkspaceViewContext } from './workspace-view.context.js'; +import type { UmbViewContext } from '@umbraco-cms/backoffice/view'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; export const UMB_WORKSPACE_VIEW_CONTEXT = new UmbContextToken( From 4c1df67804b4731ca105065624c18759d2d5f30c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 19 Aug 2025 10:05:10 +0200 Subject: [PATCH 38/65] public contentTypeMergedContainers --- .../structure/content-type-structure-manager.class.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index 077b66c2c440..2894083cdf2a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -869,8 +869,7 @@ export class UmbContentTypeStructureManager< super.destroy(); } - // TODO: Make use of this for the structure helpers. - readonly contentTypeMergedContainers = createObservablePart( + public readonly contentTypeMergedContainers = createObservablePart( this.#contentTypeContainers, (containers: UmbPropertyTypeContainerModel[]): UmbPropertyTypeContainerMergedModel[] => { // Lookup map for containers From de765be3cc6e49870f1c5bf9d0d1aa6370e2ecd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 19 Aug 2025 10:18:20 +0200 Subject: [PATCH 39/65] refactor property structure helper class --- ...nt-type-property-structure-helper.class.ts | 93 ++----------------- .../content-type-structure-manager.class.ts | 20 ++++ 2 files changed, 26 insertions(+), 87 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-property-structure-helper.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-property-structure-helper.class.ts index 120b81481437..24657b72667f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-property-structure-helper.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-property-structure-helper.class.ts @@ -68,100 +68,19 @@ export class UmbContentTypePropertyStructureHelper; #observeContainers() { - if (!this.#structure || this.#containerId === undefined) return; - - if (this.#containerId === null) { - this.observe( - this.#structure.propertyStructuresOf(null), - (properties) => { - this.#propertyStructure.setValue(properties); - }, - 'observePropertyStructures', - ); - this.removeUmbControllerByAlias('_observeContainers'); - } else { - this.observe( - this.#structure.containerById(this.#containerId), - (container) => { - if (container) { - this._containerName = container.name ?? ''; - this._containerType = container.type; - if (container.parent) { - // We have a parent for our main container, so lets observe that one as well: [NL] - this.observe( - this.#structure!.containerById(container.parent.id), - (parent) => { - if (parent) { - this._parentName = parent.name ?? ''; - this._parentType = parent.type; - this.#observeSimilarContainers(); - } else { - this.removeUmbControllerByAlias('_observeContainers'); - this._parentName = undefined; - this._parentType = undefined; - } - }, - '_observeMainParentContainer', - ); - } else { - this.removeUmbControllerByAlias('_observeMainParentContainer'); - this._parentName = null; //In this way we want to look for one without a parent. [NL] - this._parentType = undefined; - this.#observeSimilarContainers(); - } - } else { - this.removeUmbControllerByAlias('_observeContainers'); - this._containerName = undefined; - this._containerType = undefined; - this.#propertyStructure.setValue([]); - } - }, - '_observeMainContainer', - ); - } - } - - #observeSimilarContainers() { - if (this._containerName === undefined || !this._containerType || this._parentName === undefined) return; this.observe( - this.#structure!.containersByNameAndTypeAndParent( - this._containerName, - this._containerType, - this._parentName, - this._parentType, - ), - (groupContainers) => { - if (this.#containers) { - // We want to remove properties of groups that does not exist anymore: [NL] - const goneGroupContainers = this.#containers.filter((x) => !groupContainers.some((y) => y.id === x.id)); - const _propertyStructure = this.#propertyStructure - .getValue() - .filter((x) => !goneGroupContainers.some((y) => y.id === x.container?.id)); - this.#propertyStructure.setValue(_propertyStructure); - } - + this.#containerId ? this.#structure?.mergedContainersOfId(this.#containerId) : undefined, + (container) => { this.observe( - mergeObservables( - groupContainers.map((group) => this.#structure!.propertyStructuresOf(group.id)), - (sources) => { - return sources.flatMap((x) => x); - }, - ), + container ? this.#structure?.propertyStructuresOfGroupIds(container.ids ?? []) : undefined, (properties) => { - this.#propertyStructure.setValue(properties); + this.#propertyStructure.setValue(properties ?? []); }, - 'observePropertyStructures', + 'observeProperties', ); - this.#containers = groupContainers; }, - '_observeContainers', + 'observeContainer', ); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index 2894083cdf2a..314849d7f8f3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -735,6 +735,20 @@ export class UmbContentTypeStructureManager< }); } + propertyStructuresOfGroupIds(groupIds: Array) { + return this.#contentTypes.asObservablePart((docTypes) => { + const props: UmbPropertyTypeModel[] = []; + docTypes.forEach((docType) => { + docType.properties?.forEach((property) => { + if (property.container?.id && groupIds.includes(property.container.id)) { + props.push(property); + } + }); + }); + return props; + }); + } + rootContainers(containerType: UmbPropertyContainerTypes) { return createObservablePart(this.#contentTypeContainers, (data) => { return data.filter((x) => x.parent === null && x.type === containerType); @@ -905,6 +919,12 @@ export class UmbContentTypeStructureManager< return Array.from(mergedMap.values()); }, ); + + public mergedContainersOfId(containerId: string) { + return createObservablePart(this.contentTypeMergedContainers, (mergedContainers) => { + return mergedContainers.find((x) => x.ids.includes(containerId)); + }); + } } // Get a unique key for a container including all parent type/name pairs From 3b07192e1c3aa6872b973657486000e07025801a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 19 Aug 2025 13:19:02 +0200 Subject: [PATCH 40/65] a few notes for Presets --- .../content/workspace/content-detail-workspace-base.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts index 46a6270889ec..cabde3c7cbd6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts @@ -378,12 +378,17 @@ export abstract class UmbContentDetailWorkspaceContextBase< // Load the content type structure, usually this comes from the data, but in this case we are making the data, and we need this to be able to complete the data. [NL] await this.structure.loadType((data as any)[this.#contentTypePropertyName].unique); + /** + * TODO: Should we also set Preset Values when loading Content, because maybe content contains uncreated Cultures or Segments. + */ + // Set culture and segment for all values: const cultures = this.#languages.getValue().map((x) => x.unique); if (this.structure.variesBySegment) { console.warn('Segments are not yet implemented for preset'); } + // TODO: Add Segments for Presets: const segments: Array | undefined = this.structure.variesBySegment ? [] : undefined; const repo = new UmbDataTypeDetailRepository(this); From c2752794f91844092bcf902354f5e1fbadcc126e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 20 Aug 2025 11:48:24 +0200 Subject: [PATCH 41/65] set variant ID instead of parsing it to the constructor --- .../components/workspace-editor/workspace-editor.context.ts | 3 ++- .../components/workspace-editor/workspace-view.context.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) 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 b76c62b7c45b..dd916b5a2ed2 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 @@ -47,7 +47,8 @@ export class UmbWorkspaceEditorContext extends UmbContextBase { workspaceViews .filter((view) => !viewsToKeep.some((x) => x.manifest.alias === view.manifest.alias)) .forEach((view) => { - const context = new UmbWorkspaceViewContext(this, view.manifest, this.#variantId); + const context = new UmbWorkspaceViewContext(this, view.manifest); + context.setVariantId(this.#variantId); context.hints.inheritFrom(this.#hints); newViews.push(context); }); 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 1322e809a72d..f9db421c022d 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 @@ -7,8 +7,8 @@ export class UmbWorkspaceViewContext extends UmbViewContext { public manifest: ManifestWorkspaceView; - constructor(host: UmbControllerHost, manifest: ManifestWorkspaceView, variantId?: UmbVariantId) { - super(host, manifest.alias, variantId); + constructor(host: UmbControllerHost, manifest: ManifestWorkspaceView) { + super(host, manifest.alias); this.manifest = manifest; } } From 819111154d74981e1213dd5e90682744dc36f6bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 21 Aug 2025 17:23:02 +0200 Subject: [PATCH 42/65] do not inject root to the path --- .../content/workspace/content-validation-to-hints.manager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts index e8d29fa41aa3..54fa389c58c5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts @@ -64,7 +64,7 @@ export class UmbContentValidationToHintsManager< structure.getPropertyStructureByAlias(alias).then((property) => { if (!property) return; - let path: Array = ['root']; + let path: Array = []; if (property.container) { const container = this.#containers.find((c) => c.ids.includes(property.container!.id)); if (container) { @@ -78,7 +78,7 @@ export class UmbContentValidationToHintsManager< hints.addOne({ unique: message.key, - path: ['Umb.WorkspaceView.Document.Edit', ...path], + path: ['Umb.WorkspaceView.Document.Edit', 'root', ...path], text: '!', /*label: message.body,*/ color: 'invalid', From db1d91d9a899f15bfac7381482759c2e9b810dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 21 Aug 2025 17:24:29 +0200 Subject: [PATCH 43/65] adjust structure manager logic --- .../content-type-structure-manager.class.ts | 86 +++++++++++++++++-- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index 314849d7f8f3..4774c1c129ca 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -789,8 +789,8 @@ export class UmbContentTypeStructureManager< ); } - isOwnerContainer(containerId: string) { - return this.getOwnerContentType()?.containers?.filter((x) => x.id === containerId); + isOwnerContainer(containerId: string): boolean | undefined { + return this.getOwnerContentType()?.containers?.some((x) => x.id === containerId); } containersOfParentId(parentId: string, containerType: UmbPropertyContainerTypes) { @@ -883,6 +883,7 @@ export class UmbContentTypeStructureManager< super.destroy(); } + #mergedContainers: UmbPropertyTypeContainerMergedModel[] = []; public readonly contentTypeMergedContainers = createObservablePart( this.#contentTypeContainers, (containers: UmbPropertyTypeContainerModel[]): UmbPropertyTypeContainerMergedModel[] => { @@ -900,11 +901,14 @@ export class UmbContentTypeStructureManager< for (const container of containers) { const path = getContainerChainKey(container, containerByIdCache, chainCache); - const key = path?.join('|') ?? ''; + const key = path?.join('|') ?? null; if (!mergedMap.has(key)) { // Store the first occurrence mergedMap.set(key, { + key: key, ids: [container.id], + ownerId: this.isOwnerContainer(container.id) ? container.id : undefined, + parentIds: new Set([container.parent?.id ?? null]), path: path, type: container.type, name: container.name, @@ -912,19 +916,84 @@ export class UmbContentTypeStructureManager< }); } else { // existing already then just add the id: - mergedMap.get(key)?.ids.push(container.id); + const existing = mergedMap.get(key)!; + existing.ids.push(container.id); + existing.parentIds.add(container.parent?.id ?? null); + existing.ownerId ??= this.isOwnerContainer(container.id) ? container.id : undefined; } } - return Array.from(mergedMap.values()); + return (this.#mergedContainers = Array.from(mergedMap.values())); }, ); - public mergedContainersOfId(containerId: string) { + public mergedContainersOfId(id: string): Observable { return createObservablePart(this.contentTypeMergedContainers, (mergedContainers) => { - return mergedContainers.find((x) => x.ids.includes(containerId)); + return mergedContainers.find((x) => x.ids.includes(id)); }); } + + /** + * + * Find merged containers that match the provided container ids. + * Notice if you can provide one or more ids matching the same container and it will still only return return the matching container once. + * @param containerIds - An array of container ids to find merged containers for. + * @returns {Observable} - An observable that emits the merged containers that match the provided container ids. + */ + /* + public mergedContainersOfIds(searchIds: Array): Observable> { + return createObservablePart(this.contentTypeMergedContainers, (mergedContainers) => { + return mergedContainers.filter((x) => searchIds.some((id) => x.ids.includes(id))); + }); + } + */ + + /** + * + * Find merged containers that match the provided container ids. + * Notice if you can provide one or more ids matching the same container and it will still only return return the matching container once. + * @param containerIds - An array of container ids to find merged containers for. + * @returns {UmbPropertyTypeContainerMergedModel | undefined} - The merged containers that match the provided container ids. + */ + getMergedContainerById(id: string): UmbPropertyTypeContainerMergedModel | undefined { + return this.#mergedContainers.find((x) => x.ids.includes(id)); + } + + /** + * + * Find merged child containers that are children of the provided parent container ids. + * Notice this will find matching containers and include their child containers in this. + * @param containerIds - An array of container ids to find merged child containers for. + * @param type - The type of the containers to find. + * @returns {Observable} - An observable that emits the merged child containers that match the provided container ids. + */ + public mergedContainersOfParentIdAndType( + searchId: string | null, + type: UmbPropertyContainerTypes, + ): Observable> { + return createObservablePart(this.contentTypeMergedContainers, (mergedContainers) => { + return mergedContainers.filter((x) => x.type === type && x.parentIds.has(searchId)); + }); + } + + /** + * + * Find merged child containers that are children of one of the provided parent container ids. + * Notice if you can provide one or more ids matching the same parent and it will still only return return the matching child container once. + * @param containerIds - An array of container ids to find merged child containers for. + * @param type - The type of the containers to find. + * @returns {Observable} - An observable that emits the merged child containers that match the provided container ids. + */ + /* + public mergedContainersOfParentIds( + searchIds: Array, + type: UmbPropertyContainerTypes, + ): Observable> { + return createObservablePart(this.contentTypeMergedContainers, (mergedContainers) => { + return mergedContainers.filter((x) => x.type === type && searchIds.some((id) => x.parentIds.has(id))); + }); + } + */ } // Get a unique key for a container including all parent type/name pairs @@ -950,7 +1019,8 @@ function getContainerChainKey( path = [...getContainerChainKey(parent, containerById, chainCache), ...path]; } else if (!container.parent && container.type === 'Group') { // Append root to the containers with no parent. - path.unshift(`root`); + //path.unshift(`root`); + // No that is not part of the responsibility of this one. [NL] } chainCache.set(container.id, [...path]); From 14292f12b17487021dd747a44a87b0aeca1bd828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 21 Aug 2025 17:25:03 +0200 Subject: [PATCH 44/65] UmbPropertyTypeContainerMergedModel type update --- .../src/packages/content/content-type/types.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/types.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/types.ts index 1ada9548f446..5d43b761bd75 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/types.ts @@ -15,7 +15,10 @@ export interface UmbPropertyTypeContainerModel { } export interface UmbPropertyTypeContainerMergedModel { + key: string; ids: Array; + ownerId?: string; + parentIds: Set; path: Array; name: string; type: UmbPropertyContainerTypes; From bb934d9c8d8b295cd2fd671bdae8c9085bbff81f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 21 Aug 2025 17:43:53 +0200 Subject: [PATCH 45/65] correct mergedContainersOfParentIdAndType --- .../structure/content-type-structure-manager.class.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index 4774c1c129ca..55dc2d04b39f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -972,7 +972,9 @@ export class UmbContentTypeStructureManager< type: UmbPropertyContainerTypes, ): Observable> { return createObservablePart(this.contentTypeMergedContainers, (mergedContainers) => { - return mergedContainers.filter((x) => x.type === type && x.parentIds.has(searchId)); + // First find the path for the parentId, and then find matching children: + const parentIds = searchId ? (mergedContainers.find((x) => x.ids.includes(searchId))?.ids ?? []) : [null]; + return mergedContainers.filter((x) => x.type === type && parentIds.some((id) => x.parentIds.has(id))); }); } From e6f7432abac85b62b87326b3372b7c91ed18a879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 21 Aug 2025 17:51:30 +0200 Subject: [PATCH 46/65] refactor to utilize new observable for better outcome and performance --- ...ace-view-edit-content-no-router.element.ts | 28 +-- .../block-workspace-view-edit-tab.element.ts | 12 +- .../edit/block-workspace-view-edit.element.ts | 8 +- ...t-type-container-structure-helper.class.ts | 132 ++++++++++-- ...nt-type-property-structure-helper.class.ts | 15 +- .../content-type-structure-manager.class.ts | 41 +++- ...ontent-type-design-editor-group.element.ts | 78 +++---- ...ent-type-design-editor-property.element.ts | 4 + .../content-type-design-editor-tab.element.ts | 200 ++++++++++-------- .../content-type-design-editor.element.ts | 91 ++++---- .../views/edit/content-editor-tab.element.ts | 10 +- .../views/edit/content-editor.element.ts | 8 +- 12 files changed, 398 insertions(+), 229 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.ts index 06108fe2c6eb..c1fd3ba23194 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.ts @@ -4,7 +4,7 @@ import { css, html, customElement, state, repeat, nothing } from '@umbraco-cms/b 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 { UmbPropertyTypeContainerModel } from '@umbraco-cms/backoffice/content-type'; +import type { UmbPropertyTypeContainerMergedModel } from '@umbraco-cms/backoffice/content-type'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace'; /** @@ -20,10 +20,10 @@ export class UmbBlockWorkspaceViewEditContentNoRouterElement extends UmbLitEleme private _hasRootGroups = false; @state() - private _tabs?: Array; + private _tabs?: Array; @state() - private _activeTabId?: string | null | undefined; + private _activeTabKey?: string | null | undefined; //@state() //private _activeTabName?: string | null | undefined; @@ -36,7 +36,7 @@ export class UmbBlockWorkspaceViewEditContentNoRouterElement extends UmbLitEleme this.#tabsStructureHelper.setIsRoot(true); this.#tabsStructureHelper.setContainerChildType('Tab'); - this.observe(this.#tabsStructureHelper.mergedContainers, (tabs) => { + this.observe(this.#tabsStructureHelper.childContainers, (tabs) => { this._tabs = tabs; this.#checkDefaultTabName(); }); @@ -68,20 +68,20 @@ export class UmbBlockWorkspaceViewEditContentNoRouterElement extends UmbLitEleme if (!this._tabs || !this.#blockWorkspace) return; // Find the default tab to grab: - if (this._activeTabId === undefined) { + if (this._activeTabKey === undefined) { if (this._hasRootGroups) { //this._activeTabName = null; - this._activeTabId = null; + this._activeTabKey = null; } else if (this._tabs.length > 0) { //this._activeTabName = this._tabs[0].name; - this._activeTabId = this._tabs[0].id; + this._activeTabKey = this._tabs[0].key; } } } - #setTabName(tabName: string | undefined | null, tabId: string | null | undefined) { + #setTabName(tabName: string | undefined | null, tabKey: string | null | undefined) { //this._activeTabName = tabName; - this._activeTabId = tabId; + this._activeTabKey = tabKey; } override render() { @@ -93,7 +93,7 @@ export class UmbBlockWorkspaceViewEditContentNoRouterElement extends UmbLitEleme ? html` this.#setTabName(null, null)} >Content @@ -105,19 +105,19 @@ export class UmbBlockWorkspaceViewEditContentNoRouterElement extends UmbLitEleme (tab) => { return html` this.#setTabName(tab.name, tab.id)} + .active=${tab.key === this._activeTabKey} + @click=${() => this.#setTabName(tab.name, tab.key)} >${tab.name}`; }, )} ` : nothing} - ${this._activeTabId !== undefined + ${this._activeTabKey !== undefined ? html` + .containerId=${this._activeTabKey}> ` : nothing} `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-tab.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-tab.element.ts index 8bbd9cec4d13..a9165eabc1cf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-tab.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-tab.element.ts @@ -1,7 +1,7 @@ import { UMB_BLOCK_WORKSPACE_CONTEXT } from '../../block-workspace.context-token.js'; import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { UmbContentTypeModel, UmbPropertyTypeContainerModel } from '@umbraco-cms/backoffice/content-type'; +import type { UmbContentTypeModel, UmbPropertyTypeContainerMergedModel } from '@umbraco-cms/backoffice/content-type'; import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -42,7 +42,7 @@ export class UmbBlockWorkspaceViewEditTabElement extends UmbLitElement { hideSingleGroup = false; @state() - private _groups: Array = []; + private _groups: Array = []; @state() private _hasProperties = false; @@ -60,7 +60,7 @@ export class UmbBlockWorkspaceViewEditTabElement extends UmbLitElement { if (!this.#blockWorkspace || !this.#managerName) return; this.#groupStructureHelper.setStructureManager(this.#blockWorkspace[this.#managerName].structure); this.observe( - this.#groupStructureHelper.mergedContainers, + this.#groupStructureHelper.childContainers, (groups) => { this._groups = groups; }, @@ -89,18 +89,18 @@ export class UmbBlockWorkspaceViewEditTabElement extends UmbLitElement { ? this.renderGroup(this._groups[0]) : repeat( this._groups, - (group) => group.id, + (group) => group.key, (group) => html` ${this.renderGroup(group)}`, )} `; } - renderGroup(group: UmbPropertyTypeContainerModel) { + renderGroup(group: UmbPropertyTypeContainerMergedModel) { return html` + .containerId=${group.key}> `; } 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 de633b326bec..cd0ba81f9f0e 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 @@ -3,7 +3,7 @@ import { UMB_BLOCK_WORKSPACE_CONTEXT } from '../../block-workspace.context-token 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 { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { UmbContentTypeModel, UmbPropertyTypeContainerModel } from '@umbraco-cms/backoffice/content-type'; +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 { encodeFolderName } from '@umbraco-cms/backoffice/router'; @@ -34,7 +34,7 @@ export class UmbBlockWorkspaceViewEditElement extends UmbLitElement implements U private _routes: UmbRoute[] = []; @state() - private _tabs?: Array; + private _tabs?: Array; @state() private _routerPath?: string; @@ -48,7 +48,7 @@ export class UmbBlockWorkspaceViewEditElement extends UmbLitElement implements U this.#tabsStructureHelper.setIsRoot(true); this.#tabsStructureHelper.setContainerChildType('Tab'); this.observe( - this.#tabsStructureHelper.mergedContainers, + this.#tabsStructureHelper.childContainers, (tabs) => { this._tabs = tabs; this.#createRoutes(); @@ -102,7 +102,7 @@ export class UmbBlockWorkspaceViewEditElement extends UmbLitElement implements U component: () => import('./block-workspace-view-edit-tab.element.js'), setup: (component) => { (component as UmbBlockWorkspaceViewEditTabElement).managerName = this.#managerName; - (component as UmbBlockWorkspaceViewEditTabElement).containerId = tab.id; + (component as UmbBlockWorkspaceViewEditTabElement).containerId = tab.ids[0]; }, }); }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-container-structure-helper.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-container-structure-helper.class.ts index 58afbe2cb014..91f210fc55b3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-container-structure-helper.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-container-structure-helper.class.ts @@ -1,8 +1,13 @@ -import type { UmbContentTypeModel, UmbPropertyContainerTypes, UmbPropertyTypeContainerModel } from '../types.js'; +import type { + UmbContentTypeModel, + UmbPropertyContainerTypes, + UmbPropertyTypeContainerMergedModel, + UmbPropertyTypeContainerModel, +} from '../types.js'; import type { UmbContentTypeStructureManager } from './content-type-structure-manager.class.js'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbController, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState, UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; /** * This class is a helper class for managing the structure of containers in a content type. @@ -11,6 +16,7 @@ import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; export class UmbContentTypeContainerStructureHelper extends UmbControllerBase { #init; #initResolver?: (value: unknown) => void; + #initRejector?: () => void; #containerId?: string | null; #childType?: UmbPropertyContainerTypes = 'Group'; @@ -21,31 +27,55 @@ export class UmbContentTypeContainerStructureHelper([], (x) => x.id); - readonly containers = this.#childContainers.asObservable(); + get containers() { + this.#startLegacy(); + return this.#childContainers.asObservable(); + } // State containing the merged containers (only one pr. name): - #mergedChildContainers = new UmbArrayState([], (x) => x.id); - readonly mergedContainers = this.#mergedChildContainers.asObservable(); + #legacyMergedChildContainers = new UmbArrayState([], (x) => x.id); + get mergedContainers() { + this.#startLegacy(); + return this.#legacyMergedChildContainers.asObservable(); + } + + #childContainersMerged = new UmbArrayState([], (x) => x.path); + public readonly childContainers = this.#childContainersMerged.asObservable(); // Owner containers are containers owned by the owner Content Type (The specific one up for editing) #ownerChildContainers: UmbPropertyTypeContainerModel[] = []; - #hasProperties = new UmbArrayState<{ id: string | null; has: boolean }>([], (x) => x.id); - readonly hasProperties = this.#hasProperties.asObservablePart((x) => x.some((y) => y.has)); + #hasProperties = new UmbBooleanState(false); + readonly hasProperties = this.#hasProperties.asObservable(); constructor(host: UmbControllerHost) { super(host); - this.#init = new Promise((resolve) => { + this.#init = new Promise((resolve, reject) => { this.#initResolver = resolve; + this.#initRejector = reject; }); - this.#mergedChildContainers.sortBy((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); + this.#legacyMergedChildContainers.sortBy((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); + } + + // TODO: Implement UmbDeprecated and Obsolete this from v.17 [NL] + #legacyMergeLogic = false; + #startLegacy() { + if (this.#legacyMergeLogic) return; + console.log( + "Pst. we will be deprecating 'mergedContainers' and 'containers' in v.17.0, feel free to use them until v.18.0. But please use 'childContainers'", + ); + this.#legacyMergeLogic = true; + this.#legacyObserveContainers(); this.observe(this.containers, this.#performContainerMerge, null); } public setStructureManager(structure: UmbContentTypeStructureManager | undefined) { if (this.#structure === structure || !structure) return; if (this.#structure && !structure) { + this.#initRejector?.(); + this.#initResolver = undefined; + this.#initRejector = undefined; throw new Error( 'Structure manager is already set, the helpers are not designed to be re-setup with new managers', ); @@ -53,7 +83,9 @@ export class UmbContentTypeContainerStructureHelper { + this.#childContainersMerged.setValue(childContainers ?? []); + }, + 'observeChildContainers', + ); + + if (this.#containerId === null) { + this.removeUmbControllerByAlias('observeParentContainer'); + // Observe root properties: + this.observe( + this.#structure?.hasPropertyStructuresOfRoot(), + (has) => { + this.#hasProperties.setValue(has ?? false); + }, + 'observeProperties', + ); + } else { + // Observe properties of the parent container and matching containers (therefor getting the merged container of the parent id): [NL] + const parentObservable = + this.#containerId !== undefined && this.#childType + ? this.#structure?.mergedContainersOfId(this.#containerId) + : undefined; + + this.observe( + parentObservable, + (parentContainer) => { + this.observe( + parentContainer ? this.#structure?.hasPropertyStructuresOfGroupIds(parentContainer.ids ?? []) : undefined, + (has) => { + this.#hasProperties.setValue(has ?? false); + }, + 'observeProperties', + ); + }, + 'observeParentContainer', + ); + } + } + + // LEGACY properties: #containerName?: string; #containerType?: UmbPropertyContainerTypes; #parentName?: string | null; #parentType?: UmbPropertyContainerTypes; - #observeContainers() { + // LEGACY method: + #legacyObserveContainers() { + if (!this.#legacyMergeLogic) return; if (!this.#structure || this.#containerId === undefined) return; if (this.#containerId === null) { - this.#observeHasPropertiesOf(null); + //this.#observeHasPropertiesOf(null); this.#observeRootContainers(); this.removeUmbControllerByAlias('_observeContainers'); } else { @@ -133,8 +218,7 @@ export class UmbContentTypeContainerStructureHelper { - this.#hasProperties.setValue([]); + //this.#hasProperties.setValue([]); this.#childContainers.setValue([]); this.#containerObservers.forEach((x) => x.destroy()); this.#containerObservers = []; containers.forEach((container) => { - this.#observeHasPropertiesOf(container.id); + //this.#observeHasPropertiesOf(container.id); this.#containerObservers.push( this.observe( @@ -184,6 +269,7 @@ export class UmbContentTypeContainerStructureHelper) { return this.#ownerChildContainers.length > 0 ? containers.filter( @@ -226,22 +315,23 @@ export class UmbContentTypeContainerStructureHelper) => { // Remove containers that matches with a owner container: let merged = this.#filterNonOwnerContainers(containers); // Remove containers of same name and type: // This only works cause we are dealing with a single level of containers in this Helper, if we had more levels we would need to be more clever about the parent as well. [NL] merged = merged.filter((x, i, cons) => i === cons.findIndex((y) => y.name === x.name && y.type === x.type)); - this.#mergedChildContainers.setValue(merged); + this.#legacyMergedChildContainers.setValue(merged); }; /** * Returns true if the container is an owner container. * @param containerId */ - isOwnerChildContainer(containerId?: string) { + isOwnerChildContainer(containerId?: string): boolean | undefined { if (!this.#structure || !containerId) return; - return this.#ownerChildContainers.some((x) => x.id === containerId); + return this.#structure.isOwnerContainer(containerId); } getContentTypeOfContainer(containerId?: string) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-property-structure-helper.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-property-structure-helper.class.ts index 24657b72667f..be18514559e2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-property-structure-helper.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-property-structure-helper.class.ts @@ -1,13 +1,8 @@ -import type { - UmbContentTypeModel, - UmbPropertyContainerTypes, - UmbPropertyTypeContainerModel, - UmbPropertyTypeModel, -} from '../types.js'; +import type { UmbContentTypeModel, UmbPropertyTypeModel } from '../types.js'; import type { UmbContentTypeStructureManager } from './content-type-structure-manager.class.js'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbArrayState, mergeObservables } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; type UmbPropertyTypeUnique = UmbPropertyTypeModel['unique']; @@ -52,7 +47,7 @@ export class UmbContentTypePropertyStructureHelper { diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index 55dc2d04b39f..c7fd42f34e5c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -13,7 +13,7 @@ import { } from '@umbraco-cms/backoffice/repository'; import { UmbId } from '@umbraco-cms/backoffice/id'; import type { UmbControllerHost, UmbController } from '@umbraco-cms/backoffice/controller-api'; -import type { MappingFunction } from '@umbraco-cms/backoffice/observable-api'; +import type { MappingFunction, Observable } from '@umbraco-cms/backoffice/observable-api'; import { UmbArrayState, partialUpdateFrozenArray, @@ -497,19 +497,28 @@ export class UmbContentTypeStructureManager< makeEmptyContainerName( containerId: string, - containerType: UmbPropertyContainerTypes, - parentId: string | null = null, + legacyContainerType?: UmbPropertyContainerTypes, + legacyParentId?: string | null, ): string { return ( - this.makeContainerNameUniqueForOwnerContentType(containerId, 'Unnamed', containerType, parentId) ?? 'Unnamed' + this.makeContainerNameUniqueForOwnerContentType(containerId, 'Unnamed', legacyContainerType, legacyParentId) ?? + 'Unnamed' ); } makeContainerNameUniqueForOwnerContentType( containerId: string, newName: string, - containerType: UmbPropertyContainerTypes, - parentId: string | null = null, + legacyContainerType?: UmbPropertyContainerTypes, + legacyParentId?: string | null, ) { + const container = this.getOwnerContainerById(containerId); + if (!container) { + console.warn(`Container with id ${containerId} not found in owner content type.`); + return null; + } + const containerType = container.type; + const parentId = container.parent?.id ?? null; + const ownerRootContainers = this.getOwnerContainers(containerType, parentId); //getRootContainers() can't differentiates between compositions and locals if (!ownerRootContainers) { return null; @@ -749,6 +758,26 @@ export class UmbContentTypeStructureManager< }); } + hasPropertyStructuresOfGroupIds(groupIds: Array) { + return this.#contentTypes.asObservablePart((docTypes) => { + return docTypes.some((docType) => { + return docType.properties?.some((property) => { + return property.container?.id && groupIds.includes(property.container.id); + }); + }); + }); + } + + hasPropertyStructuresOfRoot() { + return this.#contentTypes.asObservablePart((docTypes) => { + return docTypes.some((docType) => { + return docType.properties?.some((property) => { + return !property.container; + }); + }); + }); + } + rootContainers(containerType: UmbPropertyContainerTypes) { return createObservablePart(this.#contentTypeContainers, (data) => { return data.filter((x) => x.parent === null && x.type === containerType); diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-group.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-group.element.ts index 2745074f6283..39c0e81d3851 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-group.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-group.element.ts @@ -1,4 +1,4 @@ -import type { UmbContentTypeModel, UmbPropertyTypeContainerModel } from '../../../types.js'; +import type { UmbContentTypeModel, UmbPropertyTypeContainerMergedModel } from '../../../types.js'; import type { UmbContentTypeContainerStructureHelper } from '../../../structure/index.js'; import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; @@ -11,16 +11,16 @@ import './content-type-design-editor-properties.element.js'; @customElement('umb-content-type-design-editor-group') export class UmbContentTypeWorkspaceViewEditGroupElement extends UmbLitElement { @property({ attribute: false }) - public set group(value: UmbPropertyTypeContainerModel | undefined) { + public set group(value: UmbPropertyTypeContainerMergedModel | undefined) { if (value === this._group) return; this._group = value; - this._groupId = value?.id; + this._groupId = value?.ownerId ?? value?.ids[0]; this.#checkInherited(); } - public get group(): UmbPropertyTypeContainerModel | undefined { + public get group(): UmbPropertyTypeContainerMergedModel | undefined { return this._group; } - private _group?: UmbPropertyTypeContainerModel | undefined; + private _group?: UmbPropertyTypeContainerMergedModel | undefined; @property({ attribute: false }) public set groupStructureHelper(value: UmbContentTypeContainerStructureHelper | undefined) { @@ -45,7 +45,8 @@ export class UmbContentTypeWorkspaceViewEditGroupElement extends UmbLitElement { @state() private _hasOwnerContainer?: boolean; - @state() + // attrbute is used by Sorter Controller in parent scope. + @property({ type: Boolean, reflect: true, attribute: 'inherited' }) private _inherited?: boolean; @state() @@ -54,48 +55,46 @@ export class UmbContentTypeWorkspaceViewEditGroupElement extends UmbLitElement { #checkInherited() { if (this.groupStructureHelper && this.group) { // Check is this container matches with any other group. If so it is inherited aka. merged with others. [NL] - if (this.group.name) { - // We can first match with something if we have a name [NL] - this.observe( - this.groupStructureHelper.containersByNameAndType(this.group.name, 'Group'), - (containers) => { - const ownerContainer = containers.find((con) => this.groupStructureHelper!.isOwnerChildContainer(con.id)); - const hasAOwnerContainer = !!ownerContainer; - const pureOwnerContainer = hasAOwnerContainer && containers.length === 1; - - this._hasOwnerContainer = hasAOwnerContainer; - this._inherited = !pureOwnerContainer; - this._inheritedFrom = containers - .filter((con) => con.id !== ownerContainer?.id) - .map((con) => this.groupStructureHelper!.getContentTypeOfContainer(con.id)) - .filter((contentType) => contentType !== undefined) as Array; - }, - 'observeGroupContainers', - ); - } else { - // We use name match to determine inheritance, so no name cannot inherit. - this._inherited = false; + if (this.group.ownerId) { this._hasOwnerContainer = true; - this.removeUmbControllerByAlias('observeGroupContainers'); + } + + const notOwnerContainerIds = this.group.ids.filter((id) => id !== this.group!.ownerId); + + if (notOwnerContainerIds.length > 0) { + this._inheritedFrom = notOwnerContainerIds + .map((id) => this.groupStructureHelper!.getContentTypeOfContainer(id)) + .filter((contentType) => contentType !== undefined) as Array; + this._inherited = true; + } else { + this._inheritedFrom = undefined; + this._inherited = true; } } } #singleValueUpdate(propertyName: string, value: string | number | boolean | null | undefined) { - if (!this._groupStructureHelper || !this.group) return; + if (!this._groupStructureHelper || !this._group) return; + + const ownerId = this._group.ownerId; + if (!ownerId) return; const partialObject = {} as any; partialObject[propertyName] = value; - this._groupStructureHelper.partialUpdateContainer(this.group.id, partialObject); + this._groupStructureHelper.partialUpdateContainer(ownerId, partialObject); } #renameGroup(e: InputEvent) { if (!this.groupStructureHelper || !this._group) return; + const ownerId = this._group.ownerId; + if (!ownerId) return; let newName = (e.target as HTMLInputElement).value; + // TODO: This does not seem right, the detection of a unique name requires better awareness on the level of the change. [NL] + // This seem to use check for root containers. const changedName = this.groupStructureHelper .getStructureManager()! - .makeContainerNameUniqueForOwnerContentType(this._group.id, newName, 'Group', this._group.parent?.id ?? null); + .makeContainerNameUniqueForOwnerContentType(ownerId, newName); if (changedName) { newName = changedName; } @@ -105,11 +104,11 @@ export class UmbContentTypeWorkspaceViewEditGroupElement extends UmbLitElement { #blurGroup(e: InputEvent) { if (!this.groupStructureHelper || !this._group) return; + const ownerId = this._group.ownerId; + if (!ownerId) return; const newName = (e.target as HTMLInputElement).value; if (newName === '') { - const changedName = this.groupStructureHelper - .getStructureManager()! - .makeEmptyContainerName(this._group.id, 'Group', this._group.parent?.id ?? null); + const changedName = this.groupStructureHelper.getStructureManager()!.makeEmptyContainerName(ownerId); this.#singleValueUpdate('name', changedName); (e.target as HTMLInputElement).value = changedName; } @@ -119,21 +118,22 @@ export class UmbContentTypeWorkspaceViewEditGroupElement extends UmbLitElement { e.preventDefault(); e.stopImmediatePropagation(); if (!this.groupStructureHelper || !this._group) return; + if (this._group.ownerId === undefined) return; // TODO: Do proper localization here: [NL] await umbConfirmModal(this, { headline: `${this.localize.term('actions_delete')} group`, content: html` - Are you sure you want to delete the group ${this._group.name ?? this._group.id} + Are you sure you want to delete the group ${this._group.name ?? this._group.ownerId}
`, confirmLabel: this.localize.term('actions_delete'), color: 'danger', }); - this.groupStructureHelper.removeContainer(this._group.id); + this.groupStructureHelper.removeContainer(this._group.ownerId); } override render() { @@ -277,6 +277,10 @@ export class UmbContentTypeWorkspaceViewEditGroupElement extends UmbLitElement { padding: var(--uui-size-space-4) var(--uui-size-space-5); } + :host([inherited]) div[slot='header'] { + cursor: default; + } + div[slot='header'] > div { display: flex; align-items: center; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-property.element.ts index 49adf60673d2..75b3f5b8c8e8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-property.element.ts @@ -456,6 +456,10 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement { height: min-content; } + #header i { + opacity: 0.55; + } + #editor { position: relative; --uui-button-background-color: var(--uui-color-background); diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-tab.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-tab.element.ts index 051f6caffce6..51eacf1c0a6e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-tab.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-tab.element.ts @@ -1,5 +1,9 @@ import { UMB_CONTENT_TYPE_WORKSPACE_CONTEXT } from '../../content-type-workspace.context-token.js'; -import type { UmbContentTypeModel, UmbPropertyTypeContainerModel } from '../../../types.js'; +import type { + UmbContentTypeModel, + UmbPropertyTypeContainerMergedModel, + UmbPropertyTypeContainerModel, +} from '../../../types.js'; import { UmbContentTypeContainerStructureHelper } from '../../../structure/index.js'; import { UMB_CONTENT_TYPE_DESIGN_EDITOR_CONTEXT } from './content-type-design-editor.context-token.js'; import type { UmbContentTypeWorkspaceViewEditGroupElement } from './content-type-design-editor-group.element.js'; @@ -13,95 +17,122 @@ import type { UmbSorterConfig } from '@umbraco-cms/backoffice/sorter'; import './content-type-design-editor-properties.element.js'; import './content-type-design-editor-group.element.js'; -const SORTER_CONFIG: UmbSorterConfig = { - getUniqueOfElement: (element) => element.group?.id, - getUniqueOfModel: (modelEntry) => modelEntry.id, - // TODO: Make specific to the current owner document. [NL] - identifier: 'content-type-container-sorter', - itemSelector: 'umb-content-type-design-editor-group', - handleSelector: '.drag-handle', - containerSelector: '.container-list', -}; +const SORTER_CONFIG: UmbSorterConfig = + { + getUniqueOfElement: (element) => element.group?.key, + getUniqueOfModel: (modelEntry) => modelEntry.key, + // TODO: Make specific to the current owner document. [NL] + identifier: 'content-type-container-sorter', + itemSelector: 'umb-content-type-design-editor-group', + handleSelector: '.drag-handle', + disabledItemSelector: '[inherited]', // Inherited attribute is set by the umb-content-type-design-editor-group. + containerSelector: '.container-list', + }; @customElement('umb-content-type-design-editor-tab') export class UmbContentTypeDesignEditorTabElement extends UmbLitElement { - #sorter = new UmbSorterController(this, { - ...SORTER_CONFIG, - onChange: ({ model }) => { - this._groups = model; - }, - onEnd: ({ item }) => { - /*if (this._inherited === undefined) { + #sorter = new UmbSorterController( + this, + { + ...SORTER_CONFIG, + onChange: ({ model }) => { + this._groups = model; + }, + onEnd: ({ item }) => { + /*if (this._inherited === undefined) { throw new Error('OwnerTabId is not set, we have not made a local duplicated of this container.'); return; }*/ - /** - * Explanation: If the item is the first in list, we compare it to the item behind it to set a sortOrder. - * If it's not the first in list, we will compare to the item in before it, and check the following item to see if it caused overlapping sortOrder, then update - * the overlap if true, which may cause another overlap, so we loop through them till no more overlaps... - */ - const model = this._groups; - const newIndex = model.findIndex((entry) => entry.id === item.id); - - // Doesn't exist in model - if (newIndex === -1) return; - - // As origin we set prev sort order to -1, so if no other then our item will become 0 - let prevSortOrder = -1; - - // Not first in list - if (newIndex > 0 && model.length > 0) { - prevSortOrder = model[newIndex - 1].sortOrder; - } - - // increase the prevSortOrder and use it for the moved item, - this.#groupStructureHelper.partialUpdateContainer(item.id, { - sortOrder: ++prevSortOrder, - }); - - // Adjust everyone right after, meaning until there is a gap between the sortOrders: - let i = newIndex + 1; - let entry: UmbPropertyTypeContainerModel | undefined; - // As long as there is an item with the index & the sortOrder is less or equal to the prevSortOrder, we will update the sortOrder: - while ((entry = model[i]) !== undefined && entry.sortOrder <= prevSortOrder) { - // Increase the prevSortOrder and use it for the item: - this.#groupStructureHelper.partialUpdateContainer(entry.id, { + /** + * Explanation: If the item is the first in list, we compare it to the item behind it to set a sortOrder. + * If it's not the first in list, we will compare to the item in before it, and check the following item to see if it caused overlapping sortOrder, then update + * the overlap if true, which may cause another overlap, so we loop through them till no more overlaps... + */ + const model = this._groups; + const newIndex = model.findIndex((entry) => entry.key === item.key); + + // Doesn't exist in model + if (newIndex === -1) return; + + // As origin we set prev sort order to -1, so if no other then our item will become 0 + let prevSortOrder = -1; + + // Not first in list + if (newIndex > 0 && model.length > 0) { + prevSortOrder = model[newIndex - 1].sortOrder; + } + + const ownerId = item.ownerId; + + if (ownerId === undefined) { + // This may be possible later, but for now this is not possible. [NL] + throw new Error( + 'OwnerId is not set for the given container, we cannot move containers that are not owned by the current Document.', + ); + } + + // increase the prevSortOrder and use it for the moved item, + this.#groupStructureHelper.partialUpdateContainer(ownerId, { sortOrder: ++prevSortOrder, }); - i++; - } - }, - onRequestDrop: async ({ unique }) => { - const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT); - if (!context) { - throw new Error('Could not get Workspace Context'); - } - return context.structure.getOwnerContainerById(unique); - }, - requestExternalRemove: async ({ item }) => { - const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT); - if (!context) { - throw new Error('Could not get Workspace Context'); - } - return await context.structure.removeContainer(null, item.id, { preventRemovingProperties: true }).then( - () => true, - () => false, - ); - }, - requestExternalInsert: async ({ item }) => { - const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT); - if (!context) { - throw new Error('Could not get Workspace Context'); - } - const parent = this.#containerId ? { id: this.#containerId } : null; - const updatedItem = { ...item, parent }; - return await context.structure.insertContainer(null, updatedItem).then( - () => true, - () => false, - ); + // Adjust everyone right after, meaning until there is a gap between the sortOrders: + let i = newIndex + 1; + let entry: UmbPropertyTypeContainerMergedModel | undefined; + // As long as there is an item with the index & the sortOrder is less or equal to the prevSortOrder, we will update the sortOrder: + while ((entry = model[i]) !== undefined && entry.sortOrder <= prevSortOrder) { + // Only updated owned containers: + if (entry.ownerId) { + // Increase the prevSortOrder and use it for the item: + this.#groupStructureHelper.partialUpdateContainer(entry.ownerId, { + sortOrder: ++prevSortOrder, + }); + + i++; + } + } + }, + onRequestDrop: async ({ unique }) => { + const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT); + if (!context) { + throw new Error('Could not get Workspace Context'); + } + return context.structure.getMergedContainerById(unique) as UmbPropertyTypeContainerMergedModel | undefined; + }, + requestExternalRemove: async ({ item }) => { + const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT); + if (!context) { + throw new Error('Could not get Workspace Context'); + } + return await context.structure.removeContainer(null, item.ownerId, { preventRemovingProperties: true }).then( + () => true, + () => false, + ); + }, + requestExternalInsert: async ({ item }) => { + const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT); + if (!context) { + throw new Error('Could not get Workspace Context'); + } + if (item.ownerId === undefined) { + // This may be possible later, but for now this is not possible. [NL] + throw new Error('OwnerId is not set, we cannot move containers that are not owned by the current Document.'); + } + const parent = this.#containerId ? { id: this.#containerId } : null; + const containerModelItem: UmbPropertyTypeContainerModel = { + id: item.ownerId, + name: item.name, + sortOrder: item.sortOrder, + type: item.type, + parent, + }; + return await context.structure.insertContainer(null, containerModelItem).then( + () => true, + () => false, + ); + }, }, - }); + ); #workspaceModal?: UmbModalRouteRegistrationController< typeof UMB_WORKSPACE_MODAL.DATA, @@ -122,7 +153,7 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement { } @state() - private _groups: Array = []; + private _groups: Array = []; @state() private _hasProperties = false; @@ -170,8 +201,9 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement { }); this.observe( - this.#groupStructureHelper.mergedContainers, + this.#groupStructureHelper.childContainers, (groups) => { + console.log('groups', groups); this._groups = groups; this.#sorter.setModel(this._groups); }, @@ -212,14 +244,14 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement {
${repeat( this._groups, - (group) => group.id, + (group) => group.key, (group) => html` `, diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts index d37eef19a0b9..7ca22cba7e7b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts @@ -2,7 +2,7 @@ import { UMB_CONTENT_TYPE_WORKSPACE_CONTEXT } from '../../content-type-workspace import type { UmbContentTypeCompositionModel, UmbContentTypeModel, - UmbPropertyTypeContainerModel, + UmbPropertyTypeContainerMergedModel, } from '../../../types.js'; import { UmbContentTypeContainerStructureHelper, @@ -33,9 +33,9 @@ import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; @customElement('umb-content-type-design-editor') export class UmbContentTypeDesignEditorElement extends UmbLitElement implements UmbWorkspaceViewElement { - #sorter = new UmbSorterController(this, { - getUniqueOfElement: (element) => element.getAttribute('data-umb-tab-id'), - getUniqueOfModel: (tab) => tab.id, + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => element.getAttribute('data-umb-tab-key'), + getUniqueOfModel: (tab) => tab.key, identifier: 'content-type-tabs-sorter', itemSelector: 'uui-tab', containerSelector: 'uui-tab-group', @@ -51,7 +51,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements * the overlap if true, which may cause another overlap, so we loop through them till no more overlaps... */ const model = this._tabs ?? []; - const newIndex = model.findIndex((entry) => entry.id === item.id); + const newIndex = model.findIndex((entry) => entry.key === item.key); // Doesn't exist in model if (newIndex === -1) return; @@ -64,20 +64,32 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements prevSortOrder = model[newIndex - 1].sortOrder; } + const ownerId = item.ownerId; + + if (ownerId === undefined) { + // This may be possible later, but for now this is not possible. [NL] + throw new Error( + 'OwnerId is not set for the given container, we cannot move containers that are not owned by the current Document.', + ); + } + // increase the prevSortOrder and use it for the moved item, - this.#tabsStructureHelper.partialUpdateContainer(item.id, { + this.#tabsStructureHelper.partialUpdateContainer(ownerId, { sortOrder: ++prevSortOrder, }); // Adjust everyone right after, until there is a gap between the sortOrders: [NL] let i = newIndex + 1; - let entry: UmbPropertyTypeContainerModel | undefined; + let entry: UmbPropertyTypeContainerMergedModel | undefined; // As long as there is an item with the index & the sortOrder is less or equal to the prevSortOrder, we will update the sortOrder: while ((entry = model[i]) !== undefined && entry.sortOrder <= prevSortOrder) { - // Increase the prevSortOrder and use it for the item: - this.#tabsStructureHelper.partialUpdateContainer(entry.id, { - sortOrder: ++prevSortOrder, - }); + // Only updated owned containers: + if (entry.ownerId) { + // Increase the prevSortOrder and use it for the item: + this.#tabsStructureHelper.partialUpdateContainer(entry.ownerId, { + sortOrder: ++prevSortOrder, + }); + } i++; } @@ -105,7 +117,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements private _routes: UmbRoute[] = []; @state() - private _tabs?: Array; + private _tabs?: Array; @state() private _routerPath?: string; @@ -136,7 +148,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements this.#tabsStructureHelper.setContainerChildType('Tab'); this.#tabsStructureHelper.setIsRoot(true); - this.observe(this.#tabsStructureHelper.mergedContainers, (tabs) => { + this.observe(this.#tabsStructureHelper.childContainers, (tabs) => { this._tabs = tabs; this.#sorter.setModel(tabs); this.#createRoutes(); @@ -180,7 +192,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements if (this._tabs.length > 0) { this._tabs?.forEach((tab) => { const tabName = tab.name && tab.name !== '' ? tab.name : '-'; - if (tab.id === this.#processingTabId) { + if (tab.ownerId && tab.ownerId === this.#processingTabId) { activeTabName = tabName; } routes.push({ @@ -188,7 +200,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements component: () => import('./content-type-design-editor-tab.element.js'), setup: (component) => { this.#currentTabComponent = component as UmbContentTypeDesignEditorTabElement; - this.#currentTabComponent.containerId = tab.id; + this.#currentTabComponent.containerId = tab.ownerId ?? tab.ids[0]; }, }); }); @@ -265,8 +277,8 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements } } - async #requestDeleteTab(tab: UmbPropertyTypeContainerModel | undefined) { - if (!tab) return; + async #requestDeleteTab(tab: UmbPropertyTypeContainerMergedModel | undefined) { + if (!tab || !tab.ownerId) return; // TODO: Localize this: const tabName = tab.name === '' ? 'Unnamed' : tab.name; // TODO: Localize this: @@ -288,10 +300,9 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements await umbConfirmModal(this, modalData); - this.#deleteTab(tab?.id); + this.#deleteTab(tab.ownerId); } - #deleteTab(tabId?: string) { - if (!tabId) return; + #deleteTab(tabId: string) { this.#workspaceContext?.structure.removeContainer(null, tabId); if (this.#processingTabId === tabId) { this.#processingTabId = undefined; @@ -332,12 +343,14 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements }, 100); } - async #tabNameChanged(event: InputEvent, tab: UmbPropertyTypeContainerModel) { - this.#processingTabId = tab.id; + async #tabNameChanged(event: InputEvent, tab: UmbPropertyTypeContainerMergedModel) { + if (!this.#workspaceContext || !tab.ownerId) return; + this.#processingTabId = tab.ownerId; + console.log('this.#processingTabId', this.#processingTabId); let newName = (event.target as HTMLInputElement).value; - const changedName = this.#workspaceContext?.structure.makeContainerNameUniqueForOwnerContentType( - tab.id, + const changedName = this.#workspaceContext.structure.makeContainerNameUniqueForOwnerContentType( + tab.ownerId, newName, 'Tab', ); @@ -349,20 +362,20 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements (event.target as HTMLInputElement).value = newName; } - this.#tabsStructureHelper.partialUpdateContainer(tab.id!, { + this.#tabsStructureHelper.partialUpdateContainer(tab.ownerId, { name: newName, }); } - async #tabNameBlur(event: FocusEvent, tab: UmbPropertyTypeContainerModel) { - if (!this.#processingTabId) return; + async #tabNameBlur(event: FocusEvent, tab: UmbPropertyTypeContainerMergedModel) { + if (!this.#processingTabId || !tab.ownerId) return; const newName = (event.target as HTMLInputElement | undefined)?.value; if (newName === '') { const changedName = this.#workspaceContext!.structure.makeEmptyContainerName(this.#processingTabId, 'Tab'); (event.target as HTMLInputElement).value = changedName; - this.#tabsStructureHelper.partialUpdateContainer(tab.id!, { + this.#tabsStructureHelper.partialUpdateContainer(tab.ownerId, { name: changedName, }); } @@ -506,7 +519,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements ${this.renderRootTab()} ${repeat( this._tabs, - (tab) => tab.id, + (tab) => tab.ownerId ?? tab.ids[0], (tab) => this.renderTab(tab), )} @@ -535,16 +548,17 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements `; } - renderTab(tab: UmbPropertyTypeContainerModel) { + renderTab(tab: UmbPropertyTypeContainerMergedModel) { const path = this._routerPath + '/tab/' + encodeFolderName(tab.name && tab.name !== '' ? tab.name : '-'); const tabActive = path === this._activePath; - const ownedTab = this.#tabsStructureHelper.isOwnerChildContainer(tab.id!) ?? false; + const ownedTab = tab.ownerId ? true : false; return html` this.#onDragOver(event, path)}> @@ -552,14 +566,15 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements `; } - renderTabInner(tab: UmbPropertyTypeContainerModel, tabActive: boolean, ownedTab: boolean) { + renderTabInner(tab: UmbPropertyTypeContainerMergedModel, tabActive: boolean, ownedTab: boolean) { // TODO: Localize this: const hasTabName = tab.name && tab.name !== ''; const tabName = hasTabName ? tab.name : 'Unnamed'; + const tabId = tab.ownerId ?? tab.ids[0]; if (this._sortModeActive) { return html`
${ownedTab - ? html` ${tabName} + ? html` ${tabName} (this); @state() - private _groups: Array = []; + private _groups: Array = []; @state() private _hasProperties = false; @@ -42,7 +42,7 @@ export class UmbContentWorkspaceViewEditTabElement extends UmbLitElement { workspaceContext?.structure as unknown as UmbContentTypeStructureManager, ); }); - this.observe(this.#groupStructureHelper.mergedContainers, (groups) => { + this.observe(this.#groupStructureHelper.childContainers, (groups) => { this._groups = groups; }); this.observe(this.#groupStructureHelper.hasProperties, (hasProperties) => { @@ -63,12 +63,12 @@ export class UmbContentWorkspaceViewEditTabElement extends UmbLitElement { : ''} ${repeat( this._groups, - (group) => group.id, + (group) => group.key, (group) => html` + .containerId=${group.ids[0]}> `, )} `; 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 c983d1f63afd..53444b737a9c 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 @@ -4,7 +4,7 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbContentTypeModel, UmbContentTypeStructureManager, - UmbPropertyTypeContainerModel, + UmbPropertyTypeContainerMergedModel, } from '@umbraco-cms/backoffice/content-type'; import { UmbContentTypeContainerStructureHelper, @@ -34,7 +34,7 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements private _routes: UmbRoute[] = []; @state() - private _tabs?: Array; + private _tabs?: Array; @state() private _routerPath?: string; @@ -64,7 +64,7 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements this._tabsStructureHelper.setIsRoot(true); this._tabsStructureHelper.setContainerChildType('Tab'); this.observe( - this._tabsStructureHelper.mergedContainers, + this._tabsStructureHelper.childContainers, (tabs) => { this._tabs = tabs; this.#createRoutes(); @@ -117,7 +117,7 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements path, component: () => import('./content-editor-tab.element.js'), setup: (component) => { - (component as UmbContentWorkspaceViewEditTabElement).containerId = tab.id; + (component as UmbContentWorkspaceViewEditTabElement).containerId = tab.ownerId ?? tab.ids[0]; }, }); this.#createViewContext(path); From ae465503e9d7e4785711c36bdb367e15724a7e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 21 Aug 2025 17:55:15 +0200 Subject: [PATCH 47/65] fix lint errors --- .../content-type-property-structure-helper.class.ts | 9 ++------- .../workspace-editor/workspace-view.context.ts | 1 - 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-property-structure-helper.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-property-structure-helper.class.ts index 24657b72667f..10237041bf44 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-property-structure-helper.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-property-structure-helper.class.ts @@ -1,13 +1,8 @@ -import type { - UmbContentTypeModel, - UmbPropertyContainerTypes, - UmbPropertyTypeContainerModel, - UmbPropertyTypeModel, -} from '../types.js'; +import type { UmbContentTypeModel, UmbPropertyTypeModel } from '../types.js'; import type { UmbContentTypeStructureManager } from './content-type-structure-manager.class.js'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbArrayState, mergeObservables } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; type UmbPropertyTypeUnique = UmbPropertyTypeModel['unique']; 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 f9db421c022d..a268bde27f80 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,6 +1,5 @@ import type { ManifestWorkspaceView } from '../../types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UmbViewContext } from '@umbraco-cms/backoffice/view'; export class UmbWorkspaceViewContext extends UmbViewContext { public readonly IS_WORKSPACE_VIEW_CONTEXT = true as const; From 34d6a1ad2d4c5ab81c45ec47d0749b405628f0e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 21 Aug 2025 17:58:31 +0200 Subject: [PATCH 48/65] fix missing import --- .../structure/content-type-structure-manager.class.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index 55dc2d04b39f..192a36204a66 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -13,7 +13,7 @@ import { } from '@umbraco-cms/backoffice/repository'; import { UmbId } from '@umbraco-cms/backoffice/id'; import type { UmbControllerHost, UmbController } from '@umbraco-cms/backoffice/controller-api'; -import type { MappingFunction } from '@umbraco-cms/backoffice/observable-api'; +import type { MappingFunction, Observable } from '@umbraco-cms/backoffice/observable-api'; import { UmbArrayState, partialUpdateFrozenArray, From 19a262ca36ada03d88d0b0147e59a075933f22e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 21 Aug 2025 18:14:18 +0200 Subject: [PATCH 49/65] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/packages/core/hint/context/hints.controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts index 0c9f9fd36d3a..6498039bbe06 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts @@ -244,6 +244,5 @@ export class UmbHintController< this.#parent = undefined; this.#hints.destroy(); - super.destroy(); } } From 240dc3634c52518b12e6c53451c39046b080c19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 21 Aug 2025 18:15:34 +0200 Subject: [PATCH 50/65] Update src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/packages/core/hint/context/hints.controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts index 0c9f9fd36d3a..6498039bbe06 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts @@ -244,6 +244,5 @@ export class UmbHintController< this.#parent = undefined; this.#hints.destroy(); - super.destroy(); } } From de7689d08e9cb558e9d2cb64e0ed12c54ac89e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 21 Aug 2025 18:15:44 +0200 Subject: [PATCH 51/65] Update src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../content/workspace/content-validation-to-hints.manager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts index 54fa389c58c5..ad2e33d0dc29 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts @@ -85,7 +85,6 @@ export class UmbContentValidationToHintsManager< weight: 1000, variantId, }); - console.log('add one for', hints.getAll()); this.#hintedMsgs.add(message.key); }); }); From b395af88be00816a3c674514016d18bf99ae5d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 21 Aug 2025 18:15:50 +0200 Subject: [PATCH 52/65] Update src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../content/workspace/content-validation-to-hints.manager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts index ad2e33d0dc29..9474dfd94618 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts @@ -90,7 +90,6 @@ export class UmbContentValidationToHintsManager< }); this.#hintedMsgs.forEach((key) => { if (!messages.some((msg) => msg.key === key)) { - console.log('remove one', key); this.#hintedMsgs.delete(key); hints.removeOne(key); } From 01885d0a409d8d2cd65c4ab76ec7727d01654be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 21 Aug 2025 18:24:43 +0200 Subject: [PATCH 53/65] clean up --- .../core/hint/context/hints.controller.ts | 65 +++++++++---------- .../workspace-editor.element.ts | 2 +- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts index 6498039bbe06..d655c10ae399 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts @@ -102,38 +102,35 @@ export class UmbHintController< this.#scaffold.update(scaffold as any); } }); - this.observe( - parent?.descendingHints(this.#viewAlias), - (hints) => { - if (!hints) { - // Parent properly lost, so lets assume the parent hints are empty: [NL] - hints = []; - } - this.initiateChange(); - if (this.#parentHints) { - // Remove the local messages that does not exist in the parent anymore: - const toRemove = this.#parentHints.filter((hint) => !hints.find((m) => m.unique === hint.unique)); - this.remove(toRemove.map((hint) => hint.unique)); - } - this.#parentHints = hints; - - hints.forEach((hint) => { - // Remove first entry of hint.path, if it matches viewAlias. - if (this.#viewAlias && hint.path[0] === this.#viewAlias) { - hint = { ...hint, path: hint.path.slice(1) }; - } - this.#hints.appendOne(hint as HintType); - }); - - this.finishChange(); - }, - 'observeParentHints', - ); - - this.observe(this.hints, this.#transferHints, 'observeLocalMessages'); - } - - #transferHints = (hints: Array) => { + this.observe(parent?.descendingHints(this.#viewAlias), this.#receiveHints, 'observeParentHints'); + this.observe(this.hints, this.#propagateHints, 'observeLocalMessages'); + } + + #receiveHints = (hints: UmbHint[] | undefined) => { + if (!hints) { + // Parent properly lost, so lets assume the parent hints are empty: [NL] + hints = []; + } + this.initiateChange(); + if (this.#parentHints) { + // Remove the local messages that does not exist in the parent anymore: + const toRemove = this.#parentHints.filter((hint) => !hints.find((m) => m.unique === hint.unique)); + this.remove(toRemove.map((hint) => hint.unique)); + } + this.#parentHints = hints; + + hints.forEach((hint) => { + // Remove first entry of hint.path, if it matches viewAlias. + if (this.#viewAlias && hint.path[0] === this.#viewAlias) { + hint = { ...hint, path: hint.path.slice(1) }; + } + this.#hints.appendOne(hint as HintType); + }); + + this.finishChange(); + }; + + #propagateHints = (hints: Array) => { if (!this.#parent) return; this.#parent!.initiateChange(); @@ -171,10 +168,6 @@ export class UmbHintController< * @returns {HintType['unique']} Unique value of the hint */ addOne(hint: IncomingHintType): string | symbol { - /** - * TODO: - * Works, but the Hint does not stay when navigating away from a variant and back... - */ const newHint = { ...this.#scaffold.getValue(), ...hint } as unknown as HintType; newHint.unique ??= Symbol(); newHint.weight ??= 0; 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 7f3b3240b5bd..499278f5d13d 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 @@ -112,7 +112,7 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { return { path: UMB_WORKSPACE_VIEW_PATH_PATTERN.generateLocal({ viewPathname: manifest.meta.pathname }), component: () => createExtensionElement(manifest), - setup: (component: any) => { + setup: (component?: any) => { if (component) { context.provideAt(component); component.manifest = manifest; From da63fc989ef7668ae87c2c28d92259280f6cbacc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 21 Aug 2025 19:12:12 +0200 Subject: [PATCH 54/65] remove console.log --- .../content/workspace/content-validation-to-hints.manager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts index 9474dfd94618..d9727ccb863b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts @@ -45,7 +45,6 @@ export class UmbContentValidationToHintsManager< super(host); this.observe(structure.contentTypeMergedContainers, (merged) => { - //console.log(merged); this.#containers = merged; }); From 982a811326f8a74ce52df0b0e1c2c4752d2c0406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 21 Aug 2025 21:50:18 +0200 Subject: [PATCH 55/65] declare new exports of core --- src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts b/src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts index 45570f2893fe..6df35d3cd1ad 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts @@ -32,6 +32,7 @@ export default defineConfig({ 'event/index': './event/index.ts', 'extension-registry/index': './extension-registry/index.ts', 'http-client/index': './http-client/index.ts', + 'hint/index': './hint/index.ts', 'icon-registry/index': './icon-registry/index.ts', 'id/index': './id/index.ts', 'lit-element/index': './lit-element/index.ts', @@ -62,6 +63,7 @@ export default defineConfig({ 'utils/index': './utils/index.ts', 'validation/index': './validation/index.ts', 'variant/index': './variant/index.ts', + 'view/index': './view/index.ts', 'workspace/index': './workspace/index.ts', manifests: 'manifests.ts', }, From f54af570b62e04cfee48236bc51e586ccc377881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 21 Aug 2025 22:12:53 +0200 Subject: [PATCH 56/65] Update src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-tab.element.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../views/design/content-type-design-editor-tab.element.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-tab.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-tab.element.ts index 51eacf1c0a6e..568e59fa9caa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-tab.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-tab.element.ts @@ -203,7 +203,6 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement { this.observe( this.#groupStructureHelper.childContainers, (groups) => { - console.log('groups', groups); this._groups = groups; this.#sorter.setModel(this._groups); }, From 59e49d46aa8582f24e586ac59ab8c2550ce62912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 21 Aug 2025 22:13:19 +0200 Subject: [PATCH 57/65] clean up --- .../workspace/views/design/content-type-design-editor.element.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts index 7ca22cba7e7b..9f632ab6fc3e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts @@ -346,7 +346,6 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements async #tabNameChanged(event: InputEvent, tab: UmbPropertyTypeContainerMergedModel) { if (!this.#workspaceContext || !tab.ownerId) return; this.#processingTabId = tab.ownerId; - console.log('this.#processingTabId', this.#processingTabId); let newName = (event.target as HTMLInputElement).value; const changedName = this.#workspaceContext.structure.makeContainerNameUniqueForOwnerContentType( From 752afb0543e6882c87ac59d1262daa1d0c6b0847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 22 Aug 2025 08:25:48 +0200 Subject: [PATCH 58/65] fix const export --- .../src/packages/core/hint/context/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/index.ts index 09ac45710dff..9523595c70ed 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/hint/context/index.ts @@ -1,2 +1,3 @@ -export * from './hints.controller.js'; +export * from './hint.context-token.js'; export * from './hints.context.js'; +export * from './hints.controller.js'; From 8c98c38d8641c35ffa9210b918f02f94507ea4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 22 Aug 2025 12:45:08 +0200 Subject: [PATCH 59/65] remove root from hints path --- .../content/workspace/content-validation-to-hints.manager.ts | 2 +- .../content/workspace/views/edit/content-editor.element.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts index d9727ccb863b..8dcd90539b4d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-validation-to-hints.manager.ts @@ -77,7 +77,7 @@ export class UmbContentValidationToHintsManager< hints.addOne({ unique: message.key, - path: ['Umb.WorkspaceView.Document.Edit', 'root', ...path], + path: ['Umb.WorkspaceView.Document.Edit', ...path], text: '!', /*label: message.body,*/ color: 'invalid', 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 c983d1f63afd..dce11b6601cd 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 @@ -151,9 +151,9 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements view.firstHintOfVariant, (hint) => { if (hint) { - this._hintMap.set(view.viewAlias, hint); + this._hintMap.set(viewAlias, hint); } else { - this._hintMap.delete(view.viewAlias); + this._hintMap.delete(viewAlias); } this.requestUpdate('_hintMap'); }, From a5a4a5aae71309eabfec054cd7d99ddfcc2a4abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 22 Aug 2025 12:56:46 +0200 Subject: [PATCH 60/65] also check for invariant --- .../src/packages/core/view/context/view.context.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 6c8a85bc126a..fdd124adcacb 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 @@ -34,7 +34,9 @@ export class UmbViewContext extends UmbControllerBase { }); this.firstHintOfVariant = mergeObservables([this.variantId, this.hints.hints], ([variantId, hints]) => { if (variantId) { - return hints.find((hint) => (hint.variantId ? hint.variantId.equal(variantId!) : true)); + return hints.find((hint) => + hint.variantId ? hint.variantId.equal(variantId!) || hint.variantId.isInvariant() : true, + ); } else { return hints[0]; } From 89af479773758576ddb421cb30396bdd8c571bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 25 Aug 2025 08:54:19 +0200 Subject: [PATCH 61/65] name more as legacy --- .../content-type-container-structure-helper.class.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-container-structure-helper.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-container-structure-helper.class.ts index 91f210fc55b3..9558578e3914 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-container-structure-helper.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-container-structure-helper.class.ts @@ -67,7 +67,7 @@ export class UmbContentTypeContainerStructureHelper | undefined) { @@ -182,7 +182,7 @@ export class UmbContentTypeContainerStructureHelper) { + #legacyFilterNonOwnerContainers(containers: Array) { return this.#ownerChildContainers.length > 0 ? containers.filter( (anyCon) => @@ -316,9 +316,9 @@ export class UmbContentTypeContainerStructureHelper) => { + #legacyPerformContainerMerge = (containers: Array) => { // Remove containers that matches with a owner container: - let merged = this.#filterNonOwnerContainers(containers); + let merged = this.#legacyFilterNonOwnerContainers(containers); // Remove containers of same name and type: // This only works cause we are dealing with a single level of containers in this Helper, if we had more levels we would need to be more clever about the parent as well. [NL] merged = merged.filter((x, i, cons) => i === cons.findIndex((y) => y.name === x.name && y.type === x.type)); From 6f923ab871cd0f9cce11caac065eedcb4eb275f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 28 Aug 2025 12:33:30 +0200 Subject: [PATCH 62/65] fix eslint --- .../block/workspace/block-workspace.context.ts | 2 +- .../content-type-structure-manager.class.ts | 16 ++++++++++++++-- .../core/utils/path/stored-path.function.ts | 2 +- .../workspace-editor/workspace-editor.element.ts | 4 +++- .../repository/sources/package.server.data.ts | 2 +- 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts index 29cb77d48915..7be599c5a20f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts @@ -3,6 +3,7 @@ import { UMB_BLOCK_ENTRIES_CONTEXT, UMB_BLOCK_ENTRY_CONTEXT, UMB_BLOCK_MANAGER_C import { UmbBlockWorkspaceEditorElement } from './block-workspace-editor.element.js'; import { UmbBlockElementManager } from './block-element-manager.js'; import type { UmbBlockWorkspaceOriginData } from './block-workspace.modal-token.js'; +import { UMB_BLOCK_WORKSPACE_VIEW_CONTENT, UMB_BLOCK_WORKSPACE_VIEW_SETTINGS } from './constants.js'; import { UmbSubmittableWorkspaceContextBase, type UmbRoutableWorkspaceContext, @@ -23,7 +24,6 @@ import { decodeFilePath, UmbReadOnlyVariantGuardManager } from '@umbraco-cms/bac import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui'; -import { UMB_BLOCK_WORKSPACE_VIEW_CONTENT, UMB_BLOCK_WORKSPACE_VIEW_SETTINGS } from './constants.js'; export type UmbBlockWorkspaceElementManagerNames = 'content' | 'settings'; export class UmbBlockWorkspaceContext diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index c7fd42f34e5c..6d97aaef57b1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -505,11 +505,21 @@ export class UmbContentTypeStructureManager< 'Unnamed' ); } + /** + * + * @param {string} containerId - The id of the container to make unique + * @param {string} newName - The new name to make unique + * @param {never} _legacyContainerType - do not use, has no effect. Is deprecated and will be removed in v.17 + * @param {never} _legacyParentId - do not use, has no effect. Is deprecated and will be removed in v.17 + * @returns + */ makeContainerNameUniqueForOwnerContentType( containerId: string, newName: string, - legacyContainerType?: UmbPropertyContainerTypes, - legacyParentId?: string | null, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _legacyContainerType?: UmbPropertyContainerTypes, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _legacyParentId?: string | null, ) { const container = this.getOwnerContainerById(containerId); if (!container) { @@ -982,6 +992,7 @@ export class UmbContentTypeStructureManager< * Find merged containers that match the provided container ids. * Notice if you can provide one or more ids matching the same container and it will still only return return the matching container once. * @param containerIds - An array of container ids to find merged containers for. + * @param id * @returns {UmbPropertyTypeContainerMergedModel | undefined} - The merged containers that match the provided container ids. */ getMergedContainerById(id: string): UmbPropertyTypeContainerMergedModel | undefined { @@ -993,6 +1004,7 @@ export class UmbContentTypeStructureManager< * Find merged child containers that are children of the provided parent container ids. * Notice this will find matching containers and include their child containers in this. * @param containerIds - An array of container ids to find merged child containers for. + * @param searchId * @param type - The type of the containers to find. * @returns {Observable} - An observable that emits the merged child containers that match the provided container ids. */ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/stored-path.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/stored-path.function.ts index 24d280c861b1..6f485bf99113 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/stored-path.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/stored-path.function.ts @@ -35,7 +35,7 @@ export function setStoredPath(path: string): void { * Redirect the user to the stored path or the base path if not available. * If the basePath matches the start of the stored path, the browser will replace the state instead of redirecting. * @param {string} basePath - The base path to redirect to if no stored path is available. - * @param {boolean} [force=false] - If true, will redirect using Location + * @param {boolean} force - If true, will redirect using Location */ export function redirectToStoredPath(basePath: string, force = false): void { const url = retrieveStoredPath(); 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 10866294c8d5..72276e6ac844 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 @@ -213,7 +213,9 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { } #renderRoutes() { - if (!this._routes || this._routes.length === 0 || !this._workspaceViews || this._workspaceViews.length === 0) return nothing; + if (!this._routes || this._routes.length === 0 || !this._workspaceViews || this._workspaceViews.length === 0) { + return nothing; + } return html` Date: Fri, 29 Aug 2025 12:28:57 +0200 Subject: [PATCH 63/65] fix container id setting --- .../views/edit/block-workspace-view-edit-tab.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-tab.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-tab.element.ts index a9165eabc1cf..986598b50c4f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-tab.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-tab.element.ts @@ -100,7 +100,7 @@ export class UmbBlockWorkspaceViewEditTabElement extends UmbLitElement { + .containerId=${group.ids[0]}> `; } From 3fde465e5f41b1235eb404e26c8a5b5d04c88cde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 29 Aug 2025 12:31:17 +0200 Subject: [PATCH 64/65] fix resetting inherited property --- .../views/design/content-type-design-editor-group.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-group.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-group.element.ts index 39c0e81d3851..68f83dac6852 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-group.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-group.element.ts @@ -68,7 +68,7 @@ export class UmbContentTypeWorkspaceViewEditGroupElement extends UmbLitElement { this._inherited = true; } else { this._inheritedFrom = undefined; - this._inherited = true; + this._inherited = false; } } } From c512bfa01350d27f98ddf8781c244ace75ae9b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 29 Aug 2025 19:36:45 +0200 Subject: [PATCH 65/65] fix re-rendering problem --- .../views/design/content-type-design-editor-tab.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-tab.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-tab.element.ts index 568e59fa9caa..34e8ddf56c7d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-tab.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-tab.element.ts @@ -243,7 +243,7 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement {
${repeat( this._groups, - (group) => group.key, + (group) => group.ids[0], (group) => html`