Merge pull request #1345 from umbraco/feature/extension_initializer_for_api_and_element

Extension Initiliazer for Element And Api
This commit is contained in:
Niels Lyngsø
2024-03-02 20:29:51 +01:00
committed by GitHub
16 changed files with 653 additions and 123 deletions

View File

@@ -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 extends { manifest: any }> = ControllerType & {
manifest: Required<Pick<ControllerType, 'manifest'>>;
@@ -62,7 +63,7 @@ export abstract class UmbBaseExtensionsInitializer<
? this.#extensionRegistry.byTypes<ManifestType>(this.#type as string[])
: this.#extensionRegistry.byType<ManifestTypeName, ManifestType>(this.#type as ManifestTypeName);
if (this.#filter) {
source = source.pipe(map((extensions: Array<ManifestType>) => extensions.filter(this.#filter!)));
source = createObservablePart(source, (extensions: Array<ManifestType>) => 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) {

View File

@@ -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.
}

View File

@@ -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<ManifestType, any> = 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<ManifestType, ControllerType> {
#defaultElement?: string;
#component?: ExtensionElementInterface;
#api?: ExtensionApiInterface;
#constructorArguments?: Array<unknown>;
/**
* 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<string, any>}
* @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<string, unknown>;
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<ManifestCondition>,
alias: string,
constructorArguments: Array<unknown> | 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;
}
}

View File

@@ -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<HTMLElement, UmbTestApiController> {
type: 'test-type';
}
describe('UmbExtensionElementAndApiController', () => {
describe('Manifest without conditions', () => {
let hostElement: UmbControllerHostElement;
let extensionRegistry: UmbExtensionRegistry<TestManifest>;
let manifest: TestManifest;
beforeEach(async () => {
hostElement = await fixture(html`<umb-test-controller-host></umb-test-controller-host>`);
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<TestManifest>;
let manifest: TestManifest;
beforeEach(async () => {
hostElement = await fixture(html`<umb-test-controller-host></umb-test-controller-host>`);
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<TestManifest>;
let manifest: TestManifest;
beforeEach(async () => {
hostElement = await fixture(html`<umb-test-controller-host></umb-test-controller-host>`);
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<TestManifest>(
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<TestManifest>(
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<TestManifest>;
let manifest: TestManifest;
beforeEach(async () => {
hostElement = await fixture(html`<umb-test-controller-host></umb-test-controller-host>`);
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<TestManifest>(
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.
}
},
);
});
});
});

View File

@@ -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<ManifestType, any> = any,
ExtensionInterface extends ManifestElement = ManifestType extends ManifestElement ? ManifestType : never,
ExtensionElementInterface extends HTMLElement | undefined = ExtensionInterface['ELEMENT_TYPE'],
> extends UmbBaseExtensionInitializer<ManifestType, ControllerType> {
#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;

View File

@@ -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<ManifestTypes, ManifestTypeName>,
ControllerType extends
UmbExtensionElementAndApiInitializer<ManifestType> = UmbExtensionElementAndApiInitializer<ManifestType>,
MyPermittedControllerType extends ControllerType = PermittedControllerType<ControllerType>,
> extends UmbBaseExtensionsInitializer<
ManifestTypes,
ManifestTypeName,
ManifestType,
ControllerType,
MyPermittedControllerType
> {
//
#extensionRegistry;
#defaultElement?: string;
#constructorArgs: Array<unknown> | undefined;
#props?: Record<string, unknown>;
public get properties() {
return this.#props;
}
public set properties(props: Record<string, unknown> | undefined) {
this.#props = props;
this._extensions.forEach((controller) => {
controller.properties = props;
});
}
constructor(
host: UmbControllerHost,
extensionRegistry: UmbExtensionRegistry<ManifestTypes>,
type: ManifestTypeName | Array<ManifestTypeName>,
constructorArguments: Array<unknown> | undefined,
filter: undefined | null | ((manifest: ManifestType) => boolean),
onChange: (permittedManifests: Array<MyPermittedControllerType>) => 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<ManifestType>(
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;
}
}

View File

@@ -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';

View File

@@ -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<string, unknown> | 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<string, unknown> = {};
#extensionController?: UmbExtensionElementAndApiInitializer<ManifestType>;
@state()
_element: ManifestType['ELEMENT_TYPE'] | undefined;
abstract getExtensionType(): string;
abstract getDefaultElementName(): string;
#observeManifest() {
if (!this._alias) return;
this.#extensionController = new UmbExtensionElementAndApiInitializer<ManifestType>(
this,
umbExtensionsRegistry,
this._alias,
[this],
this.#extensionChanged,
this.getDefaultElementName(),
);
this.#extensionController.properties = this.#props;
}
#extensionChanged = (isPermitted: boolean, controller: UmbExtensionElementAndApiInitializer<ManifestType>) => {
this._element = isPermitted ? controller.component : undefined;
this.requestUpdate('_element');
};
render() {
return html`${this._element}`;
}
}

View File

@@ -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<string, unknown> | 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<string, unknown> = {};
#extensionElementController?: UmbExtensionElementInitializer<ManifestType>;
@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<ManifestType>(
this,
umbExtensionsRegistry,
manifest.alias,
this.#extensionChanged,
this.getDefaultElementName(),
);
extController.properties = this.#props;
this.#extensionElementController = extController;
}
#extensionChanged = (isPermitted: boolean, controller: UmbExtensionElementInitializer<ManifestType>) => {
this._element = isPermitted ? controller.component : undefined;
this.requestUpdate('_element');
};
render() {
return html`${this._element}`;
}
}

View File

@@ -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';

View File

@@ -65,19 +65,23 @@ export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModelBase>
/**
* 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;

View File

@@ -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<any> | Map<PropertyKey, unknown>): Promise<void> {
super.updated(_changedProperties);
await this.#init;

View File

@@ -85,12 +85,15 @@ export abstract class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemMod
* @param {ManifestCollection} manifest
* @memberof UmbCollectionContext
*/
// TODO: Revisit if this instead should be a getter/setter property because it might be set by extension initializer
public setManifest(manifest: ManifestTreeItem | undefined) {
public set manifest(manifest: ManifestTreeItem | undefined) {
if (this.#manifest === manifest) return;
this.#manifest = manifest;
}
public get manifest() {
return this.#manifest;
}
// TODO: Be aware that this method, could be removed and we can use the getter method instead [NL]
/**
* Returns the manifest.
* @return {ManifestCollection}

View File

@@ -51,6 +51,7 @@ export abstract class UmbTreeItemElementBase<TreeItemModelType extends UmbTreeIt
constructor() {
super();
// TODO: Notice this can be retrieve via a api property. [NL]
this.consumeContext(UMB_TREE_ITEM_CONTEXT, (instance) => {
this.#treeItemContext = instance;
if (!this.#treeItemContext) return;

View File

@@ -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<ManifestTreeItem> {
export class UmbTreeItemElement extends UmbExtensionElementAndApiSlotElementBase<ManifestTreeItem> {
_entityType?: string;
@property({ type: String, reflect: true })
get entityType() {
@@ -11,10 +15,10 @@ export class UmbTreeItemElement extends UmbExtensionInitializerElementBase<Manif
}
set entityType(newVal) {
this._entityType = newVal;
this.#observeManifest();
this.#observeEntityType();
}
#observeManifest() {
#observeEntityType() {
if (!this._entityType) return;
const filterByEntityType = (manifest: ManifestTreeItem) => {
@@ -23,15 +27,16 @@ export class UmbTreeItemElement extends UmbExtensionInitializerElementBase<Manif
};
this.observe(
umbExtensionsRegistry.byTypeAndFilter(this.getExtensionType(), filterByEntityType),
(manifests) => {
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',
);
}

View File

@@ -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<ManifestTree> {
export class UmbTreeElement extends UmbExtensionElementAndApiSlotElementBase<ManifestTree> {
getExtensionType() {
return 'tree';
}