From 7912e61e2918bcabb4f7c8d4fdc84f2f9d837cd7 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 27 Jun 2025 21:04:21 +0200 Subject: [PATCH 001/118] wip section menu expansion --- .../section-sidebar-menu.context.token.ts | 6 ++ .../context/section-sidebar-menu.context.ts | 12 +++ .../expansion-manager/expansion-manager.ts | 85 +++++++++++++++++++ .../expansion-manager/index.ts | 1 + .../expansion-manager/types.ts | 3 + .../section-sidebar-menu.element.ts | 17 ++++ .../section-main/section-main.element.ts | 5 +- .../packages/core/section/section.context.ts | 2 + .../tree-expansion-manager.ts | 6 +- .../tree-menu-item-default.element.ts | 37 +++++++- .../src/packages/core/tree/tree.element.ts | 7 ++ .../events/expansion-change.event.ts | 8 ++ .../core/utils/expansion/events/index.ts | 1 + .../packages/core/utils/expansion/index.ts | 1 + .../src/packages/core/utils/index.ts | 1 + .../umbraco-news-dashboard.element.ts | 55 +++++++++++- 16 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/context/section-sidebar-menu.context.token.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/context/section-sidebar-menu.context.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/expansion-manager/expansion-manager.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/expansion-manager/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/expansion-manager/types.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/utils/expansion/events/expansion-change.event.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/utils/expansion/events/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/utils/expansion/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/context/section-sidebar-menu.context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/context/section-sidebar-menu.context.token.ts new file mode 100644 index 000000000000..175ad9a9dbad --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/context/section-sidebar-menu.context.token.ts @@ -0,0 +1,6 @@ +import type { UmbSectionSidebarMenuContext } from './section-sidebar-menu.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_SECTION_SIDEBAR_MENU_CONTEXT = new UmbContextToken( + 'UmbSectionSidebarMenuContext', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/context/section-sidebar-menu.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/context/section-sidebar-menu.context.ts new file mode 100644 index 000000000000..e1e9e11fd64f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/context/section-sidebar-menu.context.ts @@ -0,0 +1,12 @@ +import { UmbMenuExpansionManager } from '../expansion-manager/index.js'; +import { UMB_SECTION_SIDEBAR_MENU_CONTEXT } from './section-sidebar-menu.context.token.js'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbSectionSidebarMenuContext extends UmbContextBase { + public readonly expansion = new UmbMenuExpansionManager(this); + + constructor(host: UmbControllerHost) { + super(host, UMB_SECTION_SIDEBAR_MENU_CONTEXT.toString()); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/expansion-manager/expansion-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/expansion-manager/expansion-manager.ts new file mode 100644 index 000000000000..4b69108d5f9c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/expansion-manager/expansion-manager.ts @@ -0,0 +1,85 @@ +import type { UmbMenuExpansionModel } from './types.js'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import { UmbArrayState, type Observable } from '@umbraco-cms/backoffice/observable-api'; + +/** + * Manages the expansion state of a tree + * @exports + * @class UmbTreeExpansionManager + * @augments {UmbControllerBase} + */ +export class UmbMenuExpansionManager extends UmbControllerBase { + #expansion = new UmbArrayState([], (x) => x.entityType + x.unique); + expansion = this.#expansion.asObservable(); + + /** + * Checks if an entity is expanded + * @param {UmbEntityModel} entity The entity to check + * @param {string} entity.entityType The entity type + * @param {string} entity.unique The unique key + * @returns {Observable} True if the entity is expanded + * @memberof UmbTreeExpansionManager + */ + isExpanded(entity: UmbEntityModel): Observable { + return this.#expansion.asObservablePart((entries) => + entries?.some((entry) => entry.entityType === entity.entityType && entry.unique === entity.unique), + ); + } + + /** + * Sets the expansion state + * @param {UmbMenuExpansionModel | undefined} expansion The expansion state + * @memberof UmbTreeExpansionManager + * @returns {void} + */ + setExpansion(expansion: UmbMenuExpansionModel): void { + this.#expansion.setValue(expansion); + } + + /** + * Gets the expansion state + * @memberof UmbTreeExpansionManager + * @returns {UmbMenuExpansionModel} The expansion state + */ + getExpansion(): UmbMenuExpansionModel { + return this.#expansion.getValue(); + } + + updateExpansion(expansion: UmbMenuExpansionModel): void { + this.#expansion.append(expansion); + } + + /** + * Opens a child tree item + * @param {UmbEntityModel} entity The entity to open + * @param {string} entity.entityType The entity type + * @param {string} entity.unique The unique key + * @memberof UmbTreeExpansionManager + * @returns {Promise} + */ + public async expandItem(entity: UmbEntityModel): Promise { + this.#expansion.appendOne(entity); + } + + /** + * Closes a child tree item + * @param {UmbEntityModel} entity The entity to close + * @param {string} entity.entityType The entity type + * @param {string} entity.unique The unique key + * @memberof UmbTreeExpansionManager + * @returns {Promise} + */ + public async collapseItem(entity: UmbEntityModel): Promise { + this.#expansion.filter((x) => x.entityType !== entity.entityType || x.unique !== entity.unique); + } + + /** + * Closes all child tree items + * @memberof UmbTreeExpansionManager + * @returns {Promise} + */ + public async collapseAll(): Promise { + this.#expansion.setValue([]); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/expansion-manager/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/expansion-manager/index.ts new file mode 100644 index 000000000000..84d2e28140e0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/expansion-manager/index.ts @@ -0,0 +1 @@ +export * from './expansion-manager.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/expansion-manager/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/expansion-manager/types.ts new file mode 100644 index 000000000000..f8e8127414f6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/expansion-manager/types.ts @@ -0,0 +1,3 @@ +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; + +export type UmbMenuExpansionModel = Array; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/section-sidebar-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/section-sidebar-menu.element.ts index 2c7c75e35120..f3ae02537f04 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/section-sidebar-menu.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu/section-sidebar-menu.element.ts @@ -5,6 +5,7 @@ import { css, html, customElement, property } from '@umbraco-cms/backoffice/exte import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_SECTION_SIDEBAR_MENU_CONTEXT } from './context/section-sidebar-menu.context.token.js'; // TODO: Move to separate file: const manifest: UmbExtensionManifestKind = { @@ -34,6 +35,22 @@ export class UmbSectionSidebarMenuElement< return html`

