diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api-by-alias.function.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api-by-alias.function.ts index d56027afa7..a45416e089 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api-by-alias.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api-by-alias.function.ts @@ -1,14 +1,30 @@ import type { UmbApi } from '../models/api.interface.js'; import { createExtensionApi } from './create-extension-api.function.js'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +/** + * + * @param {UmbControllerHost} host - The host to parse on as the host to the extension api. + * @param {string} alias - The alias of the extension api to create. + * @param {Array} constructorArgs - The constructor arguments to pass to the extension api, host will always be appended as the first argument, meaning these arguments will be second and so forth. + * @returns {ApiType} a class instance of the api provided via the manifest that matches the given alias. You have to type this via the generic `ApiType` parameter. + */ export async function createExtensionApiByAlias( host: UmbControllerHost, + alias: string, constructorArgs?: Array, -): Promise { - // Get Manifest: - const manifest = {}; +): Promise { + // Get manifest: + const manifest = umbExtensionsRegistry.getByAlias(alias); + if (!manifest) { + throw new Error(`Failed to get manifest by alias: ${alias}`); + } + const api = await createExtensionApi(host, manifest, constructorArgs); + if (!api) { + throw new Error(`Failed to create extension api from alias: ${alias}`); + } // Create extension: - return createExtensionApi(host, manifest, constructorArgs); + return api; } diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api-by-alias.test.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api-by-alias.test.ts new file mode 100644 index 0000000000..ac030a5082 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api-by-alias.test.ts @@ -0,0 +1,127 @@ +import { expect, fixture } from '@open-wc/testing'; +import type { ManifestApi } from '../types/index.js'; +import type { UmbApi } from '../models/api.interface.js'; +import { createExtensionApiByAlias } from './create-extension-api-by-alias.function.js'; +import { customElement, html } from '@umbraco-cms/backoffice/external/lit'; +import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; + +@customElement('umb-test-controller-host') +class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {} + +interface UmbExtensionApiBoolTestClass extends UmbApi { + isValidClassInstance(): boolean; +} +class UmbExtensionApiTrueTestClass implements UmbExtensionApiBoolTestClass { + isValidClassInstance() { + return true; + } + destroy() {} +} + +class UmbExtensionApiFalseTestClass implements UmbExtensionApiBoolTestClass { + isValidClassInstance() { + return false; + } + destroy() {} +} + +const jsModuleWithDefaultExport = { + default: UmbExtensionApiTrueTestClass, +}; + +const jsModuleWithApiExport = { + api: UmbExtensionApiTrueTestClass, +}; + +const jsModuleWithDefaultAndApiExport = { + default: UmbExtensionApiFalseTestClass, + api: UmbExtensionApiTrueTestClass, +}; + +describe('Create Extension Api By Alias Method', () => { + let hostElement: UmbTestControllerHostElement; + + beforeEach(async () => { + hostElement = await fixture(html``); + }); + + it('Returns `undefined` when manifest does not have any correct properties', (done) => { + const manifest: ManifestApi = { + type: 'my-test-type', + alias: 'Umb.Test.createManifestApi', + name: 'pretty name', + }; + umbExtensionsRegistry.register(manifest); + + createExtensionApiByAlias(hostElement, manifest.alias, []).then(() => { + umbExtensionsRegistry.unregister(manifest.alias); + done(new Error('Should not resolve')); + }); + + setTimeout(() => { + umbExtensionsRegistry.unregister(manifest.alias); + done(); + }, 10); + }); + + it('Handles when `api` property contains a class constructor', async () => { + const manifest: ManifestApi = { + type: 'my-test-type', + alias: 'Umb.Test.createManifestApi', + name: 'pretty name', + api: UmbExtensionApiTrueTestClass, + }; + umbExtensionsRegistry.register(manifest); + + const api = await createExtensionApiByAlias(hostElement, manifest.alias, []); + expect(api.isValidClassInstance()).to.be.true; + + umbExtensionsRegistry.unregister(manifest.alias); + }); + + it('Handles when `loader` has a default export', async () => { + const manifest: ManifestApi = { + type: 'my-test-type', + alias: 'Umb.Test.createManifestApi', + name: 'pretty name', + js: () => Promise.resolve(jsModuleWithDefaultExport), + }; + umbExtensionsRegistry.register(manifest); + + const api = await createExtensionApiByAlias(hostElement, manifest.alias, []); + expect(api.isValidClassInstance()).to.be.true; + + umbExtensionsRegistry.unregister(manifest.alias); + }); + + it('Handles when `loader` has a api export', async () => { + const manifest: ManifestApi = { + type: 'my-test-type', + alias: 'Umb.Test.createManifestApi', + name: 'pretty name', + js: () => Promise.resolve(jsModuleWithApiExport), + }; + umbExtensionsRegistry.register(manifest); + + const api = await createExtensionApiByAlias(hostElement, manifest.alias, []); + expect(api.isValidClassInstance()).to.be.true; + + umbExtensionsRegistry.unregister(manifest.alias); + }); + + it('Prioritizes api export from loader property', async () => { + const manifest: ManifestApi = { + type: 'my-test-type', + alias: 'Umb.Test.createManifestApi', + name: 'pretty name', + js: () => Promise.resolve(jsModuleWithDefaultAndApiExport), + }; + umbExtensionsRegistry.register(manifest); + + const api = await createExtensionApiByAlias(hostElement, manifest.alias, []); + expect(api.isValidClassInstance()).to.be.true; + + umbExtensionsRegistry.unregister(manifest.alias); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/index.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/index.ts index cf2758ba9f..9d1cf4cd89 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/index.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/index.ts @@ -1,3 +1,4 @@ +export * from './create-extension-api-by-alias.function.js'; export * from './create-extension-api.function.js'; export * from './create-extension-element-with-api.function.js'; export * from './create-extension-element.function.js'; diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/registry/extension.registry.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/registry/extension.registry.ts index 1459943d78..245ddf6667 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/registry/extension.registry.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/registry/extension.registry.ts @@ -248,8 +248,8 @@ export class UmbExtensionRegistry< } /** - * Get an observable that provides extensions matching the given alias. - * @param alias {string} - The alias of the extensions to get. + * Get an observable that provides an extension matching the given alias. + * @param alias {string} - The alias of the extension to get. * @returns {Observable} - An observable of the extension that matches the alias. */ byAlias(alias: string): Observable { @@ -267,6 +267,19 @@ export class UmbExtensionRegistry< ) as Observable; } + /** + * Get an extension that matches the given alias, this will not return an observable, it is a one of retrieval if it exists at the given point in time. + * @param alias {string} - The alias of the extension to get. + * @returns {} - The extension manifest that matches the alias. + */ + getByAlias(alias: string): T | undefined { + const ext = this._extensions.getValue().find((ext) => ext.alias === alias) as T | undefined; + if (ext?.kind) { + return this.#mergeExtensionWithKinds([ext, this._kinds.getValue()]); + } + return ext; + } + /** * Get an observable that provides extensions matching the given type and alias. * @param type {string} - The type of the extensions to get. diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.controller.ts index fdd5689913..0bf1596d9b 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.controller.ts @@ -13,8 +13,8 @@ export class UmbObserverController extends UmbObserver implement constructor( host: UmbControllerHost, source: Observable, - callback: ObserverCallback, - alias: UmbControllerAlias, + callback?: ObserverCallback, + alias?: UmbControllerAlias, ) { super(source, callback); this.#host = host; diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.ts index f9bee2b51b..19e86e2874 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.ts @@ -15,9 +15,11 @@ export class UmbObserver { #callback!: ObserverCallback; #subscription!: Subscription; - constructor(source: Observable, callback: ObserverCallback) { + constructor(source: Observable, callback?: ObserverCallback) { this.#source = source; - this.#subscription = source.subscribe(callback); + if (callback) { + this.#subscription = source.subscribe(callback); + } } /** @@ -63,9 +65,9 @@ export class UmbObserver { destroy(): void { if (this.#subscription) { this.#subscription.unsubscribe(); - (this.#source as any) = undefined; (this.#callback as any) = undefined; (this.#subscription as any) = undefined; } + (this.#source as any) = undefined; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.ts index e351478096..72b4b150cb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.ts @@ -1,53 +1,23 @@ import { UmbEntityActionBase } from '../../entity-action-base.js'; -import type { UmbEntityActionArgs } from '../../types.js'; import type { MetaEntityActionDeleteKind } from '@umbraco-cms/backoffice/extension-registry'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; -import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-api'; import type { UmbDetailRepository, UmbItemRepository } from '@umbraco-cms/backoffice/repository'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/event'; export class UmbDeleteEntityAction extends UmbEntityActionBase { // TODO: make base type for item and detail models - #itemRepository?: UmbItemRepository; - #detailRepository?: UmbDetailRepository; - #init: Promise; - - constructor(host: UmbControllerHost, args: UmbEntityActionArgs) { - super(host, args); - - // TODO: We should properly look into how we can simplify the one time usage of a extension api, as its a bit of overkill to take conditions/overwrites and observation of extensions into play here: [NL] - // But since this happens when we execute an action, it does most likely not hurt any users, but it is a bit of a overkill to do this for every action: [NL] - this.#init = Promise.all([ - new UmbExtensionApiInitializer( - this._host, - umbExtensionsRegistry, - this.args.meta.itemRepositoryAlias, - [this._host], - (permitted, ctrl) => { - this.#itemRepository = permitted ? (ctrl.api as UmbItemRepository) : undefined; - }, - ).asPromise(), - - new UmbExtensionApiInitializer( - this._host, - umbExtensionsRegistry, - this.args.meta.detailRepositoryAlias, - [this._host], - (permitted, ctrl) => { - this.#detailRepository = permitted ? (ctrl.api as UmbDetailRepository) : undefined; - }, - ).asPromise(), - ]); - } async execute() { if (!this.args.unique) throw new Error('Cannot delete an item without a unique identifier.'); - await this.#init; - const { data } = await this.#itemRepository!.requestItems([this.args.unique]); + const itemRepository = await createExtensionApiByAlias>( + this, + this.args.meta.itemRepositoryAlias, + ); + + const { data } = await itemRepository.requestItems([this.args.unique]); const item = data?.[0]; if (!item) throw new Error('Item not found.'); @@ -59,7 +29,11 @@ export class UmbDeleteEntityAction extends UmbEntityActionBase>( + this, + this.args.meta.detailRepositoryAlias, + ); + detailRepository.delete(this.args.unique); const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); const event = new UmbRequestReloadStructureForEntityEvent({ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/detail/detail-repository.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/detail/detail-repository.interface.ts index b6146921de..6dc9fe7003 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/detail/detail-repository.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/detail/detail-repository.interface.ts @@ -3,9 +3,10 @@ import type { UmbRepositoryResponse, UmbRepositoryResponseWithAsObservable, } from '../types.js'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; -export interface UmbDetailRepository { +export interface UmbDetailRepository extends UmbApi { createScaffold(preset?: Partial): Promise>; requestByUnique(unique: string): Promise>; byUnique(unique: string): Promise>;