Merge pull request #1345 from umbraco/feature/extension_initializer_for_api_and_element
Extension Initiliazer for Element And Api
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user