${this.localize.string(this.manifest?.meta?.label ?? '')}

`; } + #context?: typeof UMB_SECTION_SIDEBAR_MENU_CONTEXT.TYPE; + + constructor() { + super(); + this.consumeContext(UMB_SECTION_SIDEBAR_MENU_CONTEXT, (context) => { + this.#context = context; + this.#observeExpansion(); + }); + } + + #observeExpansion() { + this.observe(this.#context?.expansion.expansion, (items) => { + console.log('Expanded items:', items); + }); + } + override render() { return html` ${this.renderHeader()} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main/section-main.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main/section-main.element.ts index 144a2b27049c..ebae159f82fe 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main/section-main.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main/section-main.element.ts @@ -1,8 +1,9 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { css, html, LitElement, customElement } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @customElement('umb-section-main') -export class UmbSectionMainElement extends LitElement { +export class UmbSectionMainElement extends UmbLitElement { override render() { return html`
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/section.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section.context.ts index fbb626897fb3..5cd54f3fd96d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/section.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section.context.ts @@ -1,3 +1,4 @@ +import { UmbSectionSidebarMenuContext } from '../menu/section-sidebar-menu/context/section-sidebar-menu.context.js'; import type { ManifestSection } from './extensions/section.extension.js'; import { UmbStringState } from '@umbraco-cms/backoffice/observable-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; @@ -14,6 +15,7 @@ export class UmbSectionContext extends UmbContextBase { constructor(host: UmbControllerHost) { super(host, UMB_SECTION_CONTEXT); + new UmbSectionSidebarMenuContext(this); } public setManifest(manifest?: ManifestSection) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/tree-expansion-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/tree-expansion-manager.ts index a2144f8c1cc0..f19739d90516 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/tree-expansion-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/tree-expansion-manager.ts @@ -2,6 +2,7 @@ import type { UmbTreeExpansionModel } from './types.js'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; import { UmbArrayState, type Observable } from '@umbraco-cms/backoffice/observable-api'; +import { UmbExpansionChangeEvent } from '@umbraco-cms/backoffice/utils'; /** * Manages the expansion state of a tree @@ -10,7 +11,7 @@ import { UmbArrayState, type Observable } from '@umbraco-cms/backoffice/observab * @augments {UmbControllerBase} */ export class UmbTreeExpansionManager extends UmbControllerBase { - #expansion = new UmbArrayState([], (x) => x.unique); + #expansion = new UmbArrayState([], (x) => x.entityType + x.unique); expansion = this.#expansion.asObservable(); /** @@ -56,6 +57,7 @@ export class UmbTreeExpansionManager extends UmbControllerBase { */ public async expandItem(entity: UmbEntityModel): Promise { this.#expansion.appendOne(entity); + this.getHostElement().dispatchEvent(new UmbExpansionChangeEvent()); } /** @@ -68,6 +70,7 @@ export class UmbTreeExpansionManager extends UmbControllerBase { */ public async collapseItem(entity: UmbEntityModel): Promise { this.#expansion.filter((x) => x.entityType !== entity.entityType || x.unique !== entity.unique); + this.getHostElement().dispatchEvent(new UmbExpansionChangeEvent()); } /** @@ -77,5 +80,6 @@ export class UmbTreeExpansionManager extends UmbControllerBase { */ public async collapseAll(): Promise { this.#expansion.setValue([]); + this.getHostElement().dispatchEvent(new UmbExpansionChangeEvent()); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-menu-item-default/tree-menu-item-default.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-menu-item-default/tree-menu-item-default.element.ts index 93ce54fa6d57..8c443d9f5398 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-menu-item-default/tree-menu-item-default.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-menu-item-default/tree-menu-item-default.element.ts @@ -1,9 +1,13 @@ +import { UMB_SECTION_SIDEBAR_MENU_CONTEXT } from '../../menu/section-sidebar-menu/context/section-sidebar-menu.context.token.js'; import type { ManifestMenuItemTreeKind } from './types.js'; -import { html, nothing, customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import { html, nothing, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import type { UmbMenuItemElement } from '@umbraco-cms/backoffice/menu'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import type { UmbExpansionChangeEvent } from '@umbraco-cms/backoffice/utils'; +import type { UmbTreeElement } from '../tree.element.js'; // TODO: Move to separate file: const manifest: UmbExtensionManifestKind = { @@ -23,6 +27,33 @@ export class UmbMenuItemTreeDefaultElement extends UmbLitElement implements UmbM @property({ type: Object }) manifest?: ManifestMenuItemTreeKind; + @state() + _expansion: Array = []; + + #context?: typeof UMB_SECTION_SIDEBAR_MENU_CONTEXT.TYPE; + + constructor() { + super(); + // TODO: make another context abstraction UMB_MENU_CONTEXT + this.consumeContext(UMB_SECTION_SIDEBAR_MENU_CONTEXT, (context) => { + this.#context = context; + this.#observeExpansion(); + }); + } + + #observeExpansion() { + this.observe(this.#context?.expansion.expansion, (items) => { + this._expansion = items || []; + }); + } + + #onExpansionChange(event: UmbExpansionChangeEvent) { + event.stopPropagation(); + const target = event.target as UmbTreeElement; + const expansion = target.getExpansion(); + this.#context?.expansion.updateExpansion(expansion); + } + override render() { return this.manifest ? html` @@ -34,7 +65,9 @@ export class UmbMenuItemTreeDefaultElement extends UmbLitElement implements UmbM selectable: false, multiple: false, }, - }}> + expansion: this._expansion, + }} + @expansion-change=${this.#onExpansionChange}> ` : nothing; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree.element.ts index 889ab0849b0b..a091f5e73269 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree.element.ts @@ -18,6 +18,13 @@ export class UmbTreeElement extends UmbExtensionElementAndApiSlotElementBase = []; + + constructor() { + super(); + this.consumeContext(UMB_SECTION_SIDEBAR_MENU_CONTEXT, (context) => { + this.#context = context; + this.#observeExpansion(); + }); + } + + #onExpand() { + const item: UmbEntityModel = { + entityType: 'document', + unique: '315c76fd-2446-4a4d-9762-e002ff31a220', + }; + this.#context?.expansion.expandItem(item); + } + + #onCollapse(event: PointerEvent, item: UmbEntityModel) { + this.#context?.expansion.collapseItem(item); + } + + #onCollapseAll() { + this.#context?.expansion.collapseAll(); + } + + #observeExpansion() { + this.observe(this.#context?.expansion.expansion, (items) => { + this._expansion = items || []; + }); + } + override render() { return html`