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-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 new file mode 100644 index 0000000000..8d5d680a67 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts @@ -0,0 +1,161 @@ +import { createExtensionApi } from '../functions/create-extension-api.function.js'; +import { createExtensionElement } from '../functions/create-extension-element.function.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'; +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 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} + * @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 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.#api = newApi; + 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.'); + } + + 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.'); + } + + 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; + } + // 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; + } +} 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. + } + }, + ); + }); + }); +}); 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; 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; + } +} 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..ff4a187d2a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-element-and-api-slot-element-base.ts @@ -0,0 +1,66 @@ +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; + + this.#extensionController = new UmbExtensionElementAndApiInitializer( + this, + umbExtensionsRegistry, + this._alias, + [this], + this.#extensionChanged, + this.getDefaultElementName(), + ); + this.#extensionController.properties = this.#props; + } + + #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 76135380e6..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-initializer-element-base.ts +++ /dev/null @@ -1,88 +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; - } - 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/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-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts index 5f39e7113c..13ff962fac 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 { this.#treeItemContext = instance; if (!this.#treeItemContext) return; 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..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'; }