From 1c283ac6462e8b1208f611ed09ab4da4508fc767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 2 Mar 2024 10:59:34 +0100 Subject: [PATCH 01/11] take initiative out of action --- .../extension-initializer-element-base.ts | 12 ++++++++---- .../core/tree/tree-item/tree-item.element.ts | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-initializer-element-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-initializer-element-base.ts index 76135380e6..c1be770b0b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-initializer-element-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-initializer-element-base.ts @@ -16,20 +16,23 @@ export abstract class UmbExtensionInitializerElementBase< } set alias(newVal) { this._alias = newVal; - this.#observeManifest(); + //this.#observeManifest(); } @property({ type: Object, attribute: false }) get props() { - return this.#props; + //return this.#props; + return {}; } set props(newVal: Record | undefined) { // TODO, compare changes since last time. only reset the ones that changed. This might be better done by the controller is self: - this.#props = newVal; + /*this.#props = newVal; if (this.#extensionElementController) { this.#extensionElementController.properties = newVal; - } + }*/ } + + /* #props?: Record = {}; #extensionElementController?: UmbExtensionElementInitializer; @@ -85,4 +88,5 @@ export abstract class UmbExtensionInitializerElementBase< render() { return html`${this._element}`; } + */ } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item.element.ts index dd750b2ac8..cb059ed4f0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item.element.ts @@ -28,8 +28,8 @@ export class UmbTreeItemElement extends UmbExtensionInitializerElementBase Date: Sat, 2 Mar 2024 10:59:46 +0100 Subject: [PATCH 02/11] take initiative out of action --- .../extension-registry/extension-initializer-element-base.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-initializer-element-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-initializer-element-base.ts index c1be770b0b..fe3994d1d0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-initializer-element-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-initializer-element-base.ts @@ -1,8 +1,8 @@ -import { umbExtensionsRegistry } from './registry.js'; +//import { umbExtensionsRegistry } from './registry.js'; import { html, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; -import { UmbExtensionElementInitializer, createExtensionApi } from '@umbraco-cms/backoffice/extension-api'; +//import { UmbExtensionElementInitializer, createExtensionApi } from '@umbraco-cms/backoffice/extension-api'; // TODO: Eslint: allow abstract element class to end with "ElementBase" instead of "Element" // eslint-disable-next-line local-rules/enforce-element-suffix-on-element-class-name From a72ec1a14e535dc606e0e786a360d8cb1b2f97c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 2 Mar 2024 11:00:08 +0100 Subject: [PATCH 03/11] minor refactor --- .../controller/base-extensions-initializer.controller.ts | 7 ++----- .../extension-element-initializer.controller.ts | 8 +++++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extensions-initializer.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extensions-initializer.controller.ts index 083e4ebbbd..0769bb67c5 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extensions-initializer.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extensions-initializer.controller.ts @@ -7,6 +7,7 @@ import type { } from '@umbraco-cms/backoffice/extension-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { createObservablePart } from '@umbraco-cms/backoffice/observable-api'; export type PermittedControllerType = ControllerType & { manifest: Required>; @@ -62,7 +63,7 @@ export abstract class UmbBaseExtensionsInitializer< ? this.#extensionRegistry.byTypes(this.#type as string[]) : this.#extensionRegistry.byType(this.#type as ManifestTypeName); if (this.#filter) { - source = source.pipe(map((extensions: Array) => extensions.filter(this.#filter!))); + source = createObservablePart(source, (extensions: Array) => extensions.filter(this.#filter!)); } this.observe(source, this.#gotManifests, '_observeManifests') as any; } @@ -89,10 +90,6 @@ export abstract class UmbBaseExtensionsInitializer< return true; }); - // --------------------------------------------------------------- - // May change this into a Extensions Manager Controller??? - // --------------------------------------------------------------- - manifests.forEach((manifest) => { const existing = this._extensions.find((x) => x.alias === manifest.alias); if (!existing) { diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-initializer.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-initializer.controller.ts index f9dfedb7fc..0ef62a2c24 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-initializer.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-initializer.controller.ts @@ -1,6 +1,6 @@ import { createExtensionElement } from '../functions/create-extension-element.function.js'; import type { UmbExtensionRegistry } from '../registry/extension.registry.js'; -import type { ManifestCondition, ManifestWithDynamicConditions } from '../types/index.js'; +import type { ManifestCondition, ManifestElement, ManifestWithDynamicConditions } from '../types/index.js'; import { UmbBaseExtensionInitializer } from './base-extension-initializer.controller.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; @@ -18,9 +18,11 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbExtensionElementInitializer< ManifestType extends ManifestWithDynamicConditions = ManifestWithDynamicConditions, ControllerType extends UmbExtensionElementInitializer = any, + ExtensionInterface extends ManifestElement = ManifestType extends ManifestElement ? ManifestType : never, + ExtensionElementInterface extends HTMLElement | undefined = ExtensionInterface['ELEMENT_TYPE'], > extends UmbBaseExtensionInitializer { #defaultElement?: string; - #component?: HTMLElement; + #component?: ExtensionElementInterface; /** * The component that is created for this extension. @@ -84,7 +86,7 @@ export class UmbExtensionElementInitializer< // We are not positive anymore, so we will back out of this creation. return false; } - this.#component = newComponent; + this.#component = newComponent as ExtensionElementInterface; if (this.#component) { this.#assignProperties(); (this.#component as any).manifest = manifest; From cd295e2e4a8e95e1fbd984b60cf172813ed1b6ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 2 Mar 2024 11:00:23 +0100 Subject: [PATCH 04/11] initial work --- ...-element-and-api-initializer.controller.ts | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts new file mode 100644 index 0000000000..7a29da26d5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts @@ -0,0 +1,121 @@ +import { createExtensionElement } from '../functions/create-extension-element.function.js'; +import type { UmbApi } from '../index.js'; +import type { UmbExtensionRegistry } from '../registry/extension.registry.js'; +import type { ManifestElementAndApi, ManifestCondition, ManifestWithDynamicConditions } from '../types/index.js'; +import { UmbBaseExtensionInitializer } from './base-extension-initializer.controller.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +/** + * This Controller manages a single Extension initializing its Element and API. + * When the extension is permitted to be used, its Element and API will be instantiated and available for the consumer. + * + * @example + * ```ts + * const controller = new UmbExtensionApiAndElementInitializer(host, extensionRegistry, alias, (permitted, ctrl) => { console.log("Extension is permitted and this is the element: ", ctrl.component) })); + * ``` + * @export + * @class UmbExtensionElementAndApiInitializer + */ +export class UmbExtensionElementAndApiInitializer< + ManifestType extends ManifestWithDynamicConditions = ManifestWithDynamicConditions, + ControllerType extends UmbExtensionElementAndApiInitializer = any, + ExtensionInterface extends ManifestElementAndApi = ManifestType extends ManifestElementAndApi ? ManifestType : never, + ExtensionElementInterface extends HTMLElement | undefined = ExtensionInterface['ELEMENT_TYPE'], + ExtensionApiInterface extends UmbApi | undefined = ExtensionInterface['API_TYPE'], +> extends UmbBaseExtensionInitializer { + #defaultElement?: string; + #component?: ExtensionElementInterface; + #api?: ExtensionApiInterface; + #constructorArguments?: Array; + + /** + * The component that is created for this extension. + * @readonly + * @type {(HTMLElement | undefined)} + */ + public get component() { + return this.#component; + } + + /** + * The props that are passed to the component. + * @type {Record} + * @memberof UmbElementExtensionController + * @example + * ```ts + * const controller = new UmbElementExtensionController(host, extensionRegistry, alias, onPermissionChanged); + * controller.props = { foo: 'bar' }; + * ``` + * Is equivalent to: + * ```ts + * controller.component.foo = 'bar'; + * ``` + */ + #properties?: Record; + get properties() { + return this.#properties; + } + set properties(newVal) { + this.#properties = newVal; + // TODO: we could optimize this so we only re-set the changed props. + this.#assignProperties(); + } + + constructor( + host: UmbControllerHost, + extensionRegistry: UmbExtensionRegistry, + alias: string, + constructorArguments: Array | undefined, + onPermissionChanged: (isPermitted: boolean, controller: ControllerType) => void, + defaultElement?: string, + ) { + super(host, extensionRegistry, 'extApiAndElement_', alias, onPermissionChanged); + this.#constructorArguments = constructorArguments; + this.#defaultElement = defaultElement; + this._init(); + } + + #assignProperties = () => { + if (!this.#component || !this.#properties) return; + + // TODO: we could optimize this so we only re-set the updated props. + Object.keys(this.#properties).forEach((key) => { + (this.#component as any)[key] = this.#properties![key]; + }); + }; + + protected async _conditionsAreGood() { + const manifest = this.manifest!; // In this case we are sure its not undefined. + + const newComponent = await createExtensionElement(manifest, this.#defaultElement); + if (!this._isConditionsPositive) { + // We are not positive anymore, so we will back out of this creation. + return false; + } + this.#component = newComponent as ExtensionElementInterface; + if (this.#component) { + this.#assignProperties(); + (this.#component as any).manifest = manifest; + return true; // we will confirm we have a component and are still good to go. + } else { + console.warn('Manifest did not provide any useful data for a web component to be created.'); + } + + return false; // we will reject the state, we have no component, we are not good to be shown. + } + + protected async _conditionsAreBad() { + // Destroy the element: + if (this.#component) { + if ('destroy' in this.#component) { + (this.#component as unknown as { destroy: () => void }).destroy(); + } + this.#component = undefined; + } + } + + public destroy(): void { + super.destroy(); + this.#properties = undefined; + } +} From 9c6b21c46e798bdcae0a684ddad3c27fed4b60d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 2 Mar 2024 11:14:42 +0100 Subject: [PATCH 05/11] extension controller --- .../extension-api-initializer.controller.ts | 1 + ...-element-and-api-initializer.controller.ts | 44 +++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-api-initializer.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-api-initializer.controller.ts index 54ee212e20..873ad46c60 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-api-initializer.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-api-initializer.controller.ts @@ -98,6 +98,7 @@ export class UmbExtensionApiInitializer< this.#api = newApi; if (this.#api) { + (this.#api as any).manifest = manifest; //this.#assignProperties(); return true; // we will confirm we have a component and are still good to go. } diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts index 7a29da26d5..26467462a7 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts @@ -1,5 +1,5 @@ import { createExtensionElement } from '../functions/create-extension-element.function.js'; -import type { UmbApi } from '../index.js'; +import { createExtensionApi, type UmbApi } from '../index.js'; import type { UmbExtensionRegistry } from '../registry/extension.registry.js'; import type { ManifestElementAndApi, ManifestCondition, ManifestWithDynamicConditions } from '../types/index.js'; import { UmbBaseExtensionInitializer } from './base-extension-initializer.controller.js'; @@ -37,6 +37,15 @@ export class UmbExtensionElementAndApiInitializer< return this.#component; } + /** + * The api that is created for this extension. + * @readonly + * @type {(class | undefined)} + */ + public get api() { + return this.#api; + } + /** * The props that are passed to the component. * @type {Record} @@ -87,15 +96,36 @@ export class UmbExtensionElementAndApiInitializer< protected async _conditionsAreGood() { const manifest = this.manifest!; // In this case we are sure its not undefined. - const newComponent = await createExtensionElement(manifest, this.#defaultElement); + const promises = await Promise.all([ + createExtensionApi(manifest, this.#constructorArguments), + createExtensionElement(manifest, this.#defaultElement), + ]); + + const newApi = promises[0] as ExtensionApiInterface; + const newComponent = promises[1] as ExtensionElementInterface; + if (!this._isConditionsPositive) { + newApi?.destroy?.(); + if (newComponent && 'destroy' in newComponent) { + (newComponent as unknown as { destroy: () => void }).destroy(); + } // We are not positive anymore, so we will back out of this creation. return false; } - this.#component = newComponent as ExtensionElementInterface; + + this.#api = newApi; + if (!this.#api) { + (this.#api as any).manifest = manifest; + console.warn('Manifest did not provide any useful data for a api to be created.'); + } + + this.#component = newComponent; if (this.#component) { this.#assignProperties(); (this.#component as any).manifest = manifest; + if (this.#api) { + (this.#component as any).api = newApi; + } return true; // we will confirm we have a component and are still good to go. } else { console.warn('Manifest did not provide any useful data for a web component to be created.'); @@ -112,10 +142,18 @@ export class UmbExtensionElementAndApiInitializer< } this.#component = undefined; } + // Destroy the api: + if (this.#api) { + if ('destroy' in this.#api) { + (this.#api as unknown as { destroy: () => void }).destroy(); + } + this.#api = undefined; + } } public destroy(): void { super.destroy(); + this.#constructorArguments = undefined; this.#properties = undefined; } } From b395c73d86d95a160c970bfec4699887c43de983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 2 Mar 2024 11:27:52 +0100 Subject: [PATCH 06/11] extension controller tests --- ...ension-element-and-api-initializer.test.ts | 299 ++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.test.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.test.ts new file mode 100644 index 0000000000..977a8dbcd7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.test.ts @@ -0,0 +1,299 @@ +import { expect, fixture } from '@open-wc/testing'; +import { UmbExtensionRegistry } from '../registry/extension.registry.js'; +import type { ManifestElementAndApi, ManifestWithDynamicConditions } from '../index.js'; +import { UmbExtensionElementAndApiInitializer } from './extension-element-and-api-initializer.controller.js'; +import type { UmbControllerHost, UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; +import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api'; +import { customElement, html } from '@umbraco-cms/backoffice/external/lit'; +import { UmbSwitchCondition } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; + +@customElement('umb-test-controller-host') +class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {} + +class UmbTestApiController extends UmbControllerBase { + public i_am_test_api_controller = true; + + constructor(host: UmbControllerHost) { + super(host); + } +} + +interface TestManifest extends ManifestWithDynamicConditions, ManifestElementAndApi { + type: 'test-type'; +} + +describe('UmbExtensionElementAndApiController', () => { + describe('Manifest without conditions', () => { + let hostElement: UmbControllerHostElement; + let extensionRegistry: UmbExtensionRegistry; + let manifest: TestManifest; + + beforeEach(async () => { + hostElement = await fixture(html``); + extensionRegistry = new UmbExtensionRegistry(); + manifest = { + type: 'test-type', + name: 'test-type-1', + alias: 'Umb.Test.Type-1', + elementName: 'section', + api: UmbTestApiController, + }; + + extensionRegistry.register(manifest); + }); + + it('permits when there is no conditions', (done) => { + let called = false; + const extensionController = new UmbExtensionElementAndApiInitializer( + hostElement, + extensionRegistry, + 'Umb.Test.Type-1', + [hostElement], + (permitted) => { + if (called === false) { + called = true; + expect(permitted).to.be.true; + if (permitted) { + expect(extensionController?.manifest?.alias).to.eq('Umb.Test.Type-1'); + expect(extensionController.component?.nodeName).to.eq('SECTION'); + done(); + extensionController.destroy(); + } + } + }, + ); + }); + + it('utilized the default element when there is none provided by manifest', (done) => { + extensionRegistry.unregister(manifest.alias); + + const noElementManifest = { ...manifest, elementName: undefined }; + extensionRegistry.register(noElementManifest); + + let called = false; + const extensionController = new UmbExtensionElementAndApiInitializer( + hostElement, + extensionRegistry, + 'Umb.Test.Type-1', + [hostElement], + (permitted) => { + if (called === false) { + called = true; + expect(permitted).to.be.true; + if (permitted) { + expect(extensionController?.manifest?.alias).to.eq('Umb.Test.Type-1'); + expect(extensionController.component?.nodeName).to.eq('UMB-TEST-FALLBACK-ELEMENT'); + done(); + extensionController.destroy(); + } + } + }, + 'umb-test-fallback-element', + ); + }); + }); + + describe('Manifest with multiple conditions that changes over time', () => { + let hostElement: UmbControllerHostElement; + let extensionRegistry: UmbExtensionRegistry; + let manifest: TestManifest; + + beforeEach(async () => { + hostElement = await fixture(html``); + extensionRegistry = new UmbExtensionRegistry(); + + manifest = { + type: 'test-type', + name: 'test-type-1', + alias: 'Umb.Test.Type-1', + elementName: 'section', + api: UmbTestApiController, + conditions: [ + { + alias: 'Umb.Test.Condition.Delay', + frequency: '100', + } as any, + { + alias: 'Umb.Test.Condition.Delay', + frequency: '200', + } as any, + ], + }; + + // A ASCII timeline for the conditions, when allowed and then not allowed: + // Condition 0ms 100ms 200ms 300ms 400ms 500ms + // First condition: - + - + - + + // Second condition: - - + + - - + // Sum: - - - + - - + + const conditionManifest = { + type: 'condition', + name: 'test-condition-delay', + alias: 'Umb.Test.Condition.Delay', + api: UmbSwitchCondition, + }; + + extensionRegistry.register(manifest); + extensionRegistry.register(conditionManifest); + }); + + it('does change permission as conditions change', (done) => { + let count = 0; + const extensionController = new UmbExtensionElementAndApiInitializer( + hostElement, + extensionRegistry, + 'Umb.Test.Type-1', + [hostElement], + async () => { + count++; + // We want the controller callback to first fire when conditions are initialized. + expect(extensionController.manifest?.conditions?.length).to.be.equal(2); + expect(extensionController?.manifest?.alias).to.eq('Umb.Test.Type-1'); + if (count === 1) { + expect(extensionController?.permitted).to.be.true; + expect(extensionController.component?.nodeName).to.eq('SECTION'); + } else if (count === 2) { + expect(extensionController?.permitted).to.be.false; + expect(extensionController.component).to.be.undefined; + done(); + extensionController.destroy(); // need to destroy the controller. + } + }, + ); + }); + }); + + describe('Manifest without conditions', () => { + let hostElement: UmbControllerHostElement; + let extensionRegistry: UmbExtensionRegistry; + let manifest: TestManifest; + + beforeEach(async () => { + hostElement = await fixture(html``); + extensionRegistry = new UmbExtensionRegistry(); + manifest = { + type: 'test-type', + name: 'test-type-1', + alias: 'Umb.Test.Type-1', + elementName: 'section', + api: UmbTestApiController, + }; + + extensionRegistry.register(manifest); + }); + + it('permits when there is no conditions', (done) => { + let called = false; + const extensionController = new UmbExtensionElementAndApiInitializer( + hostElement, + extensionRegistry, + 'Umb.Test.Type-1', + [hostElement], + (permitted) => { + if (called === false) { + called = true; + expect(permitted).to.be.true; + if (permitted) { + expect(extensionController?.manifest?.alias).to.eq('Umb.Test.Type-1'); + expect(extensionController.api?.i_am_test_api_controller).to.be.true; + done(); + extensionController.destroy(); + } + } + }, + ); + /* + TODO: Consider if builder pattern would be a more nice way to setup this: + const extensionController = new UmbExtensionElementAndApiInitializer( + hostElement, + extensionRegistry, + 'Umb.Test.Type-1' + ) + .withConstructorArguments([hostElement]) + .onPermitted((permitted) => { + if (called === false) { + called = true; + expect(permitted).to.be.true; + if (permitted) { + expect(extensionController?.manifest?.alias).to.eq('Umb.Test.Type-1'); + expect(extensionController.api?.i_am_test_api_controller).to.be.true; + done(); + extensionController.destroy(); + } + } + ).observe(); + */ + }); + }); + + describe('Manifest with multiple conditions that changes over time', () => { + let hostElement: UmbControllerHostElement; + let extensionRegistry: UmbExtensionRegistry; + let manifest: TestManifest; + + beforeEach(async () => { + hostElement = await fixture(html``); + extensionRegistry = new UmbExtensionRegistry(); + + manifest = { + type: 'test-type', + name: 'test-type-1', + alias: 'Umb.Test.Type-1', + elementName: 'section', + api: UmbTestApiController, + conditions: [ + { + alias: 'Umb.Test.Condition.Delay', + frequency: '100', + } as any, + { + alias: 'Umb.Test.Condition.Delay', + frequency: '200', + } as any, + ], + }; + + // A ASCII timeline for the conditions, when allowed and then not allowed: + // Condition 0ms 100ms 200ms 300ms 400ms 500ms + // First condition: - + - + - + + // Second condition: - - + + - - + // Sum: - - - + - - + + const conditionManifest = { + type: 'condition', + name: 'test-condition-delay', + alias: 'Umb.Test.Condition.Delay', + api: UmbSwitchCondition, + }; + + extensionRegistry.register(manifest); + extensionRegistry.register(conditionManifest); + }); + + it('does change permission as conditions change', (done) => { + let count = 0; + const extensionController = new UmbExtensionElementAndApiInitializer( + hostElement, + extensionRegistry, + 'Umb.Test.Type-1', + [hostElement], + async () => { + count++; + // We want the controller callback to first fire when conditions are initialized. + expect(extensionController.manifest?.conditions?.length).to.be.equal(2); + expect(extensionController?.manifest?.alias).to.eq('Umb.Test.Type-1'); + if (count === 1) { + expect(extensionController?.permitted).to.be.true; + expect(extensionController.api?.i_am_test_api_controller).to.be.true; + } else if (count === 2) { + expect(extensionController?.permitted).to.be.false; + expect(extensionController.api).to.be.undefined; + done(); + extensionController.destroy(); // need to destroy the controller. + } + }, + ); + }); + }); +}); From ba5bc7d9e654e4d8633bb206099171a116f39c51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 2 Mar 2024 11:31:25 +0100 Subject: [PATCH 07/11] extensions controller --- ...-element-and-api-initializer.controller.ts | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extensions-element-and-api-initializer.controller.ts diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extensions-element-and-api-initializer.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extensions-element-and-api-initializer.controller.ts new file mode 100644 index 0000000000..fad332a3d9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extensions-element-and-api-initializer.controller.ts @@ -0,0 +1,81 @@ +import type { ManifestBase } from '../types/index.js'; +import type { UmbExtensionRegistry } from '../registry/extension.registry.js'; +import type { SpecificManifestTypeOrManifestBase } from '../types/map.types.js'; +import { UmbExtensionElementAndApiInitializer } from './extension-element-and-api-initializer.controller.js'; +import { + type PermittedControllerType, + UmbBaseExtensionsInitializer, +} from './base-extensions-initializer.controller.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +/** + */ +export class UmbExtensionsElementAndApiInitializer< + ManifestTypes extends ManifestBase = ManifestBase, + ManifestTypeName extends string = string, + ManifestType extends ManifestBase = SpecificManifestTypeOrManifestBase, + ControllerType extends + UmbExtensionElementAndApiInitializer = UmbExtensionElementAndApiInitializer, + MyPermittedControllerType extends ControllerType = PermittedControllerType, +> extends UmbBaseExtensionsInitializer< + ManifestTypes, + ManifestTypeName, + ManifestType, + ControllerType, + MyPermittedControllerType +> { + // + #extensionRegistry; + #defaultElement?: string; + #constructorArgs: Array | undefined; + #props?: Record; + + public get properties() { + return this.#props; + } + public set properties(props: Record | undefined) { + this.#props = props; + this._extensions.forEach((controller) => { + controller.properties = props; + }); + } + + constructor( + host: UmbControllerHost, + extensionRegistry: UmbExtensionRegistry, + type: ManifestTypeName | Array, + constructorArguments: Array | undefined, + filter: undefined | null | ((manifest: ManifestType) => boolean), + onChange: (permittedManifests: Array) => void, + controllerAlias?: string, + defaultElement?: string, + ) { + super(host, extensionRegistry, type, filter, onChange, controllerAlias); + this.#extensionRegistry = extensionRegistry; + this.#constructorArgs = constructorArguments; + this.#defaultElement = defaultElement; + this._init(); + } + + protected _createController(manifest: ManifestType) { + const extController = new UmbExtensionElementAndApiInitializer( + this, + this.#extensionRegistry, + manifest.alias, + this.#constructorArgs, + this._extensionChanged, + this.#defaultElement, + ) as ControllerType; + + extController.properties = this.#props; + + return extController; + } + + public destroy(): void { + super.destroy(); + this.#constructorArgs = undefined; + this.#props = undefined; + (this.#extensionRegistry as any) = undefined; + } +} From bae4ec073a06654595196bfe62bce389cdaf30c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 2 Mar 2024 11:56:31 +0100 Subject: [PATCH 08/11] UmbExtensionElementAndApiSlotElementBase+ refactors --- ...-element-and-api-initializer.controller.ts | 3 +- .../libs/extension-api/controller/index.ts | 2 + ...nsion-element-and-api-slot-element-base.ts | 65 +++++++++++++ .../extension-initializer-element-base.ts | 92 ------------------- .../packages/core/extension-registry/index.ts | 2 +- .../core/tree/default/default-tree.context.ts | 14 ++- .../core/tree/tree-item/tree-item.element.ts | 29 +++--- .../src/packages/core/tree/tree.element.ts | 4 +- 8 files changed, 98 insertions(+), 113 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-element-and-api-slot-element-base.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-initializer-element-base.ts diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts index 26467462a7..9ac994a5f1 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts @@ -1,5 +1,6 @@ +import { createExtensionApi } from '../functions/create-extension-api.function.js'; import { createExtensionElement } from '../functions/create-extension-element.function.js'; -import { createExtensionApi, type UmbApi } from '../index.js'; +import type { UmbApi } from '../models/api.interface.js'; import type { UmbExtensionRegistry } from '../registry/extension.registry.js'; import type { ManifestElementAndApi, ManifestCondition, ManifestWithDynamicConditions } from '../types/index.js'; import { UmbBaseExtensionInitializer } from './base-extension-initializer.controller.js'; diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/index.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/index.ts index db717874aa..a49d53fb99 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/index.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/index.ts @@ -6,3 +6,5 @@ export * from './extension-element-initializer.controller.js'; export * from './extensions-element-initializer.controller.js'; export * from './extension-manifest-initializer.controller.js'; export * from './extensions-manifest-initializer.controller.js'; +export * from './extension-element-and-api-initializer.controller.js'; +export * from './extensions-element-and-api-initializer.controller.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-element-and-api-slot-element-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-element-and-api-slot-element-base.ts new file mode 100644 index 0000000000..6f1a3001d6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-element-and-api-slot-element-base.ts @@ -0,0 +1,65 @@ +import { umbExtensionsRegistry } from './registry.js'; +import { html, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api'; +import { UmbExtensionElementAndApiInitializer } from '@umbraco-cms/backoffice/extension-api'; + +// TODO: Eslint: allow abstract element class to end with "ElementBase" instead of "Element" +// eslint-disable-next-line local-rules/enforce-element-suffix-on-element-class-name +export abstract class UmbExtensionElementAndApiSlotElementBase< + ManifestType extends ManifestElementAndApi, +> extends UmbLitElement { + _alias?: string; + @property({ type: String, reflect: true }) + get alias() { + return this._alias; + } + set alias(newVal) { + this._alias = newVal; + this.#observeManifest(); + } + + @property({ type: Object, attribute: false }) + set props(newVal: Record | undefined) { + // TODO, compare changes since last time. only reset the ones that changed. This might be better done by the controller is self: + this.#props = newVal; + if (this.#extensionController) { + this.#extensionController.properties = newVal; + } + } + get props() { + return this.#props; + } + + #props?: Record = {}; + + #extensionController?: UmbExtensionElementAndApiInitializer; + + @state() + _element: ManifestType['ELEMENT_TYPE'] | undefined; + + abstract getExtensionType(): string; + abstract getDefaultElementName(): string; + + #observeManifest() { + if (!this._alias) return; + + new UmbExtensionElementAndApiInitializer( + this, + umbExtensionsRegistry, + this._alias, + [this], + this.#extensionChanged, + this.getDefaultElementName(), + ); + } + + #extensionChanged = (isPermitted: boolean, controller: UmbExtensionElementAndApiInitializer) => { + this._element = isPermitted ? controller.component : undefined; + this.requestUpdate('_element'); + }; + + render() { + return html`${this._element}`; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-initializer-element-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-initializer-element-base.ts deleted file mode 100644 index fe3994d1d0..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-initializer-element-base.ts +++ /dev/null @@ -1,92 +0,0 @@ -//import { umbExtensionsRegistry } from './registry.js'; -import { html, property, state } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; -//import { UmbExtensionElementInitializer, createExtensionApi } from '@umbraco-cms/backoffice/extension-api'; - -// TODO: Eslint: allow abstract element class to end with "ElementBase" instead of "Element" -// eslint-disable-next-line local-rules/enforce-element-suffix-on-element-class-name -export abstract class UmbExtensionInitializerElementBase< - ManifestType extends ManifestWithDynamicConditions, -> extends UmbLitElement { - _alias?: string; - @property({ type: String, reflect: true }) - get alias() { - return this._alias; - } - set alias(newVal) { - this._alias = newVal; - //this.#observeManifest(); - } - - @property({ type: Object, attribute: false }) - get props() { - //return this.#props; - return {}; - } - set props(newVal: Record | undefined) { - // TODO, compare changes since last time. only reset the ones that changed. This might be better done by the controller is self: - /*this.#props = newVal; - if (this.#extensionElementController) { - this.#extensionElementController.properties = newVal; - }*/ - } - - /* - #props?: Record = {}; - - #extensionElementController?: UmbExtensionElementInitializer; - - @state() - _element: HTMLElement | undefined; - - abstract getExtensionType(): string; - abstract getDefaultElementName(): string; - - #observeManifest() { - if (!this._alias) return; - this.observe( - umbExtensionsRegistry.byTypeAndAlias(this.getExtensionType(), this._alias), - async (m) => { - if (!m) return; - const manifest = m as unknown as ManifestType; - this.createApi(manifest); - this.createElement(manifest); - }, - 'umbObserveTreeManifest', - ); - } - - protected async createApi(manifest?: ManifestType) { - if (!manifest) throw new Error('No manifest'); - const api = (await createExtensionApi(manifest, [this])) as unknown as any; - if (!api) throw new Error('No api'); - api.setManifest(manifest); - } - - protected async createElement(manifest?: ManifestType) { - if (!manifest) throw new Error('No manifest'); - - const extController = new UmbExtensionElementInitializer( - this, - umbExtensionsRegistry, - manifest.alias, - this.#extensionChanged, - this.getDefaultElementName(), - ); - - extController.properties = this.#props; - - this.#extensionElementController = extController; - } - - #extensionChanged = (isPermitted: boolean, controller: UmbExtensionElementInitializer) => { - this._element = isPermitted ? controller.component : undefined; - this.requestUpdate('_element'); - }; - - render() { - return html`${this._element}`; - } - */ -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/index.ts index 964c344673..e890b7fe55 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/index.ts @@ -3,4 +3,4 @@ export * from './interfaces/index.js'; export * from './models/index.js'; export * from './registry.js'; -export { UmbExtensionInitializerElementBase } from './extension-initializer-element-base.js'; +export { UmbExtensionElementAndApiSlotElementBase } from './extension-element-and-api-slot-element-base.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts index cd276f59d4..1d5eafa6e0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts @@ -65,19 +65,23 @@ export class UmbDefaultTreeContext /** * Sets the manifest - * @param {ManifestCollection} manifest - * @memberof UmbCollectionContext + * @param {ManifestTree} manifest + * @memberof UmbDefaultTreeContext */ - public setManifest(manifest: ManifestTree | undefined) { + public set manifest(manifest: ManifestTree | undefined) { if (this.#manifest === manifest) return; this.#manifest = manifest; this.#observeRepository(this.#manifest?.meta.repositoryAlias); } + public get manifest() { + return this.#manifest; + } + // TODO: getManifest, could be refactored to use the getter method [NL] /** * Returns the manifest. - * @return {ManifestCollection} - * @memberof UmbCollectionContext + * @return {ManifestTree} + * @memberof UmbDefaultTreeContext */ public getManifest() { return this.#manifest; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item.element.ts index cb059ed4f0..43ffdd96f4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item.element.ts @@ -1,9 +1,13 @@ import { customElement, property } from '@umbraco-cms/backoffice/external/lit'; import type { ManifestTreeItem } from '@umbraco-cms/backoffice/extension-registry'; -import { UmbExtensionInitializerElementBase, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { + UmbExtensionElementAndApiSlotElementBase, + umbExtensionsRegistry, +} from '@umbraco-cms/backoffice/extension-registry'; +import { createObservablePart } from '@umbraco-cms/backoffice/observable-api'; @customElement('umb-tree-item') -export class UmbTreeItemElement extends UmbExtensionInitializerElementBase { +export class UmbTreeItemElement extends UmbExtensionElementAndApiSlotElementBase { _entityType?: string; @property({ type: String, reflect: true }) get entityType() { @@ -11,10 +15,10 @@ export class UmbTreeItemElement extends UmbExtensionInitializerElementBase { @@ -23,15 +27,16 @@ export class UmbTreeItemElement extends UmbExtensionInitializerElementBase { - if (!manifests) return; - // TODO: what should we do if there are multiple tree items for an entity type? - const manifest = manifests[0]; - //this.createApi(manifest); - //this.createElement(manifest); + // TODO: what should we do if there are multiple tree items for an entity type? + // This method gets all extensions based on a type, then filters them based on the entity type. and then we get the alias of the first one [NL] + createObservablePart( + umbExtensionsRegistry.byTypeAndFilter(this.getExtensionType(), filterByEntityType), + (x) => x[0].alias, + ), + (alias) => { + this.alias = alias; }, - 'umbObserveTreeManifest', + 'umbObserveAlias', ); } 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 a009817be2..6eb0e6ee57 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 @@ -1,9 +1,9 @@ import { customElement } from '@umbraco-cms/backoffice/external/lit'; import type { ManifestTree } from '@umbraco-cms/backoffice/extension-registry'; -import { UmbExtensionInitializerElementBase } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbExtensionElementAndApiSlotElementBase } from '@umbraco-cms/backoffice/extension-registry'; @customElement('umb-tree') -export class UmbTreeElement extends UmbExtensionInitializerElementBase { +export class UmbTreeElement extends UmbExtensionElementAndApiSlotElementBase { getExtensionType() { return 'tree'; } From a96470036f7cfefe4b96f0c2d17012d172d6207b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 2 Mar 2024 11:58:03 +0100 Subject: [PATCH 09/11] manifest as getter/setter method --- .../tree-item/tree-item-base/tree-item-context-base.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts index 6498941ccd..63d722125a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts @@ -85,12 +85,15 @@ export abstract class UmbTreeItemContextBase Date: Sat, 2 Mar 2024 12:02:43 +0100 Subject: [PATCH 10/11] parse properties --- .../extension-element-and-api-slot-element-base.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-element-and-api-slot-element-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-element-and-api-slot-element-base.ts index 6f1a3001d6..ff4a187d2a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-element-and-api-slot-element-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-element-and-api-slot-element-base.ts @@ -44,7 +44,7 @@ export abstract class UmbExtensionElementAndApiSlotElementBase< #observeManifest() { if (!this._alias) return; - new UmbExtensionElementAndApiInitializer( + this.#extensionController = new UmbExtensionElementAndApiInitializer( this, umbExtensionsRegistry, this._alias, @@ -52,6 +52,7 @@ export abstract class UmbExtensionElementAndApiSlotElementBase< this.#extensionChanged, this.getDefaultElementName(), ); + this.#extensionController.properties = this.#props; } #extensionChanged = (isPermitted: boolean, controller: UmbExtensionElementAndApiInitializer) => { From f29dfed6420d2efce469d4118874021109617a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sat, 2 Mar 2024 12:17:53 +0100 Subject: [PATCH 11/11] final adjustments --- .../extension-element-and-api-initializer.controller.ts | 3 ++- .../src/packages/core/tree/default/default-tree.element.ts | 6 +----- .../tree/tree-item/tree-item-base/tree-item-element-base.ts | 1 + 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts index 9ac994a5f1..8d5d680a67 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts @@ -115,8 +115,9 @@ export class UmbExtensionElementAndApiInitializer< } this.#api = newApi; - if (!this.#api) { + if (this.#api) { (this.#api as any).manifest = manifest; + } else { console.warn('Manifest did not provide any useful data for a api to be created.'); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts index 4988ab4c42..07d1f88f76 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts @@ -44,6 +44,7 @@ export class UmbDefaultTreeElement extends UmbLitElement { super(); this.#init = Promise.all([ + // TODO: Notice this can be retrieve via a api property. [NL] this.consumeContext(UMB_DEFAULT_TREE_CONTEXT, (instance) => { this.#treeContext = instance; @@ -55,11 +56,6 @@ export class UmbDefaultTreeElement extends UmbLitElement { ]); } - connectedCallback(): void { - super.connectedCallback(); - this.#init; - } - protected async updated(_changedProperties: PropertyValueMap | Map): Promise { super.updated(_changedProperties); await this.#init; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts index e1407dd680..f29b8440e5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts @@ -51,6 +51,7 @@ export abstract class UmbTreeItemElementBase { this.#treeItemContext = instance; if (!this.#treeItemContext) return;