Merge branch 'main' into improvement-tree-repositories
This commit is contained in:
@@ -15,9 +15,9 @@ Here is the LIT documentation and playground: [https://lit.dev](https://lit.dev)
|
||||
|
||||
- Read the [README](README.md) to learn how to get the project up and running
|
||||
- Find an issue marked as [community/up-for-grabs](https://github.com/umbraco/Umbraco.CMS.Backoffice/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs) - note that some are also marked [good first issue](https://github.com/umbraco/Umbraco.CMS.Backoffice/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) which indicates they are simple to get started on
|
||||
- Umbraco HQ owns the Management API on the backend, so features can be worked on in the frontend only when there is an API, or otherwise, if no API is required
|
||||
- Umbraco HQ owns the Management API on the backend, so features can be worked on in the frontend only when there is an API, or otherwise if no API is required
|
||||
- A contribution should be made in a fork of the repository
|
||||
- Once a contribution is ready, a pull request should be made towards this repository and HQ will assign a reviewer
|
||||
- Once a contribution is ready, a pull request should be made to this repository and HQ will assign a reviewer
|
||||
- A pull request should always indicate what part of a feature it tries to solve, i.e. does it close the targeted issue (if any) or does the developer expect Umbraco HQ to take over
|
||||
|
||||
## Contributing in general terms
|
||||
@@ -115,7 +115,7 @@ To declare the Published Cache Status Dashboard as a new manifest, we need to ad
|
||||
alias: 'Umb.Dashboard.PublishedStatus',
|
||||
name: 'Published Status Dashboard',
|
||||
elementName: 'umb-dashboard-published-status',
|
||||
loader: () => import('./published-status/dashboard-published-status.element.js'),
|
||||
element: () => import('./published-status/dashboard-published-status.element.js'),
|
||||
weight: 200,
|
||||
meta: {
|
||||
label: 'Published Status',
|
||||
@@ -152,17 +152,17 @@ Let’s go through each of these properties…
|
||||
@customElement('umb-dashboard-published-status')
|
||||
```
|
||||
|
||||
- Loader: references a function call to import the file that the element is declared within
|
||||
- Js: references a function call to import the file that the element is declared within
|
||||
|
||||
- Weight: allows us to specify the order in which the dashboard will be displayed within the tabs bar
|
||||
|
||||
- Meta: allows us to reference additional data - in our case we can specify the label that is shown in the tabs bar and the pathname that will be displayed in the url
|
||||
- Meta: allows us to reference additional data - in our case, we can specify the label that is shown in the tabs bar and the pathname that will be displayed in the URL
|
||||
|
||||
- Conditions: allows us to specify the conditions that must be met in order for the dashboard to be displayed. In our case we are specifying that the dashboard will only be displayed within the Settings section
|
||||
- Conditions: allows us to specify the conditions that must be met for the dashboard to be displayed. In our case, we are specifying that the dashboard will only be displayed within the Settings section
|
||||
|
||||
## API mock handlers
|
||||
|
||||
Running the app with `npm run dev`, you will quickly notice the API requests turn into 404 errors. In order to hit the API, we need to add a mock handler to define the endpoints which our dashboard will call. In the case of the Published Cache Status section, we have a number of calls to work through. Let’s start by looking at the call to retrieve the current status of the cache:
|
||||
Running the app with `npm run dev`, you will quickly notice the API requests turn into 404 errors. To hit the API, we need to add a mock handler to define the endpoints that our dashboard will call. In the case of the Published Cache Status section, we have several calls to work through. Let’s start by looking at the call to retrieve the current status of the cache:
|
||||
|
||||

|
||||
|
||||
@@ -180,8 +180,8 @@ export const handlers = [
|
||||
// Respond with a 200 status code
|
||||
ctx.status(200),
|
||||
ctx.json<string>(
|
||||
'Database cache is ok. ContentStore contains 1 item and has 1 generation and 0 snapshot. MediaStore contains 5 items and has 1 generation and 0 snapshot.'
|
||||
)
|
||||
'Database cache is ok. ContentStore contains 1 item and has 1 generation and 0 snapshot. MediaStore contains 5 items and has 1 generation and 0 snapshot.',
|
||||
),
|
||||
);
|
||||
}),
|
||||
];
|
||||
@@ -195,7 +195,7 @@ An example `POST` is similar. Let’s take the “Refresh status” button as an
|
||||
|
||||

|
||||
|
||||
From our existing functionality we can see that this makes a `POST`call to the server to prompt a reload of the published cache. So we would add a new endpoint to the mock handler that would look like:
|
||||
From our existing functionality, we can see that this makes a `POST` call to the server to prompt a reload of the published cache. So we would add a new endpoint to the mock handler that would look like:
|
||||
|
||||
```typescript
|
||||
rest.post(umbracoPath('/published-cache/reload'), async (_req, res, ctx) => {
|
||||
@@ -216,7 +216,7 @@ This call returns a simple `OK` status code and no other object.
|
||||
|
||||
We try to make good Storybook stories for new components, which is a nice way to work with a component in an isolated state. Imagine you are working with a dialog on page 3 and have to navigate back to that every time you make a change - this is now eliminated with Storybook as you can just make a story that displays that step. Storybook can only show one component at a time, so it also helps us to isolate view logic into more and smaller components, which in turn are more testable.
|
||||
|
||||
In depth: [https://storybook.js.org/docs/web-components/get-started/introduction](https://storybook.js.org/docs/web-components/get-started/introduction)
|
||||
In-depth: [https://storybook.js.org/docs/web-components/get-started/introduction](https://storybook.js.org/docs/web-components/get-started/introduction)
|
||||
|
||||
Reference: [https://ambitious-stone-0033b3603.1.azurestaticapps.net/](https://ambitious-stone-0033b3603.1.azurestaticapps.net/)
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"backoffice",
|
||||
"Backoffice",
|
||||
"combobox",
|
||||
"ctrls",
|
||||
"devs",
|
||||
"Elementable",
|
||||
"invariantable",
|
||||
"lucide",
|
||||
|
||||
@@ -7,7 +7,7 @@ export const manifests: Array<ManifestExternalLoginProvider> = [
|
||||
alias: 'Umb.ExternalLoginProvider.Test',
|
||||
name: 'Test External Login Provider',
|
||||
elementName: 'umb-external-login-provider-test',
|
||||
loader: () => import('./external-login-provider-test.element.js'),
|
||||
element: () => import('./external-login-provider-test.element.js'),
|
||||
weight: 2,
|
||||
meta: {
|
||||
label: 'Test External Login Provider',
|
||||
@@ -19,7 +19,7 @@ export const manifests: Array<ManifestExternalLoginProvider> = [
|
||||
alias: 'Umb.ExternalLoginProvider.Test2',
|
||||
name: 'Test External Login Provider 2',
|
||||
elementName: 'umb-external-login-provider-test2',
|
||||
loader: () => import('./external-login-provider-test2.element.js'),
|
||||
element: () => import('./external-login-provider-test2.element.js'),
|
||||
weight: 1,
|
||||
meta: {
|
||||
label: 'Test External Login Provider 2',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
|
||||
@@ -7,14 +8,14 @@ import { UmbLitElement } from '@umbraco-cms/element';
|
||||
* @element {{ extensionTagName extensionType name }}
|
||||
*/
|
||||
@customElement('{{ extensionTagName extensionType name }}')
|
||||
export class {{className extensionType name }} extends UmbLitElement {
|
||||
export class {{className extensionType name }} extends UmbLitElement implements UmbPropertyEditorUiElement {
|
||||
static styles = [UmbTextStyles];
|
||||
|
||||
@property()
|
||||
value = '';
|
||||
|
||||
@property({ type: Array, attribute: false })
|
||||
public config = [];
|
||||
public config?: UmbPropertyEditorConfigCollection;
|
||||
|
||||
render() {
|
||||
return html`<div>{{ extensionTagName extensionType name }}</div>`;
|
||||
|
||||
31
src/Umbraco.Web.UI.Client/package-lock.json
generated
31
src/Umbraco.Web.UI.Client/package-lock.json
generated
@@ -76,9 +76,9 @@
|
||||
"rollup-plugin-web-worker-loader": "^1.6.1",
|
||||
"storybook": "7.5.3",
|
||||
"tiny-glob": "^0.2.9",
|
||||
"tsc-alias": "^1.8.7",
|
||||
"tsc-alias": "^1.8.8",
|
||||
"typescript": "^5.1.6",
|
||||
"typescript-json-schema": "^0.59.0",
|
||||
"typescript-json-schema": "^0.62.0",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-static-copy": "^0.17.0",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
@@ -20734,9 +20734,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tsc-alias": {
|
||||
"version": "1.8.7",
|
||||
"resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.7.tgz",
|
||||
"integrity": "sha512-59Q/zUQa3miTf99mLbSqaW0hi1jt4WoG8Uhe5hSZJHQpSoFW9eEwvW7jlKMHXWvT+zrzy3SN9PE/YBhQ+WVydA==",
|
||||
"version": "1.8.8",
|
||||
"resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.8.tgz",
|
||||
"integrity": "sha512-OYUOd2wl0H858NvABWr/BoSKNERw3N9GTi3rHPK8Iv4O1UyUXIrTTOAZNHsjlVpXFOhpJBVARI1s+rzwLivN3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^3.5.3",
|
||||
@@ -20909,9 +20909,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-json-schema": {
|
||||
"version": "0.59.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.59.0.tgz",
|
||||
"integrity": "sha512-eYB9RO8p4PntznWUukdDQHckNfxzjEFCJUgsWeCE43mcFioE0wXGTSECGk1uhty9XQMxkpuI4pKAqqnb62ln3Q==",
|
||||
"version": "0.62.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.62.0.tgz",
|
||||
"integrity": "sha512-qRO6pCgyjKJ230QYdOxDRpdQrBeeino4v5p2rYmSD72Jf4rD3O+cJcROv46sQukm46CLWoeusqvBgKpynEv25g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.9",
|
||||
@@ -20920,7 +20920,7 @@
|
||||
"path-equal": "^1.2.5",
|
||||
"safe-stable-stringify": "^2.2.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "~4.9.5",
|
||||
"typescript": "~5.1.0",
|
||||
"yargs": "^17.1.1"
|
||||
},
|
||||
"bin": {
|
||||
@@ -20947,19 +20947,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-json-schema/node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typical": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz",
|
||||
|
||||
@@ -194,9 +194,9 @@
|
||||
"rollup-plugin-web-worker-loader": "^1.6.1",
|
||||
"storybook": "7.5.3",
|
||||
"tiny-glob": "^0.2.9",
|
||||
"tsc-alias": "^1.8.7",
|
||||
"tsc-alias": "^1.8.8",
|
||||
"typescript": "^5.1.6",
|
||||
"typescript-json-schema": "^0.59.0",
|
||||
"typescript-json-schema": "^0.62.0",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-static-copy": "^0.17.0",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
|
||||
import { UmbBasicState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { UmbExtensionManifestInitializer, UmbExtensionsManifestController } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbExtensionManifestInitializer, UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { ManifestSection, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export class UmbBackofficeContext {
|
||||
@@ -13,7 +13,7 @@ export class UmbBackofficeContext {
|
||||
public readonly allowedSections = this.#allowedSections.asObservable();
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
new UmbExtensionsManifestController(host, umbExtensionsRegistry, 'section', null, (sections) => {
|
||||
new UmbExtensionsManifestInitializer(host, umbExtensionsRegistry, 'section', null, (sections) => {
|
||||
this.#allowedSections.next([...sections]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ import { UmbSectionContext, UMB_SECTION_CONTEXT_TOKEN } from '@umbraco-cms/backo
|
||||
import type { UmbRoute, UmbRouterSlotChangeEvent } from '@umbraco-cms/backoffice/router';
|
||||
import type { ManifestSection, UmbSectionElement } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import {
|
||||
UmbExtensionManifestInitializer,
|
||||
createExtensionElement,
|
||||
UmbExtensionManifestInitializer, createExtensionElement
|
||||
} from '@umbraco-cms/backoffice/extension-api';
|
||||
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
|
||||
|
||||
@@ -56,7 +55,7 @@ export class UmbBackofficeMainElement extends UmbLitElement {
|
||||
return {
|
||||
alias: section.alias,
|
||||
path: this._routePrefix + (section.manifest as ManifestSection).meta.pathname,
|
||||
component: () => createExtensionElement((section.manifest as ManifestSection), 'umb-section-default'),
|
||||
component: () => createExtensionElement(section.manifest!, 'umb-section-default'),
|
||||
setup: (component) => {
|
||||
(component as UmbSectionElement).manifest = section.manifest as ManifestSection;
|
||||
},
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { PackageResource, OpenAPI } from '@umbraco-cms/backoffice/backend-api';
|
||||
import { UmbBaseController, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbBaseController, type UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbBackofficeExtensionRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
|
||||
import { ManifestBase, isManifestJSType } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { ManifestBase, isManifestBaseType } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
// TODO: consider if this can be replaced by the new extension controllers
|
||||
// TODO: move local part out of this, and name something with server.
|
||||
export class UmbServerExtensionRegistrator extends UmbBaseController {
|
||||
#extensionRegistry: UmbBackofficeExtensionRegistry;
|
||||
#apiBaseUrl = OpenAPI.BASE;
|
||||
@@ -13,7 +12,6 @@ export class UmbServerExtensionRegistrator extends UmbBaseController {
|
||||
constructor(host: UmbControllerHost, extensionRegistry: UmbBackofficeExtensionRegistry) {
|
||||
super(host, UmbServerExtensionRegistrator.name);
|
||||
this.#extensionRegistry = extensionRegistry;
|
||||
// TODO: This was before in hostConnected(), but I don't see the reason to wait. lets just do it right away.
|
||||
this.#loadServerPackages();
|
||||
}
|
||||
|
||||
@@ -25,7 +23,7 @@ export class UmbServerExtensionRegistrator extends UmbBaseController {
|
||||
|
||||
This code is copy pasted from the package repository. We probably don't need this is the package repository anymore.
|
||||
*/
|
||||
const { data: packages } = await tryExecuteAndNotify(this._host, PackageResource.getPackageManifest());
|
||||
const { data: packages } = await tryExecuteAndNotify(this, PackageResource.getPackageManifest());
|
||||
|
||||
if (packages) {
|
||||
// Append packages to the store but only if they have a name
|
||||
@@ -36,16 +34,26 @@ export class UmbServerExtensionRegistrator extends UmbBaseController {
|
||||
p.extensions?.forEach((e) => {
|
||||
// Crudely validate that the extension at least follows a basic manifest structure
|
||||
// Idea: Use `Zod` to validate the manifest
|
||||
if (this.isManifestBase(e)) {
|
||||
if (isManifestBaseType(e)) {
|
||||
/**
|
||||
* Crude check to see if extension is of type "js" since it is safe to assume we do not
|
||||
* need to load any other types of extensions in the backoffice (we need a js file to load)
|
||||
*/
|
||||
if (isManifestJSType(e)) {
|
||||
// Add API base url if the js path is relative
|
||||
if (!e.js.startsWith('http')) {
|
||||
e.js = `${this.#apiBaseUrl}${e.js}`;
|
||||
}
|
||||
|
||||
// TODO: add helper to check for relative paths
|
||||
// Add base url if the js path is relative
|
||||
if ('js' in e && typeof e.js === 'string' && !e.js.startsWith('http')) {
|
||||
e.js = `${this.#apiBaseUrl}${e.js}`;
|
||||
}
|
||||
|
||||
// Add base url if the element path is relative
|
||||
if ('element' in e && typeof e.element === 'string' && !e.element.startsWith('http')) {
|
||||
e.element = `${this.#apiBaseUrl}${e.element}`;
|
||||
}
|
||||
|
||||
// Add base url if the element path api relative
|
||||
if ('api' in e && typeof e.api === 'string' && !e.api.startsWith('http')) {
|
||||
e.api = `${this.#apiBaseUrl}${e.api}`;
|
||||
}
|
||||
|
||||
extensions.push(e);
|
||||
@@ -56,8 +64,4 @@ export class UmbServerExtensionRegistrator extends UmbBaseController {
|
||||
this.#extensionRegistry.registerMany(extensions);
|
||||
}
|
||||
}
|
||||
|
||||
private isManifestBase(x: unknown): x is ManifestBase {
|
||||
return typeof x === 'object' && x !== null && 'alias' in x;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
describe('UmbInstallerContext', () => {
|
||||
// TODO: Write tests
|
||||
});
|
||||
@@ -1,15 +1,18 @@
|
||||
/* eslint local-rules/enforce-umbraco-external-imports: 0 */
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
import styles from 'monaco-editor/min/vs/editor/editor.main.css';
|
||||
//eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
|
||||
//eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker.js?worker';
|
||||
//eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker.js?worker';
|
||||
//eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker.js?worker';
|
||||
//eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker.js?worker';
|
||||
/* eslint-enable */
|
||||
|
||||
import { css, unsafeCSS } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
export const monacoEditorStyles = css`
|
||||
|
||||
@@ -14,7 +14,7 @@ export class UmbContextConsumerController<
|
||||
public get controllerAlias() {
|
||||
return this.#controllerAlias;
|
||||
}
|
||||
|
||||
|
||||
constructor(host: UmbControllerHost, contextAlias: string | UmbContextToken<BaseType, ResultType>, callback: UmbContextCallback<ResultType>) {
|
||||
super(host.getHostElement(), contextAlias, callback);
|
||||
this.#host = host;
|
||||
|
||||
@@ -41,27 +41,24 @@ ResultType extends BaseType = BaseType> {
|
||||
) {
|
||||
this.#contextAlias = contextAlias.toString();
|
||||
this.#callback = callback;
|
||||
this.#discriminator = (contextAlias as any).getDiscriminator?.();
|
||||
this.#discriminator = (contextAlias as UmbContextToken<BaseType, ResultType>).getDiscriminator?.();
|
||||
}
|
||||
|
||||
|
||||
/* Idea: Niels: If we need to filter for specific contexts, we could make the response method return true/false. If false, the event should then then not be stopped. Alternatively parse the event it self on to the response-callback.
|
||||
This will enable the event to continue to bubble up finding a context that matches.
|
||||
The reason for such would be to have some who are more specific than others. For example, some might just need the current workspace-context, others might need the closest handling a certain entityType.
|
||||
As I'm writing this is not relevant, but I wanted to keep the idea as we have had some circumstance that might be solved with this approach.
|
||||
*/
|
||||
protected _onResponse = (instance: BaseType) => {
|
||||
|
||||
protected _onResponse = (instance: BaseType): boolean => {
|
||||
if (this.#instance === instance) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if(this.#discriminator) {
|
||||
// Notice if discriminator returns false, we do not want to setInstance.
|
||||
if(this.#discriminator(instance)) {
|
||||
this.setInstance(instance as unknown as ResultType);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
this.setInstance(instance as ResultType);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
protected setInstance(instance: ResultType) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { UmbContextRequestEventImplementation, UmbContextRequestEvent } from './
|
||||
describe('UmbContextRequestEvent', () => {
|
||||
const contextRequestCallback = () => {
|
||||
console.log('hello from callback');
|
||||
return true;
|
||||
};
|
||||
|
||||
const event: UmbContextRequestEvent = new UmbContextRequestEventImplementation(
|
||||
|
||||
@@ -11,7 +11,7 @@ export type UmbContextCallback<T> = (instance: T) => void;
|
||||
*/
|
||||
export interface UmbContextRequestEvent<ResultType = unknown> extends Event {
|
||||
readonly contextAlias: string | UmbContextToken<unknown, ResultType>;
|
||||
readonly callback: UmbContextCallback<ResultType>;
|
||||
readonly callback: (context: ResultType) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,16 +23,12 @@ export interface UmbContextRequestEvent<ResultType = unknown> extends Event {
|
||||
export class UmbContextRequestEventImplementation<ResultType = unknown> extends Event implements UmbContextRequestEvent<ResultType> {
|
||||
public constructor(
|
||||
public readonly contextAlias: string | UmbContextToken<any, ResultType>,
|
||||
public readonly callback: UmbContextCallback<ResultType>
|
||||
public readonly callback: (context: ResultType) => boolean
|
||||
) {
|
||||
super(umbContextRequestEventType, { bubbles: true, composed: true, cancelable: true });
|
||||
}
|
||||
}
|
||||
|
||||
export const isUmbContextRequestEvent = (event: Event): event is UmbContextRequestEventImplementation => {
|
||||
return event.type === umbContextRequestEventType;
|
||||
};
|
||||
|
||||
export class UmbContextDebugRequest extends Event {
|
||||
public constructor(public readonly callback: any) {
|
||||
super(umbDebugContextEventType, { bubbles: true, composed: true, cancelable: false });
|
||||
|
||||
@@ -26,8 +26,8 @@ describe('UmbContextProviderController', () => {
|
||||
it('has a controllerAlias property', () => {
|
||||
expect(provider).to.have.property('controllerAlias');
|
||||
});
|
||||
it('has a controllerAlias property, is equal to the controllerAlias', () => {
|
||||
expect(provider.controllerAlias).to.eq('my-test-context');
|
||||
it('has a controllerAlias property, is equal to the controllerAlias plus instance name', () => {
|
||||
expect(provider.controllerAlias).to.eq('my-test-context' + '_' + instance.constructor.name);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,14 +8,18 @@ export class UmbContextProviderController<
|
||||
InstanceType extends ResultType = ResultType
|
||||
> extends UmbContextProvider<BaseType, ResultType> implements UmbController {
|
||||
#host: UmbControllerHost;
|
||||
#controllerAlias:string;
|
||||
|
||||
public get controllerAlias() {
|
||||
return this._contextAlias.toString();
|
||||
return this.#controllerAlias;
|
||||
}
|
||||
|
||||
constructor(host: UmbControllerHost, contextAlias: string | UmbContextToken<BaseType, ResultType>, instance: InstanceType) {
|
||||
super(host.getHostElement(), contextAlias, instance);
|
||||
this.#host = host;
|
||||
// Makes the controllerAlias unique for this instance, this enables multiple Contexts to be provided under the same name. (This only makes sense cause of Context Token Discriminators)
|
||||
// This does mean that if someone provides a context with the same name, but with a different instance, it will not override the previous instance. But its good since it enables extensions to provide contexts at the same scope of other contexts.
|
||||
this.#controllerAlias = contextAlias.toString() + '_' + (instance as any).constructor?.name;
|
||||
|
||||
// If this API is already provided with this alias? Then we do not want to register this controller:
|
||||
const existingControllers = host.getControllers((x) => x.controllerAlias === this.controllerAlias);
|
||||
@@ -23,9 +27,10 @@ export class UmbContextProviderController<
|
||||
existingControllers.length > 0 &&
|
||||
(existingControllers[0] as UmbContextProviderController).providerInstance?.() === instance
|
||||
) {
|
||||
// This just an additional awareness feature to make devs Aware, the alternative would be adding it anyway, but that would destroy existing controller of this alias.
|
||||
// Back out, this instance is already provided, by another controller.
|
||||
throw new Error(
|
||||
`Context API: The context of '${this.controllerAlias}' is already provided with the same API by another Context Provider Controller.`
|
||||
`Context API: The context of '${this.controllerAlias}' and instance '${(instance as any).constructor?.name ?? 'unnamed'}' is already provided by another Context Provider Controller.`
|
||||
);
|
||||
} else {
|
||||
host.addController(this);
|
||||
|
||||
@@ -10,7 +10,7 @@ export class UmbTestContextElement extends UmbControllerHostElementMixin(HTMLEle
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
new UmbContextConsumerController<string>(this, 'test-context', (value) => {
|
||||
new UmbContextConsumerController<string, string>(this, 'test-context', (value) => {
|
||||
this.value = value;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ describe('UmbContextProvider', () => {
|
||||
(_instance: UmbTestContextProviderClass) => {
|
||||
expect(_instance.prop).to.eq('value from provider');
|
||||
done();
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
UmbContextRequestEvent,
|
||||
umbContextRequestEventType,
|
||||
isUmbContextRequestEvent,
|
||||
umbDebugContextEventType,
|
||||
} from '../consume/context-request.event.js';
|
||||
import { UmbContextToken } from '../token/context-token.js';
|
||||
@@ -46,13 +46,17 @@ export class UmbContextProvider<BaseType = unknown, ResultType extends BaseType
|
||||
* @param {UmbContextRequestEvent} event
|
||||
* @memberof UmbContextProvider
|
||||
*/
|
||||
#handleContextRequest = (event: Event) => {
|
||||
if (!isUmbContextRequestEvent(event)) return;
|
||||
#handleContextRequest = ((event: UmbContextRequestEvent) => {
|
||||
if (event.contextAlias !== this._contextAlias) return;
|
||||
|
||||
// Since the alias matches, we will stop it from bubbling further up. But we still allow it to ask the other Contexts of the element. Hence not calling `event.stopImmediatePropagation();`
|
||||
event.stopPropagation();
|
||||
event.callback(this.#instance);
|
||||
};
|
||||
|
||||
if(event.callback(this.#instance)) {
|
||||
// Make sure the event not hits any more Contexts as we have found a match.
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
}) as EventListener;
|
||||
|
||||
/**
|
||||
* @memberof UmbContextProvider
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ClassConstructor } from '../extension-api/types.js';
|
||||
import { ClassConstructor } from '../extension-api/types/utils.js';
|
||||
import { UmbControllerHost } from './controller-host.interface.js';
|
||||
import type { UmbController } from './controller.interface.js';
|
||||
|
||||
@@ -21,7 +21,7 @@ declare class UmbControllerHostBaseDeclaration implements Omit<UmbControllerHost
|
||||
* @param {Object} superClass - superclass to be extended.
|
||||
* @mixin
|
||||
*/
|
||||
export const UmbControllerHostBaseMixin = <T extends ClassConstructor<any>>(superClass: T) => {
|
||||
export const UmbControllerHostBaseMixin = <T extends ClassConstructor>(superClass: T) => {
|
||||
class UmbControllerHostBaseClass extends superClass {
|
||||
#controllers: UmbController[] = [];
|
||||
|
||||
@@ -59,8 +59,12 @@ export const UmbControllerHostBaseMixin = <T extends ClassConstructor<any>>(supe
|
||||
this.#controllers.push(ctrl);
|
||||
if (this.#attached) {
|
||||
// If a controller is created on a already attached element, then it will be added directly. This might not be optimal. As the controller it self has not finished its constructor method jet. therefor i postpone the call:
|
||||
Promise.resolve().then(() => ctrl.hostConnected());
|
||||
//ctrl.hostConnected();
|
||||
Promise.resolve().then(() => {
|
||||
// Extra check to see if we are still attached at this point:
|
||||
if (this.#attached) {
|
||||
ctrl.hostConnected();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,16 +101,22 @@ export const UmbControllerHostBaseMixin = <T extends ClassConstructor<any>>(supe
|
||||
|
||||
hostConnected() {
|
||||
this.#attached = true;
|
||||
// Note: this might not be optimal, as if hostDisconnected remove one of the controllers, then the next controller will be skipped.
|
||||
this.#controllers.forEach((ctrl: UmbController) => ctrl.hostConnected());
|
||||
}
|
||||
|
||||
hostDisconnected() {
|
||||
this.#attached = false;
|
||||
// Note: this might not be optimal, as if hostDisconnected remove one of the controllers, then the next controller will be skipped.
|
||||
this.#controllers.forEach((ctrl: UmbController) => ctrl.hostDisconnected());
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#controllers.forEach((ctrl: UmbController) => ctrl.destroy());
|
||||
let ctrl: UmbController | undefined;
|
||||
// Note: A very important way of doing this loop, as foreach will skip over the next item if the current item is removed.
|
||||
while ((ctrl = this.#controllers[0])) {
|
||||
ctrl.destroy();
|
||||
}
|
||||
this.#controllers.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { HTMLElementConstructor } from '../extension-api/types.js';
|
||||
import type { UmbControllerAlias } from './controller-alias.type.js';
|
||||
import { UmbControllerHostBaseMixin } from './controller-host-base.mixin.js';
|
||||
import type { UmbControllerHost } from './controller-host.interface.js';
|
||||
import type { UmbController } from './controller.interface.js';
|
||||
import { HTMLElementConstructor } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
export declare class UmbControllerHostElement extends HTMLElement implements UmbControllerHost {
|
||||
hasController(controller: UmbController): boolean;
|
||||
|
||||
@@ -66,12 +66,13 @@ describe('UmbController', () => {
|
||||
describe('Controller Public API', () => {
|
||||
let controller: UmbTestControllerImplementationElement;
|
||||
beforeEach(() => {
|
||||
controller = new UmbTestControllerImplementationElement(hostElement, 'my-test-context');
|
||||
controller = new UmbTestControllerImplementationElement(hostElement, 'my-test-controller-alias');
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
it('has an controllerAlias property', () => {
|
||||
expect(controller).to.have.property('controllerAlias').that.is.a('string');
|
||||
expect(controller.controllerAlias).to.be.equal('my-test-controller-alias');
|
||||
});
|
||||
it('has an hasController method', () => {
|
||||
expect(controller).to.have.property('hasController').that.is.a('function');
|
||||
@@ -102,8 +103,8 @@ describe('UmbController', () => {
|
||||
|
||||
describe('Controllers lifecycle', () => {
|
||||
it('controller is removed from host when destroyed', () => {
|
||||
const ctrl = new UmbTestControllerImplementationElement(hostElement, 'my-test-context');
|
||||
const subCtrl = new UmbTestControllerImplementationElement(ctrl, 'my-test-context');
|
||||
const ctrl = new UmbTestControllerImplementationElement(hostElement);
|
||||
const subCtrl = new UmbTestControllerImplementationElement(ctrl);
|
||||
|
||||
expect(hostElement.hasController(ctrl)).to.be.true;
|
||||
expect(ctrl.hasController(subCtrl)).to.be.true;
|
||||
@@ -119,8 +120,8 @@ describe('UmbController', () => {
|
||||
});
|
||||
|
||||
it('controller is destroyed when removed from host', () => {
|
||||
const ctrl = new UmbTestControllerImplementationElement(hostElement, 'my-test-context');
|
||||
const subCtrl = new UmbTestControllerImplementationElement(ctrl, 'my-test-context');
|
||||
const ctrl = new UmbTestControllerImplementationElement(hostElement);
|
||||
const subCtrl = new UmbTestControllerImplementationElement(ctrl);
|
||||
|
||||
expect(ctrl.testIsDestroyed).to.be.false;
|
||||
expect(subCtrl.testIsDestroyed).to.be.false;
|
||||
@@ -135,9 +136,45 @@ describe('UmbController', () => {
|
||||
expect(ctrl.hasController(subCtrl)).to.be.false;
|
||||
});
|
||||
|
||||
it('all controllers are destroyed when the hosting controller gets destroyed', () => {
|
||||
const ctrl = new UmbTestControllerImplementationElement(hostElement);
|
||||
const subCtrl = new UmbTestControllerImplementationElement(ctrl);
|
||||
const subCtrl2 = new UmbTestControllerImplementationElement(ctrl);
|
||||
const subSubCtrl1 = new UmbTestControllerImplementationElement(subCtrl);
|
||||
const subSubCtrl2 = new UmbTestControllerImplementationElement(subCtrl);
|
||||
|
||||
expect(ctrl.testIsDestroyed).to.be.false;
|
||||
expect(hostElement.hasController(ctrl)).to.be.true;
|
||||
// Subs:
|
||||
expect(subCtrl.testIsDestroyed).to.be.false;
|
||||
expect(subCtrl2.testIsDestroyed).to.be.false;
|
||||
expect(ctrl.hasController(subCtrl)).to.be.true;
|
||||
expect(ctrl.hasController(subCtrl2)).to.be.true;
|
||||
// Sub subs:
|
||||
expect(subSubCtrl1.testIsDestroyed).to.be.false;
|
||||
expect(subSubCtrl2.testIsDestroyed).to.be.false;
|
||||
expect(subCtrl.hasController(subSubCtrl1)).to.be.true;
|
||||
expect(subCtrl.hasController(subSubCtrl2)).to.be.true;
|
||||
|
||||
ctrl.destroy();
|
||||
|
||||
expect(ctrl.testIsDestroyed).to.be.true;
|
||||
expect(hostElement.hasController(ctrl)).to.be.false;
|
||||
// Subs:
|
||||
expect(subCtrl.testIsDestroyed).to.be.true;
|
||||
expect(subCtrl2.testIsDestroyed).to.be.true;
|
||||
expect(ctrl.hasController(subCtrl)).to.be.false;
|
||||
expect(ctrl.hasController(subCtrl2)).to.be.false;
|
||||
// Sub subs:
|
||||
expect(subSubCtrl1.testIsDestroyed).to.be.true;
|
||||
expect(subSubCtrl2.testIsDestroyed).to.be.true;
|
||||
expect(subCtrl.hasController(subSubCtrl1)).to.be.false;
|
||||
expect(subCtrl.hasController(subSubCtrl2)).to.be.false;
|
||||
});
|
||||
|
||||
it('hostConnected & hostDisconnected is triggered accordingly to the state of the controller host.', () => {
|
||||
const ctrl = new UmbTestControllerImplementationElement(hostElement, 'my-test-context');
|
||||
const subCtrl = new UmbTestControllerImplementationElement(ctrl, 'my-test-context');
|
||||
const ctrl = new UmbTestControllerImplementationElement(hostElement);
|
||||
const subCtrl = new UmbTestControllerImplementationElement(ctrl);
|
||||
|
||||
expect(hostElement.hasController(ctrl)).to.be.true;
|
||||
expect(ctrl.hasController(subCtrl)).to.be.true;
|
||||
@@ -154,12 +191,35 @@ describe('UmbController', () => {
|
||||
expect(ctrl.testIsConnected).to.be.false;
|
||||
expect(subCtrl.testIsConnected).to.be.false;
|
||||
});
|
||||
|
||||
it('hostConnected is triggered if controller host is already connected at time of adding controller.', async () => {
|
||||
document.body.appendChild(hostElement);
|
||||
|
||||
const ctrl = new UmbTestControllerImplementationElement(hostElement);
|
||||
const subCtrl = new UmbTestControllerImplementationElement(ctrl);
|
||||
|
||||
expect(hostElement.hasController(ctrl)).to.be.true;
|
||||
expect(ctrl.hasController(subCtrl)).to.be.true;
|
||||
expect(ctrl.testIsConnected).to.be.false;
|
||||
expect(subCtrl.testIsConnected).to.be.false;
|
||||
|
||||
// Wait one JS cycle, to ensure that the hostConnected is triggered. (Currently its by design that we trigger the hostConnected with one cycle delay)
|
||||
await Promise.resolve();
|
||||
|
||||
expect(ctrl.testIsConnected).to.be.true;
|
||||
expect(subCtrl.testIsConnected).to.be.true;
|
||||
|
||||
document.body.removeChild(hostElement);
|
||||
|
||||
expect(ctrl.testIsConnected).to.be.false;
|
||||
expect(subCtrl.testIsConnected).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Controllers against other Controller', () => {
|
||||
describe('Controllers against other Controllers', () => {
|
||||
it('controller is replaced by another controller using the same string as controller-alias', () => {
|
||||
const firstCtrl = new UmbTestControllerImplementationElement(hostElement, 'my-test-context');
|
||||
const secondCtrl = new UmbTestControllerImplementationElement(hostElement, 'my-test-context');
|
||||
const firstCtrl = new UmbTestControllerImplementationElement(hostElement, 'my-test-alias');
|
||||
const secondCtrl = new UmbTestControllerImplementationElement(hostElement, 'my-test-alias');
|
||||
|
||||
expect(hostElement.hasController(firstCtrl)).to.be.false;
|
||||
expect(hostElement.hasController(secondCtrl)).to.be.true;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { UmbConditionConfigBase } from '../types.js';
|
||||
import { UmbConditionConfigBase } from '../types/index.js';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
export type UmbConditionControllerArguments<
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { UmbConditionConfigBase } from '../types.js';
|
||||
import type { UmbConditionConfigBase } from '../types/index.js';
|
||||
import { UmbController } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
export interface UmbExtensionCondition extends UmbController {
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
ManifestKind,
|
||||
ManifestWithDynamicConditions,
|
||||
UmbConditionConfigBase,
|
||||
} from '../types.js';
|
||||
} from '../types/index.js';
|
||||
import { UmbExtensionRegistry } from '../registry/extension.registry.js';
|
||||
import type { UmbExtensionCondition } from '../condition/extension-condition.interface.js';
|
||||
import {
|
||||
@@ -25,9 +25,9 @@ class UmbTestExtensionController extends UmbBaseExtensionInitializer {
|
||||
host: UmbControllerHostElement,
|
||||
extensionRegistry: UmbExtensionRegistry<ManifestWithDynamicConditions>,
|
||||
alias: string,
|
||||
onPermissionChanged: (isPermitted: boolean) => void
|
||||
onPermissionChanged: (isPermitted: boolean) => void,
|
||||
) {
|
||||
super(host, extensionRegistry, 'test_', alias, onPermissionChanged);
|
||||
super(host, extensionRegistry, 'test', alias, onPermissionChanged);
|
||||
this._init();
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('UmbBaseExtensionController', () => {
|
||||
done();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -128,7 +128,7 @@ describe('UmbBaseExtensionController', () => {
|
||||
done();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -168,7 +168,7 @@ describe('UmbBaseExtensionController', () => {
|
||||
(isPermitted) => {
|
||||
// No relevant for this test.
|
||||
expect(isPermitted).to.be.true;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
extensionRegistry.register(manifest);
|
||||
@@ -189,7 +189,7 @@ describe('UmbBaseExtensionController', () => {
|
||||
'Umb.Test.Section.1',
|
||||
() => {
|
||||
expect.fail('Callback should not be called when never permitted');
|
||||
}
|
||||
},
|
||||
);
|
||||
extensionController.asPromise().then(() => {
|
||||
expect.fail('Promise should not resolve');
|
||||
@@ -216,22 +216,24 @@ describe('UmbBaseExtensionController', () => {
|
||||
hostElement,
|
||||
extensionRegistry,
|
||||
'Umb.Test.Section.1',
|
||||
() => {
|
||||
count++;
|
||||
if (count === 1) {
|
||||
// First time render, there is no conditions.
|
||||
expect(extensionController.manifest?.weight).to.be.equal(2);
|
||||
expect(extensionController.manifest?.conditions?.length).to.be.equal(1);
|
||||
} else if (count === 2) {
|
||||
// Second time render, there is conditions and weight is 22.
|
||||
expect(extensionController.manifest?.weight).to.be.equal(22);
|
||||
expect(extensionController.manifest?.conditions?.length).to.be.equal(1);
|
||||
// Check that the promise has been resolved for the first render to ensure timing is right.
|
||||
expect(initialPromiseResolved).to.be.true;
|
||||
done();
|
||||
extensionController.destroy();
|
||||
(isPermitted) => {
|
||||
if (isPermitted) {
|
||||
count++;
|
||||
if (count === 1) {
|
||||
// First time render, there is no conditions.
|
||||
expect(extensionController.manifest?.weight).to.be.equal(2);
|
||||
expect(extensionController.manifest?.conditions?.length).to.be.equal(1);
|
||||
} else if (count === 2) {
|
||||
// Second time render, there is conditions and weight is 22.
|
||||
expect(extensionController.manifest?.weight).to.be.equal(22);
|
||||
expect(extensionController.manifest?.conditions?.length).to.be.equal(1);
|
||||
// Check that the promise has been resolved for the first render to ensure timing is right.
|
||||
expect(initialPromiseResolved).to.be.true;
|
||||
done();
|
||||
extensionController.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
extensionController.asPromise().then(() => {
|
||||
initialPromiseResolved = true;
|
||||
@@ -259,22 +261,24 @@ describe('UmbBaseExtensionController', () => {
|
||||
hostElement,
|
||||
extensionRegistry,
|
||||
'Umb.Test.Section.1',
|
||||
() => {
|
||||
count++;
|
||||
if (count === 1) {
|
||||
// First time render, there is no conditions.
|
||||
expect(extensionController.manifest?.weight).to.be.equal(3);
|
||||
expect(extensionController.manifest?.conditions?.length).to.be.equal(0);
|
||||
} else if (count === 2) {
|
||||
// Second time render, there is conditions and weight is 33.
|
||||
expect(extensionController.manifest?.weight).to.be.equal(33);
|
||||
expect(extensionController.manifest?.conditions?.length).to.be.equal(0);
|
||||
// Check that the promise has been resolved for the first render to ensure timing is right.
|
||||
expect(initialPromiseResolved).to.be.true;
|
||||
done();
|
||||
extensionController.destroy();
|
||||
(isPermitted) => {
|
||||
if (isPermitted) {
|
||||
count++;
|
||||
if (count === 1) {
|
||||
// First time render, there is no conditions.
|
||||
expect(extensionController.manifest?.weight).to.be.equal(3);
|
||||
expect(extensionController.manifest?.conditions?.length).to.be.equal(0);
|
||||
} else if (count === 2) {
|
||||
// Second time render, there is conditions and weight is 33.
|
||||
expect(extensionController.manifest?.weight).to.be.equal(33);
|
||||
expect(extensionController.manifest?.conditions?.length).to.be.equal(0);
|
||||
// Check that the promise has been resolved for the first render to ensure timing is right.
|
||||
expect(initialPromiseResolved).to.be.true;
|
||||
done();
|
||||
extensionController.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
extensionController.asPromise().then(() => {
|
||||
initialPromiseResolved = true;
|
||||
@@ -302,22 +306,24 @@ describe('UmbBaseExtensionController', () => {
|
||||
hostElement,
|
||||
extensionRegistry,
|
||||
'Umb.Test.Section.1',
|
||||
() => {
|
||||
count++;
|
||||
if (count === 1) {
|
||||
// First time render, there is no conditions.
|
||||
expect(extensionController.manifest?.weight).to.be.equal(4);
|
||||
expect(extensionController.manifest?.conditions?.length).to.be.equal(0);
|
||||
} else if (count === 2) {
|
||||
// Second time render, there is conditions and weight is 33.
|
||||
expect(extensionController.manifest?.weight).to.be.equal(44);
|
||||
expect(extensionController.manifest?.conditions?.length).to.be.equal(1);
|
||||
// Check that the promise has been resolved for the first render to ensure timing is right.
|
||||
expect(initialPromiseResolved).to.be.true;
|
||||
done();
|
||||
extensionController.destroy();
|
||||
(isPermitted) => {
|
||||
if (isPermitted) {
|
||||
count++;
|
||||
if (count === 1) {
|
||||
// First time render, there is no conditions.
|
||||
expect(extensionController.manifest?.weight).to.be.equal(4);
|
||||
expect(extensionController.manifest?.conditions?.length).to.be.equal(0);
|
||||
} else if (count === 2) {
|
||||
// Second time render, there is conditions and weight is 33.
|
||||
expect(extensionController.manifest?.weight).to.be.equal(44);
|
||||
expect(extensionController.manifest?.conditions?.length).to.be.equal(1);
|
||||
// Check that the promise has been resolved for the first render to ensure timing is right.
|
||||
expect(initialPromiseResolved).to.be.true;
|
||||
done();
|
||||
extensionController.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
extensionController.asPromise().then(() => {
|
||||
initialPromiseResolved = true;
|
||||
@@ -355,22 +361,24 @@ describe('UmbBaseExtensionController', () => {
|
||||
hostElement,
|
||||
extensionRegistry,
|
||||
'Umb.Test.Section.1',
|
||||
() => {
|
||||
count++;
|
||||
if (count === 1) {
|
||||
// First time render, there is no conditions.
|
||||
expect(extensionController.manifest?.weight).to.be.undefined;
|
||||
expect(extensionController.manifest?.conditions?.length).to.be.equal(0);
|
||||
} else if (count === 2) {
|
||||
// Second time render, there is a matching kind and then weight is 123.
|
||||
expect(extensionController.manifest?.weight).to.be.equal(123);
|
||||
expect(extensionController.manifest?.conditions?.length).to.be.equal(0);
|
||||
// Check that the promise has been resolved for the first render to ensure timing is right.
|
||||
expect(initialPromiseResolved).to.be.true;
|
||||
done();
|
||||
extensionController.destroy();
|
||||
(isPermitted) => {
|
||||
if (isPermitted) {
|
||||
count++;
|
||||
if (count === 1) {
|
||||
// First time render, there is no conditions.
|
||||
expect(extensionController.manifest?.weight).to.be.undefined;
|
||||
expect(extensionController.manifest?.conditions?.length).to.be.equal(0);
|
||||
} else if (count === 2) {
|
||||
// Second time render, there is a matching kind and then weight is 123.
|
||||
expect(extensionController.manifest?.weight).to.be.equal(123);
|
||||
expect(extensionController.manifest?.conditions?.length).to.be.equal(0);
|
||||
// Check that the promise has been resolved for the first render to ensure timing is right.
|
||||
expect(initialPromiseResolved).to.be.true;
|
||||
done();
|
||||
extensionController.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
extensionController.asPromise().then(() => {
|
||||
initialPromiseResolved = true;
|
||||
@@ -421,7 +429,7 @@ describe('UmbBaseExtensionController', () => {
|
||||
() => {
|
||||
// This should not be called.
|
||||
expect(true).to.be.false;
|
||||
}
|
||||
},
|
||||
);
|
||||
Promise.resolve().then(() => {
|
||||
expect(extensionController?.manifest?.alias).to.eq('Umb.Test.Section.1');
|
||||
@@ -439,7 +447,7 @@ describe('UmbBaseExtensionController', () => {
|
||||
() => {
|
||||
// This should not be called.
|
||||
expect(true).to.be.false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
extensionRegistry.register(manifest);
|
||||
@@ -460,7 +468,7 @@ describe('UmbBaseExtensionController', () => {
|
||||
'Umb.Test.Section.1',
|
||||
() => {
|
||||
// Empty callback.
|
||||
}
|
||||
},
|
||||
);
|
||||
extensionController.hasConditions().then((hasConditions) => {
|
||||
expect(hasConditions).to.be.true;
|
||||
@@ -528,7 +536,7 @@ describe('UmbBaseExtensionController', () => {
|
||||
extensionController.destroy(); // need to destroy the conditions.
|
||||
done();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -549,20 +557,20 @@ describe('UmbBaseExtensionController', () => {
|
||||
conditions: [
|
||||
{
|
||||
alias: 'Umb.Test.Condition.Delay',
|
||||
value: '100',
|
||||
value: '10',
|
||||
},
|
||||
{
|
||||
alias: 'Umb.Test.Condition.Delay',
|
||||
value: '200',
|
||||
value: '20',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// A ASCII timeline for the conditions, when allowed and then not allowed:
|
||||
// Condition 0ms 100ms 200ms 300ms 400ms 500ms
|
||||
// First condition: - + - + - +
|
||||
// Second condition: - - + + - -
|
||||
// Sum: - - - + - -
|
||||
// Condition 0ms 10ms 20ms 30ms 40ms 50ms 60ms
|
||||
// First condition: - + - + - + -
|
||||
// Second condition: - - + + - - +
|
||||
// Sum: - - - + - - -
|
||||
|
||||
const conditionManifest = {
|
||||
type: 'condition',
|
||||
@@ -581,27 +589,33 @@ describe('UmbBaseExtensionController', () => {
|
||||
hostElement,
|
||||
extensionRegistry,
|
||||
'Umb.Test.Section.1',
|
||||
async () => {
|
||||
async (isPermitted) => {
|
||||
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.Section.1');
|
||||
if (count === 1) {
|
||||
expect(isPermitted).to.be.true;
|
||||
expect(extensionController?.permitted).to.be.true;
|
||||
// Hack to double check that its two conditions that make up the state:
|
||||
expect(extensionController.getControllers((controller) => (controller as any).permitted).length).to.equal(
|
||||
2
|
||||
2,
|
||||
);
|
||||
} else if (count === 2) {
|
||||
expect(isPermitted).to.be.false;
|
||||
expect(extensionController?.permitted).to.be.false;
|
||||
// Hack to double check that its two conditions that make up the state, in this case its one, cause we already got the callback when one of the conditions changed. meaning in this split second one is still good:
|
||||
expect(extensionController.getControllers((controller) => (controller as any).permitted).length).to.equal(
|
||||
1
|
||||
1,
|
||||
);
|
||||
extensionController.destroy(); // need to destroy the conditions.
|
||||
done();
|
||||
|
||||
// Then we are done:
|
||||
extensionController.destroy(); // End this test.
|
||||
setTimeout(() => done(), 60); // Lets wait another round of the conditions approve/disapprove, just to see if the destroy stopped the conditions. (60ms, as that should be enough to test that another round does not happen.)
|
||||
} else if (count === 5) {
|
||||
expect(false).to.be.true; // This should not be called.
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,13 +5,23 @@ import {
|
||||
type ManifestWithDynamicConditions,
|
||||
type UmbExtensionRegistry,
|
||||
createExtensionApi,
|
||||
UmbConditionConfigBase,
|
||||
} from '@umbraco-cms/backoffice/extension-api';
|
||||
import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
|
||||
|
||||
/**
|
||||
* This abstract Controller holds the core to manage a single Extension.
|
||||
* When the extension is permitted to be used, then the extender of this class can instantiate what is relevant for this type and thereby make it available for the consumer.
|
||||
*
|
||||
* @export
|
||||
* @abstract
|
||||
* @class UmbBaseExtensionInitializer
|
||||
*/
|
||||
export abstract class UmbBaseExtensionInitializer<
|
||||
ManifestType extends ManifestWithDynamicConditions = ManifestWithDynamicConditions,
|
||||
SubClassType = never
|
||||
SubClassType = never,
|
||||
> extends UmbBaseController {
|
||||
//
|
||||
#promiseResolvers: Array<() => void> = [];
|
||||
#manifestObserver!: UmbObserverController<ManifestType | undefined>;
|
||||
#extensionRegistry: UmbExtensionRegistry<ManifestCondition>;
|
||||
@@ -20,7 +30,7 @@ export abstract class UmbBaseExtensionInitializer<
|
||||
#manifest?: ManifestType;
|
||||
#conditionControllers: Array<UmbExtensionCondition> = [];
|
||||
#onPermissionChanged?: (isPermitted: boolean, controller: SubClassType) => void;
|
||||
protected _positive?: boolean;
|
||||
protected _isConditionsPositive?: boolean;
|
||||
#isPermitted?: boolean;
|
||||
|
||||
get weight() {
|
||||
@@ -53,9 +63,9 @@ export abstract class UmbBaseExtensionInitializer<
|
||||
extensionRegistry: UmbExtensionRegistry<ManifestCondition>,
|
||||
extensionTypeName: string,
|
||||
alias: string,
|
||||
onPermissionChanged?: (isPermitted: boolean, controller: SubClassType) => void
|
||||
onPermissionChanged?: (isPermitted: boolean, controller: SubClassType) => void,
|
||||
) {
|
||||
super(host, extensionTypeName+alias);
|
||||
super(host, extensionTypeName + '_' + alias);
|
||||
this.#extensionRegistry = extensionRegistry;
|
||||
this.#alias = alias;
|
||||
this.#onPermissionChanged = onPermissionChanged;
|
||||
@@ -64,7 +74,7 @@ export abstract class UmbBaseExtensionInitializer<
|
||||
this.#manifestObserver = this.observe(
|
||||
this.#extensionRegistry.getByAlias<ManifestType>(this.#alias),
|
||||
async (extensionManifest) => {
|
||||
this.#isPermitted = undefined;
|
||||
this.#clearPermittedState();
|
||||
this.#manifest = extensionManifest;
|
||||
if (extensionManifest) {
|
||||
if (extensionManifest.overwrites) {
|
||||
@@ -74,12 +84,13 @@ export abstract class UmbBaseExtensionInitializer<
|
||||
this.#overwrites = extensionManifest.overwrites;
|
||||
}
|
||||
}
|
||||
this.#gotConditions();
|
||||
this.#gotManifest();
|
||||
} else {
|
||||
this.#overwrites = [];
|
||||
this.#cleanConditions();
|
||||
}
|
||||
}
|
||||
},
|
||||
'_observeExtensionManifest',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,16 +101,17 @@ export abstract class UmbBaseExtensionInitializer<
|
||||
}
|
||||
|
||||
#cleanConditions() {
|
||||
if (this.#conditionControllers.length === 0) return;
|
||||
this.#conditionControllers.forEach((controller) => controller.destroy());
|
||||
this.#conditionControllers = [];
|
||||
this.removeControllerByAlias('_observeConditions');
|
||||
}
|
||||
|
||||
#gotConditions() {
|
||||
#gotManifest() {
|
||||
const conditionConfigs = this.#manifest?.conditions ?? [];
|
||||
|
||||
// As conditionConfigs might have been configured as something else than an array, then we ignorer them.
|
||||
if (conditionConfigs.length === undefined || conditionConfigs.length === 0) {
|
||||
if (conditionConfigs.length === 0) {
|
||||
this.#cleanConditions();
|
||||
this.#onConditionsChangedCallback();
|
||||
return;
|
||||
@@ -131,37 +143,8 @@ export abstract class UmbBaseExtensionInitializer<
|
||||
// Observes the conditions and initialize as they come in.
|
||||
this.observe(
|
||||
this.#extensionRegistry.getByTypeAndAliases('condition', conditionAliases),
|
||||
async (manifests) => {
|
||||
// New comers:
|
||||
manifests.forEach((conditionManifest) => {
|
||||
const configsOfThisType = conditionConfigs.filter(
|
||||
(conditionConfig) => conditionConfig.alias === conditionManifest.alias
|
||||
);
|
||||
|
||||
// Spin up conditions, based of condition configs:
|
||||
configsOfThisType.forEach(async (conditionConfig) => {
|
||||
// Check if we already have a controller for this config:
|
||||
const existing = this.#conditionControllers.find((controller) => controller.config === conditionConfig);
|
||||
if (!existing) {
|
||||
const conditionController = await createExtensionApi<UmbExtensionCondition>(conditionManifest, [
|
||||
{
|
||||
host: this,
|
||||
manifest: conditionManifest,
|
||||
config: conditionConfig,
|
||||
onChange: this.#onConditionsChangedCallback,
|
||||
},
|
||||
]);
|
||||
if (conditionController) {
|
||||
// Some how listen to it? callback/event/onChange something.
|
||||
// then call this one: this.#onConditionsChanged();
|
||||
this.#conditionControllers.push(conditionController);
|
||||
this.#onConditionsChangedCallback();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
'_observeConditions'
|
||||
this.#gotConditions,
|
||||
'_observeConditions',
|
||||
);
|
||||
} else {
|
||||
this.removeControllerByAlias('_observeConditions');
|
||||
@@ -173,6 +156,67 @@ export abstract class UmbBaseExtensionInitializer<
|
||||
}
|
||||
}
|
||||
|
||||
#gotConditions = (manifests: ManifestCondition[]) => {
|
||||
manifests.forEach(this.#gotCondition);
|
||||
};
|
||||
|
||||
#gotCondition = async (conditionManifest: ManifestCondition) => {
|
||||
const conditionConfigs = this.#manifest?.conditions ?? [];
|
||||
//
|
||||
// Get just the conditions that uses this condition alias:
|
||||
const configsOfThisType = conditionConfigs.filter(
|
||||
(conditionConfig) => conditionConfig.alias === conditionManifest.alias,
|
||||
);
|
||||
|
||||
// Create conditions, based of condition configs:
|
||||
const newConditionControllers = await Promise.all(
|
||||
configsOfThisType.map((conditionConfig) => this.#createConditionController(conditionManifest, conditionConfig)),
|
||||
);
|
||||
|
||||
const oldLength = this.#conditionControllers.length;
|
||||
|
||||
newConditionControllers
|
||||
.filter((x) => x !== undefined)
|
||||
.forEach((emerging) => {
|
||||
// TODO: All of this could use a refactor at one point, when someone is fresh in their mind.
|
||||
// Niels Notes: Current problem being that we are not aware about what is in the making, so we don't know if we end up creating the same condition multiple times.
|
||||
// Because it took some time to create the conditions, it maybe have already gotten created by another cycle, so lets test again.
|
||||
const existing = this.#conditionControllers.find((existing) => existing.config === emerging?.config);
|
||||
if (!existing) {
|
||||
this.#conditionControllers.push(emerging!);
|
||||
} else {
|
||||
emerging?.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// If a change to amount of condition controllers, this will make sure that when new conditions are added, the callback is fired, so the extensions can be re-evaluated, starting out as bad.
|
||||
if (oldLength !== this.#conditionControllers.length) {
|
||||
this.#onConditionsChangedCallback();
|
||||
}
|
||||
};
|
||||
|
||||
async #createConditionController(
|
||||
conditionManifest: ManifestCondition,
|
||||
conditionConfig: UmbConditionConfigBase,
|
||||
): Promise<UmbExtensionCondition | undefined> {
|
||||
// Check if we already have a controller for this config:
|
||||
const existing = this.#conditionControllers.find((controller) => controller.config === conditionConfig);
|
||||
if (!existing) {
|
||||
const conditionController = await createExtensionApi(conditionManifest, [
|
||||
{
|
||||
host: this,
|
||||
manifest: conditionManifest,
|
||||
config: conditionConfig,
|
||||
onChange: this.#onConditionsChangedCallback,
|
||||
},
|
||||
]);
|
||||
if (conditionController) {
|
||||
return conditionController;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
#conditionsAreInitialized() {
|
||||
// Not good if we don't have a manifest.
|
||||
// Only good if conditions of manifest is equal to the amount of condition controllers (one for each condition).
|
||||
@@ -182,23 +226,33 @@ export abstract class UmbBaseExtensionInitializer<
|
||||
}
|
||||
|
||||
#onConditionsChangedCallback = async () => {
|
||||
const oldValue = this.#isPermitted ?? false;
|
||||
// We will collect old value here, but we need to re-collect it after a async method have been called, as it could have changed in the mean time.
|
||||
let oldValue = this.#isPermitted ?? false;
|
||||
|
||||
// Find a condition that is not permitted (Notice how no conditions, means that this extension is permitted)
|
||||
const isPositive =
|
||||
this.#conditionsAreInitialized() &&
|
||||
this.#conditionControllers.find((condition) => condition.permitted === false) === undefined;
|
||||
|
||||
this._positive = isPositive;
|
||||
this._isConditionsPositive = isPositive;
|
||||
|
||||
if (isPositive) {
|
||||
if (this.#isPermitted !== true) {
|
||||
this.#isPermitted = await this._conditionsAreGood();
|
||||
const newPermission = await this._conditionsAreGood();
|
||||
// Only set new permission if we are still positive, otherwise it means that we have been destroyed in the mean time.
|
||||
if (newPermission === false) {
|
||||
return;
|
||||
}
|
||||
// We update the oldValue as this point, cause in this way we are sure its the value at this point, when doing async code someone else might have changed the state in the mean time.
|
||||
oldValue = this.#isPermitted ?? false;
|
||||
this.#isPermitted = newPermission;
|
||||
}
|
||||
} else if (this.#isPermitted !== false) {
|
||||
await this._conditionsAreBad();
|
||||
// Clean up:
|
||||
this.#isPermitted = false;
|
||||
await this._conditionsAreBad();
|
||||
}
|
||||
if (oldValue !== this.#isPermitted) {
|
||||
if (oldValue !== this.#isPermitted && this.#isPermitted !== undefined) {
|
||||
if (this.#isPermitted) {
|
||||
this.#promiseResolvers.forEach((x) => x());
|
||||
this.#promiseResolvers = [];
|
||||
@@ -215,13 +269,41 @@ export abstract class UmbBaseExtensionInitializer<
|
||||
return otherClass?.manifest === this.manifest;
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.#promiseResolvers = [];
|
||||
/*
|
||||
public hostConnected(): void {
|
||||
super.hostConnected();
|
||||
//this.#onConditionsChangedCallback();
|
||||
}
|
||||
|
||||
public hostDisconnected(): void {
|
||||
super.hostDisconnected();
|
||||
this._runtimePositive = false;
|
||||
if (this.#isPermitted === true) {
|
||||
this.#isPermitted = false;
|
||||
this._conditionsAreBad();
|
||||
this.#onPermissionChanged?.(false, this as any);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
#clearPermittedState() {
|
||||
if (this.#isPermitted === true) {
|
||||
this.#isPermitted = undefined;
|
||||
this._conditionsAreBad();
|
||||
this.#onPermissionChanged?.(false, this as any);
|
||||
}
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
if (!this.#extensionRegistry) return;
|
||||
this.#promiseResolvers = [];
|
||||
this.#clearPermittedState(); // This fires the callback as not permitted, if it was permitted before.
|
||||
this.#isPermitted = undefined;
|
||||
this._isConditionsPositive = false;
|
||||
this.#overwrites = [];
|
||||
this.#cleanConditions();
|
||||
this.#onPermissionChanged = undefined;
|
||||
(this.#extensionRegistry as any) = undefined;
|
||||
super.destroy();
|
||||
// Destroy the conditions controllers, are begin destroyed cause they are controllers.
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, fixture } from '@open-wc/testing';
|
||||
import { UmbExtensionRegistry } from '../registry/extension.registry.js';
|
||||
import { ManifestCondition, ManifestWithDynamicConditions, UmbConditionConfigBase } from '../types.js';
|
||||
import { ManifestCondition, ManifestWithDynamicConditions, UmbConditionConfigBase } from '../types/index.js';
|
||||
import { UmbExtensionCondition } from '../condition/extension-condition.interface.js';
|
||||
import { PermittedControllerType, UmbBaseExtensionInitializer, UmbBaseExtensionsInitializer } from './index.js';
|
||||
import {
|
||||
@@ -18,7 +18,7 @@ class UmbTestExtensionController extends UmbBaseExtensionInitializer {
|
||||
host: UmbControllerHost,
|
||||
extensionRegistry: UmbExtensionRegistry<ManifestWithDynamicConditions>,
|
||||
alias: string,
|
||||
onPermissionChanged: (isPermitted: boolean, controller: UmbTestExtensionController) => void
|
||||
onPermissionChanged: (isPermitted: boolean, controller: UmbTestExtensionController) => void,
|
||||
) {
|
||||
super(host, extensionRegistry, 'test_', alias, onPermissionChanged);
|
||||
this._init();
|
||||
@@ -40,7 +40,7 @@ type myTestManifestTypesUnion = 'extension-type-extra' | 'extension-type';
|
||||
type myTestManifestTypes = myTestManifestTypesUnion | myTestManifestTypesUnion[];
|
||||
|
||||
class UmbTestExtensionsController<
|
||||
MyPermittedControllerType extends UmbTestExtensionController = PermittedControllerType<UmbTestExtensionController>
|
||||
MyPermittedControllerType extends UmbTestExtensionController = PermittedControllerType<UmbTestExtensionController>,
|
||||
> extends UmbBaseExtensionsInitializer<
|
||||
myTestManifests,
|
||||
myTestManifestTypesUnion,
|
||||
@@ -48,21 +48,21 @@ class UmbTestExtensionsController<
|
||||
UmbTestExtensionController,
|
||||
MyPermittedControllerType
|
||||
> {
|
||||
#host: UmbControllerHost;
|
||||
#extensionRegistry: UmbExtensionRegistry<ManifestWithDynamicConditions>;
|
||||
constructor(
|
||||
host: UmbControllerHost,
|
||||
extensionRegistry: UmbExtensionRegistry<ManifestWithDynamicConditions>,
|
||||
type: myTestManifestTypes,
|
||||
filter: null | ((manifest: ManifestWithDynamicConditions) => boolean),
|
||||
onChange: (permittedManifests: Array<MyPermittedControllerType>) => void
|
||||
onChange: (permittedManifests: Array<MyPermittedControllerType>) => void,
|
||||
) {
|
||||
super(host, extensionRegistry, type, filter, onChange);
|
||||
this.#host = host;
|
||||
this.#extensionRegistry = extensionRegistry;
|
||||
this._init();
|
||||
}
|
||||
|
||||
protected _createController(manifest: ManifestWithDynamicConditions) {
|
||||
return new UmbTestExtensionController(this.#host, testExtensionRegistry, manifest.alias, this._extensionChanged);
|
||||
return new UmbTestExtensionController(this, this.#extensionRegistry, manifest.alias, this._extensionChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ describe('UmbBaseExtensionsController', () => {
|
||||
|
||||
it('exposes both manifests', (done) => {
|
||||
let count = 0;
|
||||
|
||||
const extensionController = new UmbTestExtensionsController(
|
||||
hostElement,
|
||||
testExtensionRegistry,
|
||||
@@ -119,13 +120,13 @@ describe('UmbBaseExtensionsController', () => {
|
||||
if (count === 1) {
|
||||
// First callback gives just one. We need to make a feature to gather changes to only reply after a computation cycle if we like to avoid this.
|
||||
expect(permitted.length).to.eq(1);
|
||||
}
|
||||
if (count === 2) {
|
||||
} else if (count === 2) {
|
||||
expect(permitted.length).to.eq(2);
|
||||
done();
|
||||
extensionController.destroy();
|
||||
} else if (count === 3) {
|
||||
done();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -136,6 +137,7 @@ describe('UmbBaseExtensionsController', () => {
|
||||
alias: 'Umb.Test.Extension.Extra',
|
||||
};
|
||||
testExtensionRegistry.register(manifestExtra);
|
||||
|
||||
let count = 0;
|
||||
const extensionController = new UmbTestExtensionsController(
|
||||
hostElement,
|
||||
@@ -147,20 +149,21 @@ describe('UmbBaseExtensionsController', () => {
|
||||
if (count === 1) {
|
||||
// First callback gives just one. We need to make a feature to gather changes to only reply after a computation cycle if we like to avoid this.
|
||||
expect(permitted.length).to.eq(1);
|
||||
}
|
||||
if (count === 2) {
|
||||
} else if (count === 2) {
|
||||
expect(permitted.length).to.eq(2);
|
||||
}
|
||||
if (count === 3) {
|
||||
} else if (count === 3) {
|
||||
expect(permitted.length).to.eq(3);
|
||||
expect(permitted[0].alias).to.eq('Umb.Test.Extension.A');
|
||||
expect(permitted[1].alias).to.eq('Umb.Test.Extension.B');
|
||||
expect(permitted[2].alias).to.eq('Umb.Test.Extension.Extra');
|
||||
|
||||
done();
|
||||
extensionController.destroy();
|
||||
} else if (count === 4) {
|
||||
// Cause we destroyed there will be a last call to reset permitted controllers:
|
||||
expect(permitted.length).to.eq(0);
|
||||
done();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -181,6 +184,7 @@ describe('UmbBaseExtensionsController', () => {
|
||||
alias: 'Umb.Test.Extension.B',
|
||||
overwrites: ['Umb.Test.Extension.A'],
|
||||
};
|
||||
|
||||
testExtensionRegistry.register(manifestA);
|
||||
testExtensionRegistry.register(manifestB);
|
||||
});
|
||||
@@ -202,8 +206,7 @@ describe('UmbBaseExtensionsController', () => {
|
||||
// First callback gives just one. We need to make a feature to gather changes to only reply after a computation cycle if we like to avoid this.
|
||||
expect(permitted.length).to.eq(1);
|
||||
expect(permitted[0].alias).to.eq('Umb.Test.Extension.A');
|
||||
}
|
||||
if (count === 2) {
|
||||
} else if (count === 2) {
|
||||
// Still just equal one, as the second one overwrites the first one.
|
||||
expect(permitted.length).to.eq(1);
|
||||
expect(permitted[0].alias).to.eq('Umb.Test.Extension.B');
|
||||
@@ -216,7 +219,7 @@ describe('UmbBaseExtensionsController', () => {
|
||||
done();
|
||||
extensionController.destroy();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -273,8 +276,7 @@ describe('UmbBaseExtensionsController', () => {
|
||||
// First callback gives just one. We need to make a feature to gather changes to only reply after a computation cycle if we like to avoid this.
|
||||
expect(permitted.length).to.eq(1);
|
||||
expect(permitted[0].alias).to.eq('Umb.Test.Extension.A');
|
||||
}
|
||||
if (count === 2) {
|
||||
} else if (count === 2) {
|
||||
// Still just equal one, as the second one overwrites the first one.
|
||||
expect(permitted.length).to.eq(1);
|
||||
expect(permitted[0].alias).to.eq('Umb.Test.Extension.B');
|
||||
@@ -284,10 +286,16 @@ describe('UmbBaseExtensionsController', () => {
|
||||
} else if (count === 3) {
|
||||
expect(permitted.length).to.eq(1);
|
||||
expect(permitted[0].alias).to.eq('Umb.Test.Extension.A');
|
||||
done();
|
||||
extensionController.destroy();
|
||||
} else if (count === 4) {
|
||||
// Expect that destroy will only result in one last callback with no permitted controllers.
|
||||
expect(permitted.length).to.eq(0);
|
||||
Promise.resolve().then(() => done()); // This wrap is to enable the test to detect if more callbacks are fired.
|
||||
} else if (count === 5) {
|
||||
// This should not happen, we do only want one last callback when destroyed.
|
||||
expect(false).to.eq(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -313,6 +321,7 @@ describe('UmbBaseExtensionsController', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Register opposite order, to ensure B is there when A comes around. A fix to be able to test this. Cause a late registration of B would not cause a change that is test able.
|
||||
testExtensionRegistry.register(manifestB);
|
||||
testExtensionRegistry.register(manifestA);
|
||||
@@ -349,7 +358,7 @@ describe('UmbBaseExtensionsController', () => {
|
||||
done();
|
||||
extensionController.destroy();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,26 +1,32 @@
|
||||
import { ManifestTypeMap, SpecificManifestTypeOrManifestBase } from '../types/map.types.js';
|
||||
import { map } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
import type {
|
||||
ManifestBase,
|
||||
ManifestTypeMap,
|
||||
SpecificManifestTypeOrManifestBase,
|
||||
UmbBaseExtensionInitializer,
|
||||
UmbExtensionRegistry,
|
||||
} from '@umbraco-cms/backoffice/extension-api';
|
||||
import { UmbBaseController, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbBaseController, type UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
export type PermittedControllerType<ControllerType extends { manifest: any }> = ControllerType & {
|
||||
manifest: Required<Pick<ControllerType, 'manifest'>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* This abstract Controller holds the core to manage a multiple Extensions.
|
||||
* When one or more extensions are permitted to be used, then the extender of this class can instantiate the relevant single extension initiator relevant for this type.
|
||||
*
|
||||
* @export
|
||||
* @abstract
|
||||
* @class UmbBaseExtensionsInitializer
|
||||
*/
|
||||
export abstract class UmbBaseExtensionsInitializer<
|
||||
ManifestTypes extends ManifestBase,
|
||||
ManifestTypeName extends keyof ManifestTypeMap<ManifestTypes> | string,
|
||||
ManifestType extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, ManifestTypeName>,
|
||||
ControllerType extends UmbBaseExtensionInitializer<ManifestType> = UmbBaseExtensionInitializer<ManifestType>,
|
||||
MyPermittedControllerType extends ControllerType = PermittedControllerType<ControllerType>
|
||||
MyPermittedControllerType extends ControllerType = PermittedControllerType<ControllerType>,
|
||||
> extends UmbBaseController {
|
||||
#promiseResolvers: Array<() => void> = [];
|
||||
#extensionRegistry: UmbExtensionRegistry<ManifestType>;
|
||||
#type: ManifestTypeName | Array<ManifestTypeName>;
|
||||
#filter: undefined | null | ((manifest: ManifestType) => boolean);
|
||||
@@ -28,14 +34,20 @@ export abstract class UmbBaseExtensionsInitializer<
|
||||
protected _extensions: Array<ControllerType> = [];
|
||||
private _permittedExts: Array<MyPermittedControllerType> = [];
|
||||
|
||||
asPromise(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this._permittedExts.length > 0 ? resolve() : this.#promiseResolvers.push(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
constructor(
|
||||
host: UmbControllerHost,
|
||||
extensionRegistry: UmbExtensionRegistry<ManifestType>,
|
||||
type: ManifestTypeName | Array<ManifestTypeName>,
|
||||
filter: undefined | null | ((manifest: ManifestType) => boolean),
|
||||
onChange?: (permittedManifests: Array<MyPermittedControllerType>) => void
|
||||
onChange?: (permittedManifests: Array<MyPermittedControllerType>) => void,
|
||||
) {
|
||||
super(host);
|
||||
super(host, 'extensionsInitializer_' + (Array.isArray(type) ? type.join('_') : type));
|
||||
this.#extensionRegistry = extensionRegistry;
|
||||
this.#type = type;
|
||||
this.#filter = filter;
|
||||
@@ -48,7 +60,7 @@ export abstract class UmbBaseExtensionsInitializer<
|
||||
if (this.#filter) {
|
||||
source = source.pipe(map((extensions: Array<ManifestType>) => extensions.filter(this.#filter!)));
|
||||
}
|
||||
this.observe(source, this.#gotManifests);
|
||||
this.observe(source, this.#gotManifests, '_observeManifests') as any;
|
||||
}
|
||||
|
||||
#gotManifests = (manifests: Array<ManifestType>) => {
|
||||
@@ -63,9 +75,9 @@ export abstract class UmbBaseExtensionsInitializer<
|
||||
}
|
||||
|
||||
// Clean up extensions that are no longer.
|
||||
this._extensions = this._extensions.filter((controller) => {
|
||||
if (!manifests.find((manifest) => manifest.alias === controller.alias)) {
|
||||
controller.destroy();
|
||||
this._extensions = this._extensions.filter((extension) => {
|
||||
if (!manifests.find((manifest) => manifest.alias === extension.alias)) {
|
||||
extension.destroy();
|
||||
// destroying the controller will, if permitted, make a last callback with isPermitted = false. This will also remove it from the _permittedExts array.
|
||||
return false;
|
||||
}
|
||||
@@ -81,7 +93,6 @@ export abstract class UmbBaseExtensionsInitializer<
|
||||
if (!existing) {
|
||||
// Idea: could be abstracted into a createController method, so we can override it in a subclass.
|
||||
// (This should be enough to be able to create a element extension controller instead.)
|
||||
|
||||
this._extensions.push(this._createController(manifest));
|
||||
}
|
||||
});
|
||||
@@ -103,6 +114,7 @@ export abstract class UmbBaseExtensionsInitializer<
|
||||
hasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanged) {
|
||||
// The final list of permitted extensions to be displayed, this will be stripped from extensions that are overwritten by another extension and sorted accordingly.
|
||||
const exposedPermittedExts = [...this._permittedExts];
|
||||
@@ -121,6 +133,10 @@ export abstract class UmbBaseExtensionsInitializer<
|
||||
// Sorting:
|
||||
exposedPermittedExts.sort((a, b) => b.weight - a.weight);
|
||||
|
||||
if (exposedPermittedExts.length > 0) {
|
||||
this.#promiseResolvers.forEach((x) => x());
|
||||
this.#promiseResolvers = [];
|
||||
}
|
||||
this.#onChange?.(exposedPermittedExts);
|
||||
}
|
||||
};
|
||||
@@ -141,9 +157,17 @@ export abstract class UmbBaseExtensionsInitializer<
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
// The this.#extensionRegistry is an indication of wether this is already destroyed.
|
||||
if (!this.#extensionRegistry) return;
|
||||
|
||||
const oldPermittedExtsLength = this._permittedExts.length;
|
||||
this._extensions.length = 0;
|
||||
this._permittedExts.length = 0;
|
||||
this.#onChange?.(this._permittedExts);
|
||||
if (oldPermittedExtsLength > 0) {
|
||||
this.#onChange?.(this._permittedExts);
|
||||
}
|
||||
this.#onChange = undefined;
|
||||
(this.#extensionRegistry as any) = undefined;
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { createExtensionApi } from '../functions/create-extension-api.function.js';
|
||||
import { UmbExtensionRegistry } from '../registry/extension.registry.js';
|
||||
import { isManifestApiType } from '../type-guards/is-manifest-apiable-type.function.js';
|
||||
import { UmbApi, ManifestApi, ManifestCondition } from '../types.js';
|
||||
import type { UmbApi } from '../models/api.interface.js';
|
||||
import type { UmbExtensionRegistry } from '../registry/extension.registry.js';
|
||||
import type { ManifestApi, ManifestCondition } from '../types/index.js';
|
||||
import { UmbBaseExtensionInitializer } from './base-extension-initializer.controller.js';
|
||||
import { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
import { type UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
/**
|
||||
* This Controller manages a single Extension and its API instance.
|
||||
* When the extension is permitted to be used, its API will be instantiated and available for the consumer.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const controller = new UmbExtensionApiController(host, extensionRegistry, alias, [], (permitted, ctrl) => { ctrl.api.helloWorld() }));
|
||||
* ```
|
||||
* ```ts
|
||||
* const controller = new UmbExtensionApiController(host, extensionRegistry, alias, [], (permitted, ctrl) => { ctrl.api.helloWorld() }));
|
||||
* ```
|
||||
* @export
|
||||
* @class UmbExtensionApiController
|
||||
*/
|
||||
export class UmbExtensionApiInitializer<
|
||||
ManifestType extends ManifestApi = ManifestApi,
|
||||
ManifestType extends ManifestApi = ManifestApi,
|
||||
ControllerType extends UmbExtensionApiInitializer<ManifestType, any> = any,
|
||||
ExtensionApiInterface extends UmbApi = ManifestType extends ManifestApi ? NonNullable<ManifestType['API_TYPE']> : UmbApi
|
||||
ExtensionApiInterface extends UmbApi = ManifestType extends ManifestApi
|
||||
? NonNullable<ManifestType['API_TYPE']>
|
||||
: UmbApi,
|
||||
> extends UmbBaseExtensionInitializer<ManifestType, ControllerType> {
|
||||
|
||||
#api?: ExtensionApiInterface;
|
||||
#constructorArguments?: Array<unknown>;
|
||||
|
||||
@@ -35,7 +35,6 @@ export class UmbExtensionApiInitializer<
|
||||
return this.#api;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The props that are passed to the class.
|
||||
* @type {Record<string, any>}
|
||||
@@ -67,7 +66,7 @@ export class UmbExtensionApiInitializer<
|
||||
extensionRegistry: UmbExtensionRegistry<ManifestCondition>,
|
||||
alias: string,
|
||||
constructorArguments: Array<unknown> | undefined,
|
||||
onPermissionChanged?: (isPermitted: boolean, controller: ControllerType) => void
|
||||
onPermissionChanged?: (isPermitted: boolean, controller: ControllerType) => void,
|
||||
) {
|
||||
super(host, extensionRegistry, 'extApi_', alias, onPermissionChanged);
|
||||
this.#constructorArguments = constructorArguments;
|
||||
@@ -88,23 +87,22 @@ export class UmbExtensionApiInitializer<
|
||||
protected async _conditionsAreGood() {
|
||||
const manifest = this.manifest!; // In this case we are sure its not undefined.
|
||||
|
||||
if (isManifestApiType(manifest)) {
|
||||
const newApi = await createExtensionApi<ExtensionApiInterface>(manifest as unknown as ManifestApi<ExtensionApiInterface>, this.#constructorArguments);
|
||||
if (!this._positive) {
|
||||
// We are not positive anymore, so we will back out of this creation.
|
||||
return false;
|
||||
}
|
||||
this.#api = newApi;
|
||||
|
||||
} else {
|
||||
this.#api = undefined;
|
||||
console.warn('Manifest did not provide any useful data for a api class to construct.')
|
||||
const newApi = await createExtensionApi<ExtensionApiInterface>(
|
||||
manifest as unknown as ManifestApi<ExtensionApiInterface>,
|
||||
this.#constructorArguments,
|
||||
);
|
||||
if (!this._isConditionsPositive) {
|
||||
// We are not positive anymore, so we will back out of this creation.
|
||||
return false;
|
||||
}
|
||||
this.#api = newApi;
|
||||
|
||||
if (this.#api) {
|
||||
//this.#assignProperties();
|
||||
return true; // we will confirm we have a component and are still good to go.
|
||||
}
|
||||
|
||||
console.warn('Manifest did not provide any useful data for a api class to construct.');
|
||||
return false; // we will reject the state, we have no component, we are not good to be shown.
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, fixture } from '@open-wc/testing';
|
||||
import { UmbExtensionRegistry } from '../registry/extension.registry.js';
|
||||
import { ManifestApi, ManifestWithDynamicConditions } from '../types.js';
|
||||
import { ManifestApi, ManifestWithDynamicConditions } from '../types/index.js';
|
||||
import { UmbExtensionApiInitializer } from './index.js';
|
||||
import { UmbBaseController, UmbControllerHost, UmbControllerHostElement, UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { customElement, html } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createExtensionElement } from '../functions/create-extension-element.function.js';
|
||||
import { UmbExtensionRegistry } from '../registry/extension.registry.js';
|
||||
import { isManifestElementableType } from '../type-guards/is-manifest-elementable-type.function.js';
|
||||
import { ManifestCondition, ManifestWithDynamicConditions } from '../types.js';
|
||||
import { ManifestCondition, ManifestWithDynamicConditions } from '../types/index.js';
|
||||
import { UmbBaseExtensionInitializer } from './base-extension-initializer.controller.js';
|
||||
import { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
@@ -10,15 +9,15 @@ import { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
* When the extension is permitted to be used, its Element will be instantiated and available for the consumer.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const controller = new UmbExtensionElementController(host, extensionRegistry, alias, (permitted, ctrl) => { console.log("Extension is permitted and this is the element: ", ctrl.component) }));
|
||||
* ```
|
||||
* ```ts
|
||||
* const controller = new UmbExtensionElementController(host, extensionRegistry, alias, (permitted, ctrl) => { console.log("Extension is permitted and this is the element: ", ctrl.component) }));
|
||||
* ```
|
||||
* @export
|
||||
* @class UmbExtensionElementController
|
||||
*/
|
||||
export class UmbExtensionElementInitializer<
|
||||
ManifestType extends ManifestWithDynamicConditions = ManifestWithDynamicConditions,
|
||||
ControllerType extends UmbExtensionElementInitializer<ManifestType, any> = any
|
||||
ControllerType extends UmbExtensionElementInitializer<ManifestType, any> = any,
|
||||
> extends UmbBaseExtensionInitializer<ManifestType, ControllerType> {
|
||||
#defaultElement?: string;
|
||||
#component?: HTMLElement;
|
||||
@@ -61,7 +60,7 @@ export class UmbExtensionElementInitializer<
|
||||
extensionRegistry: UmbExtensionRegistry<ManifestCondition>,
|
||||
alias: string,
|
||||
onPermissionChanged: (isPermitted: boolean, controller: ControllerType) => void,
|
||||
defaultElement?: string
|
||||
defaultElement?: string,
|
||||
) {
|
||||
super(host, extensionRegistry, 'extElement_', alias, onPermissionChanged);
|
||||
this.#defaultElement = defaultElement;
|
||||
@@ -80,24 +79,18 @@ export class UmbExtensionElementInitializer<
|
||||
protected async _conditionsAreGood() {
|
||||
const manifest = this.manifest!; // In this case we are sure its not undefined.
|
||||
|
||||
if (isManifestElementableType(manifest)) {
|
||||
const newComponent = await createExtensionElement(manifest, this.#defaultElement);
|
||||
if (!this._positive) {
|
||||
// We are not positive anymore, so we will back out of this creation.
|
||||
return false;
|
||||
}
|
||||
this.#component = newComponent;
|
||||
|
||||
} else if (this.#defaultElement) {
|
||||
this.#component = document.createElement(this.#defaultElement);
|
||||
} else {
|
||||
this.#component = undefined;
|
||||
console.warn('Manifest did not provide any useful data for a web component to be created.')
|
||||
const newComponent = await createExtensionElement(manifest, this.#defaultElement);
|
||||
if (!this._isConditionsPositive) {
|
||||
// We are not positive anymore, so we will back out of this creation.
|
||||
return false;
|
||||
}
|
||||
this.#component = newComponent;
|
||||
if (this.#component) {
|
||||
this.#assignProperties();
|
||||
(this.#component as any).manifest = manifest;
|
||||
return true; // we will confirm we have a component and are still good to go.
|
||||
} else {
|
||||
console.warn('Manifest did not provide any useful data for a web component to be created.');
|
||||
}
|
||||
|
||||
return false; // we will reject the state, we have no component, we are not good to be shown.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ManifestCondition, ManifestWithDynamicConditions } from '../types.js';
|
||||
import type { ManifestCondition, ManifestWithDynamicConditions } from '../types/index.js';
|
||||
import type { UmbExtensionRegistry } from '../registry/extension.registry.js';
|
||||
import { UmbBaseExtensionInitializer } from './base-extension-initializer.controller.js';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ManifestTypeMap, SpecificManifestTypeOrManifestBase } from '../types/map.types.js';
|
||||
import { type PermittedControllerType, UmbBaseExtensionsInitializer } from './base-extensions-initializer.controller.js';
|
||||
import { UmbExtensionApiInitializer } from './extension-api-initializer.controller.js';
|
||||
import {
|
||||
type ManifestTypeMap,
|
||||
type SpecificManifestTypeOrManifestBase,
|
||||
type UmbExtensionRegistry,
|
||||
ManifestApi,
|
||||
ManifestBase,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { type PermittedControllerType, UmbBaseExtensionsInitializer } from './base-extensions-initializer.controller.js';
|
||||
import type { ManifestTypeMap, SpecificManifestTypeOrManifestBase } from '../types/map.types.js';
|
||||
import {
|
||||
type PermittedControllerType,
|
||||
UmbBaseExtensionsInitializer,
|
||||
} from './base-extensions-initializer.controller.js';
|
||||
import {
|
||||
type ManifestBase,
|
||||
type ManifestTypeMap,
|
||||
type SpecificManifestTypeOrManifestBase,
|
||||
UmbExtensionElementInitializer,
|
||||
type UmbExtensionRegistry,
|
||||
} from '@umbraco-cms/backoffice/extension-api';
|
||||
@@ -10,12 +12,12 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
/**
|
||||
*/
|
||||
export class UmbExtensionsElementController<
|
||||
export class UmbExtensionsElementInitializer<
|
||||
ManifestTypes extends ManifestBase,
|
||||
ManifestTypeName extends keyof ManifestTypeMap<ManifestTypes> | string = string,
|
||||
ManifestTypeName extends keyof ManifestTypeMap<ManifestTypes> | string = ManifestTypes['type'],
|
||||
ManifestType extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, ManifestTypeName>,
|
||||
ControllerType extends UmbExtensionElementInitializer<ManifestType> = UmbExtensionElementInitializer<ManifestType>,
|
||||
MyPermittedControllerType extends ControllerType = PermittedControllerType<ControllerType>
|
||||
MyPermittedControllerType extends ControllerType = PermittedControllerType<ControllerType>,
|
||||
> extends UmbBaseExtensionsInitializer<
|
||||
ManifestTypes,
|
||||
ManifestTypeName,
|
||||
@@ -44,7 +46,7 @@ export class UmbExtensionsElementController<
|
||||
type: ManifestTypeName | Array<ManifestTypeName>,
|
||||
filter: undefined | null | ((manifest: ManifestType) => boolean),
|
||||
onChange: (permittedManifests: Array<MyPermittedControllerType>) => void,
|
||||
defaultElement?: string
|
||||
defaultElement?: string,
|
||||
) {
|
||||
super(host, extensionRegistry, type, filter, onChange);
|
||||
this.#extensionRegistry = extensionRegistry;
|
||||
@@ -58,7 +60,7 @@ export class UmbExtensionsElementController<
|
||||
this.#extensionRegistry,
|
||||
manifest.alias,
|
||||
this._extensionChanged,
|
||||
this._defaultElement
|
||||
this._defaultElement,
|
||||
) as ControllerType;
|
||||
|
||||
extController.properties = this.#props;
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import type { ManifestTypeMap, SpecificManifestTypeOrManifestBase } from '../types/map.types.js';
|
||||
import { UmbExtensionManifestInitializer } from './extension-manifest-initializer.controller.js';
|
||||
import { type PermittedControllerType, UmbBaseExtensionsInitializer } from './base-extensions-initializer.controller.js';
|
||||
import {
|
||||
ManifestBase,
|
||||
ManifestTypeMap,
|
||||
SpecificManifestTypeOrManifestBase,
|
||||
UmbExtensionRegistry,
|
||||
type ManifestBase,
|
||||
type UmbExtensionRegistry,
|
||||
} from '@umbraco-cms/backoffice/extension-api';
|
||||
import { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
/**
|
||||
*/
|
||||
export class UmbExtensionsManifestController<
|
||||
export class UmbExtensionsManifestInitializer<
|
||||
ManifestTypes extends ManifestBase,
|
||||
ManifestTypeName extends keyof ManifestTypeMap<ManifestTypes> | string,
|
||||
ManifestType extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, ManifestTypeName>,
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import { hasApiExport, hasDefaultExport, isManifestApiConstructorType } from '../type-guards/index.js';
|
||||
import type { ManifestApi, ClassConstructor, ManifestElementAndApi, UmbApi } from '../types.js';
|
||||
import { loadExtensionApi } from '../functions/load-extension-api.function.js';
|
||||
import { UmbApi } from "../models/api.interface.js";
|
||||
import { ManifestApi, ManifestElementAndApi } from "../types/base.types.js";
|
||||
import { loadManifestApi } from "./load-manifest-api.function.js";
|
||||
|
||||
//TODO: Write tests for this method:
|
||||
export async function createExtensionApi<ApiType extends UmbApi = UmbApi>(
|
||||
manifest: ManifestApi<ApiType> | ManifestElementAndApi<any, ApiType>,
|
||||
constructorArguments: unknown[] = []
|
||||
): Promise<ApiType | undefined> {
|
||||
const js = await loadExtensionApi(manifest);
|
||||
export async function createExtensionApi<ApiType extends UmbApi = UmbApi>(manifest: ManifestApi<ApiType> | ManifestElementAndApi<any, ApiType>, constructorArguments: Array<unknown> = []): Promise<ApiType | undefined> {
|
||||
|
||||
if (isManifestApiConstructorType<ApiType>(manifest)) {
|
||||
return new manifest.api(...constructorArguments);
|
||||
if(manifest.api) {
|
||||
const apiConstructor = await loadManifestApi<ApiType>(manifest.api);
|
||||
if(apiConstructor) {
|
||||
return new apiConstructor(...constructorArguments);
|
||||
} else {
|
||||
console.error(
|
||||
`-- Extension of alias "${manifest.alias}" did not succeed instantiate a API class via the extension manifest property 'api', using either a 'api' or 'default' export`,
|
||||
manifest
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (js) {
|
||||
if (hasApiExport<ClassConstructor<ApiType>>(js)) {
|
||||
return new js.api(...constructorArguments);
|
||||
if(manifest.js) {
|
||||
const apiConstructor2 = await loadManifestApi<ApiType>(manifest.js);
|
||||
if(apiConstructor2) {
|
||||
return new apiConstructor2(...constructorArguments);
|
||||
} else {
|
||||
console.error(
|
||||
`-- Extension of alias "${manifest.alias}" did not succeed instantiate a API class via the extension manifest property 'js', using either a 'api' or 'default' export`,
|
||||
manifest
|
||||
);
|
||||
}
|
||||
if (hasDefaultExport<ClassConstructor<ApiType>>(js)) {
|
||||
return new js.default(...constructorArguments);
|
||||
}
|
||||
|
||||
console.error(
|
||||
`-- Extension of alias "${manifest.alias}" did not succeed creating an api class instance, missing a 'api' or 'default' export of the served JavaScript file`,
|
||||
manifest
|
||||
);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
console.error(
|
||||
`-- Extension of alias "${manifest.alias}" did not succeed creating an api class instance, missing a JavaScript file via the 'apiJs' or 'js' property or a ClassConstructor in 'api' in the manifest.`,
|
||||
`-- Extension of alias "${manifest.alias}" did not succeed creating an api class instance, missing a JavaScript file via the 'api' or 'js' property.`,
|
||||
manifest
|
||||
);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect } from '@open-wc/testing';
|
||||
import { ManifestApi, UmbApi } from '../types.js';
|
||||
import { ManifestApi } from '../types/index.js';
|
||||
import { UmbApi } from '../models/api.interface.js';
|
||||
import { createExtensionApi } from './create-extension-api.function.js';
|
||||
|
||||
|
||||
@@ -44,7 +45,7 @@ describe('Extension-Api: Create Extension Api', () => {
|
||||
|
||||
const manifest: ManifestApi = {
|
||||
type: 'my-test-type',
|
||||
alias: 'Umb.Test.CreateExtensionApi',
|
||||
alias: 'Umb.Test.createManifestApi',
|
||||
name: 'pretty name'
|
||||
};
|
||||
|
||||
@@ -56,7 +57,7 @@ describe('Extension-Api: Create Extension Api', () => {
|
||||
|
||||
const manifest: ManifestApi<UmbExtensionApiTrueTestClass> = {
|
||||
type: 'my-test-type',
|
||||
alias: 'Umb.Test.CreateExtensionApi',
|
||||
alias: 'Umb.Test.createManifestApi',
|
||||
name: 'pretty name',
|
||||
api: UmbExtensionApiTrueTestClass
|
||||
};
|
||||
@@ -72,9 +73,9 @@ describe('Extension-Api: Create Extension Api', () => {
|
||||
|
||||
const manifest: ManifestApi<UmbExtensionApiTrueTestClass> = {
|
||||
type: 'my-test-type',
|
||||
alias: 'Umb.Test.CreateExtensionApi',
|
||||
alias: 'Umb.Test.createManifestApi',
|
||||
name: 'pretty name',
|
||||
loader: () => Promise.resolve(jsModuleWithDefaultExport)
|
||||
js: () => Promise.resolve(jsModuleWithDefaultExport)
|
||||
};
|
||||
|
||||
const api = await createExtensionApi(manifest, []);
|
||||
@@ -88,9 +89,9 @@ describe('Extension-Api: Create Extension Api', () => {
|
||||
|
||||
const manifest: ManifestApi<UmbExtensionApiTrueTestClass> = {
|
||||
type: 'my-test-type',
|
||||
alias: 'Umb.Test.CreateExtensionApi',
|
||||
alias: 'Umb.Test.createManifestApi',
|
||||
name: 'pretty name',
|
||||
loader: () => Promise.resolve(jsModuleWithApiExport)
|
||||
js: () => Promise.resolve(jsModuleWithApiExport)
|
||||
};
|
||||
|
||||
const api = await createExtensionApi(manifest, []);
|
||||
@@ -104,9 +105,9 @@ describe('Extension-Api: Create Extension Api', () => {
|
||||
|
||||
const manifest: ManifestApi<UmbExtensionApiTrueTestClass> = {
|
||||
type: 'my-test-type',
|
||||
alias: 'Umb.Test.CreateExtensionApi',
|
||||
alias: 'Umb.Test.createManifestApi',
|
||||
name: 'pretty name',
|
||||
loader: () => Promise.resolve(jsModuleWithDefaultAndApiExport)
|
||||
js: () => Promise.resolve(jsModuleWithDefaultAndApiExport)
|
||||
};
|
||||
|
||||
const api = await createExtensionApi(manifest, []);
|
||||
|
||||
@@ -1,45 +1,42 @@
|
||||
import { hasDefaultExport, hasElementExport, isManifestElementNameType } from '../type-guards/index.js';
|
||||
import type { HTMLElementConstructor, ManifestElement } from '../types.js';
|
||||
import { loadExtensionElement } from '../functions/load-extension-element.function.js';
|
||||
import { ManifestElement, ManifestElementAndApi } from "../types/base.types.js";
|
||||
import { loadManifestElement } from "./load-manifest-element.function.js";
|
||||
|
||||
export async function createExtensionElement<ElementType extends HTMLElement>(
|
||||
manifest: ManifestElement<ElementType>, fallbackElementName?: string
|
||||
): Promise<ElementType | undefined> {
|
||||
//TODO: Write tests for these extension options:
|
||||
const js = await loadExtensionElement(manifest);
|
||||
export async function createExtensionElement<ElementType extends HTMLElement>(manifest: ManifestElement<ElementType> | ManifestElementAndApi<ElementType>, fallbackElement?: string): Promise<ElementType | undefined> {
|
||||
|
||||
if (isManifestElementNameType(manifest)) {
|
||||
// created by manifest method providing HTMLElement
|
||||
if(manifest.element) {
|
||||
const elementConstructor = await loadManifestElement<ElementType>(manifest.element);
|
||||
if(elementConstructor) {
|
||||
return new elementConstructor();
|
||||
} else {
|
||||
console.error(
|
||||
`-- Extension of alias "${manifest.alias}" did not succeed creating an element class instance via the extension manifest property 'element', using either a 'element' or 'default' export`,
|
||||
manifest
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if(manifest.js) {
|
||||
const elementConstructor2 = await loadManifestElement<ElementType>(manifest.js);
|
||||
if(elementConstructor2) {
|
||||
return new elementConstructor2();
|
||||
} else {
|
||||
console.error(
|
||||
`-- Extension of alias "${manifest.alias}" did not succeed creating an element class instance via the extension manifest property 'js', using either a 'element' or 'default' export`,
|
||||
manifest
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if(manifest.elementName) {
|
||||
return document.createElement(manifest.elementName) as ElementType;
|
||||
}
|
||||
|
||||
// TODO: Do we need this except for the default() loader?
|
||||
if (js) {
|
||||
if (hasElementExport<HTMLElementConstructor<ElementType>>(js)) {
|
||||
// Element will be created by default class
|
||||
return new js.element();
|
||||
}
|
||||
if (hasDefaultExport<HTMLElementConstructor<ElementType>>(js)) {
|
||||
// Element will be created by default class
|
||||
return new js.default();
|
||||
}
|
||||
|
||||
if(!fallbackElementName) {
|
||||
console.error(
|
||||
`-- Extension of alias "${manifest.alias}" did not succeed creating an api class instance, missing a 'element' or 'default' export of the served JavaScript file`,
|
||||
manifest
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
if(fallbackElement) {
|
||||
return document.createElement(fallbackElement) as ElementType;
|
||||
}
|
||||
|
||||
if(fallbackElementName) {
|
||||
return document.createElement(fallbackElementName) as ElementType;
|
||||
}
|
||||
|
||||
// If some JS was loaded and manifest did not have a elementName neither it the JS file contain a default export, so we will fail:
|
||||
console.error(
|
||||
`-- Extension of alias "${manifest.alias}" did not succeed creating an element, missing a JavaScript file via the 'elementJs' or 'js' property or a Element Name in 'elementName' in the manifest.`,
|
||||
`-- Extension of alias "${manifest.alias}" did not succeed creating an element, missing a JavaScript file via the 'element' or 'js' property or a Element Name in 'elementName' in the manifest.`,
|
||||
manifest
|
||||
);
|
||||
return undefined;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect } from '@open-wc/testing';
|
||||
import { ManifestElement, ManifestElementAndApi } from '../types.js';
|
||||
import { ManifestElement, ManifestElementAndApi } from '../types/index.js';
|
||||
import { createExtensionElement } from './create-extension-element.function.js';
|
||||
import { customElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('Extension-Api: Create Extension Element', () => {
|
||||
|
||||
const manifest: ManifestElement = {
|
||||
type: 'my-test-type',
|
||||
alias: 'Umb.Test.CreateExtensionElement',
|
||||
alias: 'Umb.Test.CreateManifestElement',
|
||||
name: 'pretty name'
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('Extension-Api: Create Extension Element', () => {
|
||||
|
||||
const manifest: ManifestElement<UmbExtensionApiTrueTestElement> = {
|
||||
type: 'my-test-type',
|
||||
alias: 'Umb.Test.CreateExtensionElement',
|
||||
alias: 'Umb.Test.CreateManifestElement',
|
||||
name: 'pretty name'
|
||||
};
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('Extension-Api: Create Extension Element', () => {
|
||||
|
||||
const manifest: ManifestElementAndApi<UmbExtensionApiTrueTestElement> = {
|
||||
type: 'my-test-type',
|
||||
alias: 'Umb.Test.CreateExtensionElement',
|
||||
alias: 'Umb.Test.CreateManifestElement',
|
||||
name: 'pretty name',
|
||||
api: class TestApi {}
|
||||
};
|
||||
@@ -87,7 +87,7 @@ describe('Extension-Api: Create Extension Element', () => {
|
||||
|
||||
const manifest: ManifestElement<UmbExtensionApiTrueTestElement> = {
|
||||
type: 'my-test-type',
|
||||
alias: 'Umb.Test.CreateExtensionElement',
|
||||
alias: 'Umb.Test.CreateManifestElement',
|
||||
name: 'pretty name',
|
||||
elementName: 'umb-extension-api-true-test-element'
|
||||
};
|
||||
@@ -103,9 +103,9 @@ describe('Extension-Api: Create Extension Element', () => {
|
||||
|
||||
const manifest: ManifestElement<UmbExtensionApiTrueTestElement> = {
|
||||
type: 'my-test-type',
|
||||
alias: 'Umb.Test.CreateExtensionElement',
|
||||
alias: 'Umb.Test.CreateManifestElement',
|
||||
name: 'pretty name',
|
||||
loader: () => Promise.resolve(jsModuleWithDefaultExport)
|
||||
js: () => Promise.resolve(jsModuleWithDefaultExport)
|
||||
};
|
||||
|
||||
const element = await createExtensionElement(manifest);
|
||||
@@ -119,9 +119,9 @@ describe('Extension-Api: Create Extension Element', () => {
|
||||
|
||||
const manifest: ManifestElement<UmbExtensionApiTrueTestElement> = {
|
||||
type: 'my-test-type',
|
||||
alias: 'Umb.Test.CreateExtensionElement',
|
||||
alias: 'Umb.Test.CreateManifestElement',
|
||||
name: 'pretty name',
|
||||
loader: () => Promise.resolve(jsModuleWithElementExport)
|
||||
js: () => Promise.resolve(jsModuleWithElementExport)
|
||||
};
|
||||
|
||||
const element = await createExtensionElement(manifest);
|
||||
@@ -135,9 +135,9 @@ describe('Extension-Api: Create Extension Element', () => {
|
||||
|
||||
const manifest: ManifestElement<UmbExtensionApiTrueTestElement> = {
|
||||
type: 'my-test-type',
|
||||
alias: 'Umb.Test.CreateExtensionElement',
|
||||
alias: 'Umb.Test.CreateManifestElement',
|
||||
name: 'pretty name',
|
||||
loader: () => Promise.resolve(jsModuleWithDefaultAndElementExport)
|
||||
js: () => Promise.resolve(jsModuleWithDefaultAndElementExport)
|
||||
};
|
||||
|
||||
const element = await createExtensionElement(manifest);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { UmbEntryPointModule } from '../models/entry-point.interface.js';
|
||||
|
||||
/**
|
||||
* Validate if an ESModule exports a known init function called 'onInit'
|
||||
* Validate if an ESModule export has a function called 'onInit'
|
||||
*/
|
||||
export function hasInitExport(obj: unknown): obj is Pick<UmbEntryPointModule, 'onInit'> {
|
||||
return obj !== null && typeof obj === 'object' && 'onInit' in obj;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
export * from './create-extension-api.function.js';
|
||||
export * from './create-extension-element.function.js';
|
||||
export * from './has-init-export.function.js';
|
||||
export * from './load-extension-api.function.js';
|
||||
export * from './load-extension-element.function.js';
|
||||
export * from './load-extension.function.js';
|
||||
export * from './load-manifest-api.function.js';
|
||||
export * from './load-manifest-element.function.js';
|
||||
export * from './load-manifest-plain-js.function.js';
|
||||
export * from './load-manifest-plain-css.function.js';
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { isManifestApiJSType, isManifestJSType, isManifestLoaderType } from '../type-guards/index.js';
|
||||
import type { ManifestWithLoader } from '../types.js';
|
||||
|
||||
export async function loadExtensionApi<T = unknown>(manifest: ManifestWithLoader<T>): Promise<T | null> {
|
||||
try {
|
||||
if (isManifestLoaderType<T>(manifest)) {
|
||||
return manifest.loader();
|
||||
}
|
||||
|
||||
if (isManifestApiJSType<T>(manifest) && manifest.apiJs) {
|
||||
return await import(/* @vite-ignore */ manifest.apiJs);
|
||||
}
|
||||
|
||||
if (isManifestJSType<T>(manifest) && manifest.js) {
|
||||
return await import(/* @vite-ignore */ manifest.js);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.warn('-- Extension failed to load script', manifest, err);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { isManifestElementJSType, isManifestJSType, isManifestLoaderType } from '../type-guards/index.js';
|
||||
import type { ManifestWithLoader } from '../types.js';
|
||||
|
||||
export async function loadExtensionElement<T = unknown>(manifest: ManifestWithLoader<T>): Promise<T | null> {
|
||||
try {
|
||||
if (isManifestLoaderType<T>(manifest)) {
|
||||
return manifest.loader();
|
||||
}
|
||||
|
||||
if (isManifestElementJSType<T>(manifest) && manifest.elementJs) {
|
||||
return await import(/* @vite-ignore */ manifest.elementJs);
|
||||
}
|
||||
|
||||
if (isManifestJSType<T>(manifest) && manifest.js) {
|
||||
return await import(/* @vite-ignore */ manifest.js);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.warn('-- Extension failed to load script', manifest, err);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { isManifestJSType, isManifestLoaderType } from '../type-guards/index.js';
|
||||
import type { ManifestWithLoader } from '../types.js';
|
||||
|
||||
export async function loadExtension<T = unknown>(manifest: ManifestWithLoader<T>): Promise<T | null> {
|
||||
try {
|
||||
if (isManifestLoaderType<T>(manifest)) {
|
||||
return await manifest.loader();
|
||||
}
|
||||
|
||||
if (isManifestJSType<T>(manifest) && manifest.js) {
|
||||
return await import(/* @vite-ignore */ manifest.js);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.warn('-- Extension failed to load script', manifest, err);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { UmbApi } from "../models/api.interface.js";
|
||||
import type { ApiLoaderExports, ApiLoaderProperty, ClassConstructor, ElementAndApiLoaderProperty, ElementLoaderExports } from "../types/utils.js";
|
||||
|
||||
export async function loadManifestApi<ApiType extends UmbApi>(property: ApiLoaderProperty<ApiType> | ElementAndApiLoaderProperty<any, ApiType>): Promise<ClassConstructor<ApiType> | undefined> {
|
||||
const propType = typeof property
|
||||
if(propType === 'function') {
|
||||
if((property as ClassConstructor).prototype) {
|
||||
// Class Constructor
|
||||
return property as ClassConstructor<ApiType>;
|
||||
} else {
|
||||
// Promise function
|
||||
const result = await (property as (Exclude<Exclude<ApiLoaderProperty<ApiType>, string>, ClassConstructor<ApiType>>))();
|
||||
if(typeof result === 'object' && result != null) {
|
||||
const exportValue = 'api' in result ? result.api : undefined || 'default' in result ? (result as Exclude<(typeof result), ElementLoaderExports>).default : undefined;
|
||||
if(exportValue && typeof exportValue === 'function') {
|
||||
return exportValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if(propType === 'string') {
|
||||
// Import string
|
||||
const result = await (import(/* @vite-ignore */ property as string) as unknown as ApiLoaderExports<ApiType>);
|
||||
if(typeof result === 'object' && result != null) {
|
||||
const exportValue = 'api' in result ? result.api : undefined || 'default' in result ? result.default : undefined;
|
||||
if(exportValue && typeof exportValue === 'function') {
|
||||
return exportValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { ApiLoaderExports, ClassConstructor, ElementAndApiLoaderProperty, ElementLoaderExports, ElementLoaderProperty } from "../types/utils.js";
|
||||
|
||||
|
||||
export async function loadManifestElement<ElementType extends HTMLElement>(property: ElementLoaderProperty<ElementType> | ElementAndApiLoaderProperty<ElementType>): Promise<ClassConstructor<ElementType> | undefined> {
|
||||
const propType = typeof property
|
||||
if(propType === 'function') {
|
||||
if((property as ClassConstructor).prototype) {
|
||||
// Class Constructor
|
||||
return property as ClassConstructor<ElementType>;
|
||||
} else {
|
||||
// Promise function
|
||||
const result = await (property as (Exclude<Exclude<typeof property, string>, ClassConstructor<ElementType>>))();
|
||||
if(typeof result === 'object' && result !== null) {
|
||||
const exportValue = 'element' in result ? result.element : undefined || 'default' in result ? (result as Exclude<(typeof result), ApiLoaderExports>).default : undefined;
|
||||
if(exportValue && typeof exportValue === 'function') {
|
||||
return exportValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if(propType === 'string') {
|
||||
// Import string
|
||||
const result = await (import(/* @vite-ignore */ property as string) as unknown as ElementLoaderExports<ElementType>);
|
||||
if(typeof result === 'object' && result != null) {
|
||||
const exportValue = 'element' in result ? result.element : undefined || 'default' in result ? result.default : undefined;
|
||||
if(exportValue && typeof exportValue === 'function') {
|
||||
return exportValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { JsLoaderProperty } from "../types/utils.js";
|
||||
|
||||
export async function loadManifestPlainCss<CssType = string>(property: JsLoaderProperty<CssType>): Promise<CssType | undefined> {
|
||||
const propType = typeof property;
|
||||
if(propType === 'function') {
|
||||
const result = await (property as (Exclude<(typeof property), string>))();
|
||||
if(result != null) {
|
||||
return result;
|
||||
}
|
||||
} else if(propType === 'string') {
|
||||
// Import string
|
||||
const result = await (import(/* @vite-ignore */ property as string) as unknown as CssType);
|
||||
if(result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { JsLoaderProperty } from "../types/utils.js";
|
||||
|
||||
export async function loadManifestPlainJs<JsType extends object>(property: JsLoaderProperty<JsType>): Promise<JsType | undefined> {
|
||||
const propType = typeof property;
|
||||
if(propType === 'function') {
|
||||
const result = await (property as (Exclude<(typeof property), string>))();
|
||||
if(typeof result === 'object' && result != null) {
|
||||
return result;
|
||||
}
|
||||
} else if(propType === 'string') {
|
||||
// Import string
|
||||
const result = await (import(/* @vite-ignore */ property as string) as unknown as JsType);
|
||||
if(typeof result === 'object' && result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
export * from './initializers/index.js';
|
||||
export * from './condition/index.js';
|
||||
export * from './controller/index.js';
|
||||
export * from './functions/index.js';
|
||||
export * from './initializers/index.js';
|
||||
export * from './models/index.js';
|
||||
export * from './registry/extension.registry.js';
|
||||
export * from './type-guards/index.js';
|
||||
export * from './types.js';
|
||||
export * from './types/index.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ManifestBase, ManifestBundle } from '../types.js';
|
||||
import type { ManifestBase, ManifestBundle } from '../types/index.js';
|
||||
import { UmbExtensionRegistry } from '../registry/extension.registry.js';
|
||||
import { loadExtension } from '../functions/load-extension.function.js';
|
||||
import { loadManifestPlainJs } from '../functions/load-manifest-plain-js.function.js';
|
||||
import { UmbBaseController, UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
export class UmbBundleExtensionInitializer extends UmbBaseController {
|
||||
@@ -29,34 +29,38 @@ export class UmbBundleExtensionInitializer extends UmbBaseController {
|
||||
}
|
||||
|
||||
async instantiateBundle(manifest: ManifestBundle) {
|
||||
const js = await loadExtension(manifest);
|
||||
if(manifest.js) {
|
||||
const js = await loadManifestPlainJs(manifest.js);
|
||||
|
||||
if (js) {
|
||||
Object.keys(js).forEach((key) => {
|
||||
const value = js[key];
|
||||
if (js) {
|
||||
Object.keys(js).forEach((key) => {
|
||||
const value = js[key];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
this.#extensionRegistry.registerMany(value);
|
||||
} else if (typeof value === 'object') {
|
||||
this.#extensionRegistry.register(value);
|
||||
}
|
||||
});
|
||||
if (Array.isArray(value)) {
|
||||
this.#extensionRegistry.registerMany(value);
|
||||
} else if (typeof value === 'object') {
|
||||
this.#extensionRegistry.register(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async unregisterBundle(manifest: ManifestBundle) {
|
||||
const js = await loadExtension(manifest);
|
||||
if(manifest.js) {
|
||||
const js = await loadManifestPlainJs(manifest.js);
|
||||
|
||||
if (js) {
|
||||
Object.keys(js).forEach((key) => {
|
||||
const value = js[key];
|
||||
if (js) {
|
||||
Object.keys(js).forEach((key) => {
|
||||
const value = js[key];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
this.#extensionRegistry.unregisterMany(value.map((v) => v.alias));
|
||||
} else if (typeof value === 'object') {
|
||||
this.#extensionRegistry.unregister((value as ManifestBase).alias);
|
||||
}
|
||||
});
|
||||
if (Array.isArray(value)) {
|
||||
this.#extensionRegistry.unregisterMany(value.map((v) => v.alias));
|
||||
} else if (typeof value === 'object') {
|
||||
this.#extensionRegistry.unregister((value as ManifestBase).alias);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UmbBaseController } from '../../controller-api/controller.class.js';
|
||||
import type { ManifestEntryPoint } from '../types.js';
|
||||
import type { ManifestEntryPoint } from '../types/index.js';
|
||||
import { UmbExtensionRegistry } from '../registry/extension.registry.js';
|
||||
import { hasInitExport, loadExtension } from '../functions/index.js';
|
||||
import { hasInitExport, loadManifestPlainJs } from '../functions/index.js';
|
||||
import { UmbElement } from '@umbraco-cms/backoffice/element-api';
|
||||
|
||||
export class UmbEntryPointExtensionInitializer extends UmbBaseController {
|
||||
@@ -25,10 +25,12 @@ export class UmbEntryPointExtensionInitializer extends UmbBaseController {
|
||||
}
|
||||
|
||||
async instantiateEntryPoint(manifest: ManifestEntryPoint) {
|
||||
const js = await loadExtension(manifest);
|
||||
// If the extension has an onInit export, be sure to run that or else let the module handle itself
|
||||
if (hasInitExport(js)) {
|
||||
js.onInit(this.#host, this.#extensionRegistry);
|
||||
if(manifest.js) {
|
||||
const js = await loadManifestPlainJs(manifest.js);
|
||||
// If the extension has an onInit export, be sure to run that or else let the module handle itself
|
||||
if (hasInitExport(js)) {
|
||||
js.onInit(this.#host, this.#extensionRegistry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface UmbApi {
|
||||
destroy?(): void;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { UmbExtensionRegistry } from '../registry/extension.registry.js';
|
||||
import { ManifestBase } from '../types.js';
|
||||
import { ManifestBase } from '../types/index.js';
|
||||
import type { UmbElement } from '@umbraco-cms/backoffice/element-api';
|
||||
|
||||
export type UmbEntryPointOnInit = (host: UmbElement, extensionRegistry: UmbExtensionRegistry<ManifestBase>) => void;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './entry-point.interface.js'
|
||||
export * from './api.interface.js'
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { expect } from '@open-wc/testing';
|
||||
import type { ManifestElementWithElementName, ManifestKind, ManifestWithMeta } from '../types.js';
|
||||
import type { ManifestElementWithElementName, ManifestKind, ManifestBase } from '../types/index.js';
|
||||
import { UmbExtensionRegistry } from './extension.registry.js';
|
||||
|
||||
interface TestManifestWithMeta extends ManifestBase {
|
||||
meta: unknown;
|
||||
}
|
||||
|
||||
describe('UmbExtensionRegistry', () => {
|
||||
let extensionRegistry: UmbExtensionRegistry<ManifestWithMeta>;
|
||||
let manifests: Array<ManifestWithMeta>;
|
||||
let extensionRegistry: UmbExtensionRegistry<TestManifestWithMeta>;
|
||||
let manifests: Array<TestManifestWithMeta>;
|
||||
|
||||
beforeEach(() => {
|
||||
extensionRegistry = new UmbExtensionRegistry();
|
||||
@@ -170,7 +174,7 @@ describe('UmbExtensionRegistry', () => {
|
||||
describe('UmbExtensionRegistry with kinds', () => {
|
||||
let extensionRegistry: UmbExtensionRegistry<any>;
|
||||
let manifests: Array<
|
||||
ManifestElementWithElementName | ManifestWithMeta | ManifestKind<ManifestElementWithElementName>
|
||||
ManifestElementWithElementName | TestManifestWithMeta | ManifestKind<ManifestElementWithElementName>
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ManifestTypeMap, ManifestBase, SpecificManifestTypeOrManifestBase, ManifestKind } from '../types.js';
|
||||
import type { ManifestBase, ManifestKind } from '../types/index.js';
|
||||
import { ManifestTypeMap, SpecificManifestTypeOrManifestBase } from '../types/map.types.js';
|
||||
import { UmbBasicState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import {
|
||||
map,
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
|
||||
function extensionArrayMemoization<T extends Pick<ManifestBase, 'alias'>>(
|
||||
previousValue: Array<T>,
|
||||
currentValue: Array<T>
|
||||
currentValue: Array<T>,
|
||||
): boolean {
|
||||
// If length is different, data is different:
|
||||
if (previousValue.length !== currentValue.length) {
|
||||
@@ -24,10 +25,9 @@ function extensionArrayMemoization<T extends Pick<ManifestBase, 'alias'>>(
|
||||
return true;
|
||||
}
|
||||
|
||||
function extensionAndKindMatchArrayMemoization<T extends Pick<ManifestBase, 'alias'> & { isMatchedWithKind?: boolean }>(
|
||||
previousValue: Array<T>,
|
||||
currentValue: Array<T>
|
||||
): boolean {
|
||||
function extensionAndKindMatchArrayMemoization<
|
||||
T extends Pick<ManifestBase, 'alias'> & { __isMatchedWithKind?: boolean },
|
||||
>(previousValue: Array<T>, currentValue: Array<T>): boolean {
|
||||
// If length is different, data is different:
|
||||
if (previousValue.length !== currentValue.length) {
|
||||
return false;
|
||||
@@ -42,8 +42,8 @@ function extensionAndKindMatchArrayMemoization<T extends Pick<ManifestBase, 'ali
|
||||
currentValue.find((newValue: T) => {
|
||||
const oldValue = previousValue.find((c) => c.alias === newValue.alias);
|
||||
// First check if we found a previous value, matching this alias.
|
||||
// Then checking isMatchedWithKind, as this is much more performant than checking the whole object. (I assume the only change happening to an extension is the match with a kind, we do not want to watch for other changes)
|
||||
return oldValue === undefined || newValue.isMatchedWithKind !== oldValue.isMatchedWithKind;
|
||||
// Then checking __isMatchedWithKind, as this is much more performant than checking the whole object. (I assume the only change happening to an extension is the match with a kind, we do not want to watch for other changes)
|
||||
return oldValue === undefined || newValue.__isMatchedWithKind !== oldValue.__isMatchedWithKind;
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
@@ -53,7 +53,7 @@ function extensionAndKindMatchArrayMemoization<T extends Pick<ManifestBase, 'ali
|
||||
|
||||
function extensionSingleMemoization<T extends Pick<ManifestBase, 'alias'>>(
|
||||
previousValue: T | undefined,
|
||||
currentValue: T | undefined
|
||||
currentValue: T | undefined,
|
||||
): boolean {
|
||||
if (previousValue && currentValue) {
|
||||
return previousValue.alias === currentValue.alias;
|
||||
@@ -62,11 +62,12 @@ function extensionSingleMemoization<T extends Pick<ManifestBase, 'alias'>>(
|
||||
}
|
||||
|
||||
function extensionAndKindMatchSingleMemoization<
|
||||
T extends Pick<ManifestBase, 'alias'> & { isMatchedWithKind?: boolean }
|
||||
T extends Pick<ManifestBase, 'alias'> & { __isMatchedWithKind?: boolean },
|
||||
>(previousValue: T | undefined, currentValue: T | undefined): boolean {
|
||||
if (previousValue && currentValue) {
|
||||
return (
|
||||
previousValue.alias === currentValue.alias && previousValue.isMatchedWithKind === currentValue.isMatchedWithKind
|
||||
previousValue.alias === currentValue.alias &&
|
||||
previousValue.__isMatchedWithKind === currentValue.__isMatchedWithKind
|
||||
);
|
||||
}
|
||||
return previousValue === currentValue;
|
||||
@@ -76,7 +77,7 @@ const sortExtensions = (a: ManifestBase, b: ManifestBase) => (b.weight || 0) - (
|
||||
|
||||
export class UmbExtensionRegistry<
|
||||
IncomingManifestTypes extends ManifestBase,
|
||||
ManifestTypes extends ManifestBase = IncomingManifestTypes | ManifestBase
|
||||
ManifestTypes extends ManifestBase = IncomingManifestTypes | ManifestBase,
|
||||
> {
|
||||
readonly MANIFEST_TYPES: ManifestTypes = undefined as never;
|
||||
|
||||
@@ -89,7 +90,7 @@ export class UmbExtensionRegistry<
|
||||
defineKind(kind: ManifestKind<ManifestTypes>) {
|
||||
const extensionsValues = this._extensions.getValue();
|
||||
const extension = extensionsValues.find(
|
||||
(extension) => extension.alias === (kind as ManifestKind<ManifestTypes>).alias
|
||||
(extension) => extension.alias === (kind as ManifestKind<ManifestTypes>).alias,
|
||||
);
|
||||
|
||||
if (extension) {
|
||||
@@ -104,7 +105,7 @@ export class UmbExtensionRegistry<
|
||||
!(
|
||||
k.matchType === (kind as ManifestKind<ManifestTypes>).matchType &&
|
||||
k.matchKind === (kind as ManifestKind<ManifestTypes>).matchKind
|
||||
)
|
||||
),
|
||||
);
|
||||
nextData.push(kind as ManifestKind<ManifestTypes>);
|
||||
this._kinds.next(nextData);
|
||||
@@ -185,29 +186,29 @@ export class UmbExtensionRegistry<
|
||||
private _kindsOfType<Key extends keyof ManifestTypeMap<ManifestTypes> | string>(type: Key) {
|
||||
return this.kinds.pipe(
|
||||
map((kinds) => kinds.filter((kind) => kind.matchType === type)),
|
||||
distinctUntilChanged(extensionArrayMemoization)
|
||||
distinctUntilChanged(extensionArrayMemoization),
|
||||
);
|
||||
}
|
||||
private _extensionsOfType<Key extends keyof ManifestTypeMap<ManifestTypes> | string>(type: Key) {
|
||||
return this.extensions.pipe(
|
||||
map((exts) => exts.filter((ext) => ext.type === type)),
|
||||
distinctUntilChanged(extensionArrayMemoization)
|
||||
distinctUntilChanged(extensionArrayMemoization),
|
||||
);
|
||||
}
|
||||
private _kindsOfTypes(types: string[]) {
|
||||
return this.kinds.pipe(
|
||||
map((kinds) => kinds.filter((kind) => types.indexOf(kind.matchType) !== -1)),
|
||||
distinctUntilChanged(extensionArrayMemoization)
|
||||
distinctUntilChanged(extensionArrayMemoization),
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: can we get rid of as unknown here
|
||||
private _extensionsOfTypes<ExtensionType extends ManifestBase = ManifestBase>(
|
||||
types: Array<ExtensionType['type']>
|
||||
types: Array<ExtensionType['type']>,
|
||||
): Observable<Array<ExtensionType>> {
|
||||
return this.extensions.pipe(
|
||||
map((exts) => exts.filter((ext) => types.indexOf(ext.type) !== -1)),
|
||||
distinctUntilChanged(extensionArrayMemoization)
|
||||
distinctUntilChanged(extensionArrayMemoization),
|
||||
) as unknown as Observable<Array<ExtensionType>>;
|
||||
}
|
||||
|
||||
@@ -225,7 +226,7 @@ export class UmbExtensionRegistry<
|
||||
const baseManifest = kinds.find((kind) => kind.matchKind === ext.kind)?.manifest;
|
||||
// TODO: This check can go away when making a find kind based on type and kind.
|
||||
if (baseManifest) {
|
||||
const merged = { isMatchedWithKind: true, ...baseManifest, ...ext } as any;
|
||||
const merged = { __isMatchedWithKind: true, ...baseManifest, ...ext } as any;
|
||||
if ((baseManifest as any).meta) {
|
||||
merged.meta = { ...(baseManifest as any).meta, ...(ext as any).meta };
|
||||
}
|
||||
@@ -233,24 +234,24 @@ export class UmbExtensionRegistry<
|
||||
}
|
||||
}
|
||||
return ext;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
return of(ext);
|
||||
}),
|
||||
|
||||
distinctUntilChanged(extensionAndKindMatchSingleMemoization)
|
||||
distinctUntilChanged(extensionAndKindMatchSingleMemoization),
|
||||
) as Observable<T | undefined>;
|
||||
}
|
||||
|
||||
getByTypeAndAlias<
|
||||
Key extends keyof ManifestTypeMap<ManifestTypes> | string,
|
||||
T extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, Key>
|
||||
T extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, Key>,
|
||||
>(type: Key, alias: string) {
|
||||
return combineLatest([
|
||||
this.extensions.pipe(
|
||||
map((exts) => exts.find((ext) => ext.type === type && ext.alias === alias)),
|
||||
distinctUntilChanged(extensionSingleMemoization)
|
||||
distinctUntilChanged(extensionSingleMemoization),
|
||||
),
|
||||
this._kindsOfType(type),
|
||||
]).pipe(
|
||||
@@ -260,7 +261,7 @@ export class UmbExtensionRegistry<
|
||||
if (ext) {
|
||||
const baseManifest = kinds.find((kind) => kind.matchKind === ext.kind)?.manifest;
|
||||
if (baseManifest) {
|
||||
const merged = { isMatchedWithKind: true, ...baseManifest, ...ext } as any;
|
||||
const merged = { __isMatchedWithKind: true, ...baseManifest, ...ext } as any;
|
||||
if ((baseManifest as any).meta) {
|
||||
merged.meta = { ...(baseManifest as any).meta, ...(ext as any).meta };
|
||||
}
|
||||
@@ -269,18 +270,18 @@ export class UmbExtensionRegistry<
|
||||
}
|
||||
return ext;
|
||||
}),
|
||||
distinctUntilChanged(extensionAndKindMatchSingleMemoization)
|
||||
distinctUntilChanged(extensionAndKindMatchSingleMemoization),
|
||||
) as Observable<T | undefined>;
|
||||
}
|
||||
|
||||
getByTypeAndAliases<
|
||||
Key extends keyof ManifestTypeMap<ManifestTypes> | string,
|
||||
T extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, Key>
|
||||
T extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, Key>,
|
||||
>(type: Key, aliases: Array<string>) {
|
||||
return combineLatest([
|
||||
this.extensions.pipe(
|
||||
map((exts) => exts.filter((ext) => ext.type === type && aliases.indexOf(ext.alias) !== -1)),
|
||||
distinctUntilChanged(extensionArrayMemoization)
|
||||
distinctUntilChanged(extensionArrayMemoization),
|
||||
),
|
||||
this._kindsOfType(type),
|
||||
]).pipe(
|
||||
@@ -290,7 +291,7 @@ export class UmbExtensionRegistry<
|
||||
// Specific Extension Meta merge (does not merge conditions)
|
||||
const baseManifest = kinds.find((kind) => kind.matchKind === ext.kind)?.manifest;
|
||||
if (baseManifest) {
|
||||
const merged = { isMatchedWithKind: true, ...baseManifest, ...ext } as any;
|
||||
const merged = { __isMatchedWithKind: true, ...baseManifest, ...ext } as any;
|
||||
if ((baseManifest as any).meta) {
|
||||
merged.meta = { ...(baseManifest as any).meta, ...(ext as any).meta };
|
||||
}
|
||||
@@ -298,15 +299,15 @@ export class UmbExtensionRegistry<
|
||||
}
|
||||
return ext;
|
||||
})
|
||||
.sort(sortExtensions)
|
||||
.sort(sortExtensions),
|
||||
),
|
||||
distinctUntilChanged(extensionAndKindMatchArrayMemoization)
|
||||
distinctUntilChanged(extensionAndKindMatchArrayMemoization),
|
||||
) as Observable<Array<T>>;
|
||||
}
|
||||
|
||||
extensionsOfType<
|
||||
Key extends keyof ManifestTypeMap<ManifestTypes> | string,
|
||||
T extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, Key>
|
||||
T extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, Key>,
|
||||
>(type: Key) {
|
||||
return combineLatest([this._extensionsOfType(type), this._kindsOfType(type)]).pipe(
|
||||
map(([exts, kinds]) =>
|
||||
@@ -315,7 +316,7 @@ export class UmbExtensionRegistry<
|
||||
// Specific Extension Meta merge (does not merge conditions)
|
||||
const baseManifest = kinds.find((kind) => kind.matchKind === ext.kind)?.manifest;
|
||||
if (baseManifest) {
|
||||
const merged = { isMatchedWithKind: true, ...baseManifest, ...ext } as any;
|
||||
const merged = { __isMatchedWithKind: true, ...baseManifest, ...ext } as any;
|
||||
if ((baseManifest as any).meta) {
|
||||
merged.meta = { ...(baseManifest as any).meta, ...(ext as any).meta };
|
||||
}
|
||||
@@ -323,14 +324,14 @@ export class UmbExtensionRegistry<
|
||||
}
|
||||
return ext;
|
||||
})
|
||||
.sort(sortExtensions)
|
||||
.sort(sortExtensions),
|
||||
),
|
||||
distinctUntilChanged(extensionAndKindMatchArrayMemoization)
|
||||
distinctUntilChanged(extensionAndKindMatchArrayMemoization),
|
||||
) as Observable<Array<T>>;
|
||||
}
|
||||
|
||||
extensionsOfTypes<ExtensionTypes extends ManifestBase = ManifestBase>(
|
||||
types: string[]
|
||||
types: string[],
|
||||
): Observable<Array<ExtensionTypes>> {
|
||||
return combineLatest([this._extensionsOfTypes(types), this._kindsOfTypes(types)]).pipe(
|
||||
map(([exts, kinds]) =>
|
||||
@@ -340,7 +341,7 @@ export class UmbExtensionRegistry<
|
||||
if (ext) {
|
||||
const baseManifest = kinds.find((kind) => kind.matchKind === ext.kind)?.manifest;
|
||||
if (baseManifest) {
|
||||
const merged = { isMatchedWithKind: true, ...baseManifest, ...ext } as any;
|
||||
const merged = { __isMatchedWithKind: true, ...baseManifest, ...ext } as any;
|
||||
if ((baseManifest as any).meta) {
|
||||
merged.meta = { ...(baseManifest as any).meta, ...(ext as any).meta };
|
||||
}
|
||||
@@ -349,9 +350,9 @@ export class UmbExtensionRegistry<
|
||||
}
|
||||
return ext;
|
||||
})
|
||||
.sort(sortExtensions)
|
||||
.sort(sortExtensions),
|
||||
),
|
||||
distinctUntilChanged(extensionAndKindMatchArrayMemoization)
|
||||
distinctUntilChanged(extensionAndKindMatchArrayMemoization),
|
||||
) as Observable<Array<ExtensionTypes>>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
export * from './is-manifest-api-instance-type.function.js';
|
||||
export * from './is-manifest-apiable-type.function.js';
|
||||
export * from './is-manifest-element-name-type.function.js';
|
||||
export * from './is-manifest-elementable-type.function.js';
|
||||
export * from './is-manifest-js-type.function.js';
|
||||
export * from './is-manifest-element-js-type.function.js';
|
||||
export * from './is-manifest-api-js-type.function.js';
|
||||
export * from './is-manifest-loader-type.function.js';
|
||||
export * from './has-api-export.function.js';
|
||||
export * from './has-default-export.function.js';
|
||||
export * from './has-element-export.function.js';
|
||||
export * from './has-api-export.function.js';
|
||||
export * from './is-manifest-base-type.function.js';
|
||||
export * from './is-manifest-element-name-type.function.js';
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { ClassConstructor, UmbApi, ManifestApi } from '../types.js';
|
||||
|
||||
export function isManifestApiConstructorType<ApiType extends UmbApi>(manifest: unknown): manifest is ManifestApiWithClassConstructor<ApiType> {
|
||||
return typeof manifest === 'object' && manifest !== null && (manifest as ManifestApi).api !== undefined;
|
||||
}
|
||||
|
||||
export interface ManifestApiWithClassConstructor<T extends UmbApi = UmbApi> extends ManifestApi<T> {
|
||||
api: ClassConstructor<T>;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import type { ManifestBase, ManifestWithLoader } from '../types.js';
|
||||
|
||||
export type ManifestApiJSType<T> = ManifestWithLoader<T> & { apiJs: string };
|
||||
export function isManifestApiJSType<T>(manifest: ManifestBase | unknown): manifest is ManifestApiJSType<T> {
|
||||
return (manifest as ManifestApiJSType<T>).apiJs !== undefined;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { ManifestBase, ManifestApi } from '../types.js';
|
||||
import { isManifestJSType } from './is-manifest-js-type.function.js';
|
||||
import { isManifestLoaderType } from './is-manifest-loader-type.function.js';
|
||||
import { isManifestApiConstructorType } from './is-manifest-api-instance-type.function.js';
|
||||
import { isManifestApiJSType } from './is-manifest-api-js-type.function.js';
|
||||
|
||||
export function isManifestApiType(manifest: ManifestBase): manifest is ManifestApi {
|
||||
return (
|
||||
isManifestApiConstructorType(manifest) ||
|
||||
isManifestLoaderType<object>(manifest) ||
|
||||
isManifestApiJSType<object>(manifest) ||
|
||||
isManifestJSType<object>(manifest)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { ManifestBase } from "../types/manifest-base.interface.js";
|
||||
|
||||
export function isManifestBaseType(x: unknown): x is ManifestBase {
|
||||
return typeof x === 'object' && x !== null && 'alias' in x;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import type { ManifestBase, ManifestWithLoader } from '../types.js';
|
||||
|
||||
export type ManifestElementJSType<T> = ManifestWithLoader<T> & { elementJs: string };
|
||||
export function isManifestElementJSType<T>(manifest: ManifestBase | unknown): manifest is ManifestElementJSType<T> {
|
||||
return (manifest as ManifestElementJSType<T>).elementJs !== undefined;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ManifestElement, ManifestElementWithElementName } from '../types.js';
|
||||
import type { ManifestElement, ManifestElementWithElementName } from '../types/index.js';
|
||||
|
||||
export function isManifestElementNameType(manifest: unknown): manifest is ManifestElementWithElementName {
|
||||
return typeof manifest === 'object' && manifest !== null && (manifest as ManifestElement).elementName !== undefined;
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { ManifestElement, ManifestBase } from '../types.js';
|
||||
import { isManifestElementJSType } from './is-manifest-element-js-type.function.js';
|
||||
import { isManifestElementNameType } from './is-manifest-element-name-type.function.js';
|
||||
import { isManifestJSType } from './is-manifest-js-type.function.js';
|
||||
import { isManifestLoaderType } from './is-manifest-loader-type.function.js';
|
||||
|
||||
export function isManifestElementableType<ElementType extends HTMLElement = HTMLElement>(
|
||||
manifest: ManifestBase
|
||||
): manifest is ManifestElement {
|
||||
return (
|
||||
isManifestElementNameType(manifest) ||
|
||||
isManifestLoaderType<ElementType>(manifest) ||
|
||||
isManifestElementJSType<ElementType>(manifest) ||
|
||||
isManifestJSType<ElementType>(manifest)
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import type { ManifestBase, ManifestWithLoader } from '../types.js';
|
||||
|
||||
export type ManifestJSType<T> = ManifestWithLoader<T> & { js: string };
|
||||
export function isManifestJSType<T>(manifest: ManifestBase | unknown): manifest is ManifestJSType<T> {
|
||||
return (manifest as ManifestJSType<T>).js !== undefined;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { ManifestBase, ManifestWithLoader } from '../types.js';
|
||||
|
||||
export type ManifestLoaderType<T> = ManifestWithLoader<T> & {
|
||||
loader: () => Promise<T>;
|
||||
};
|
||||
|
||||
export function isManifestLoaderType<T>(manifest: ManifestBase): manifest is ManifestLoaderType<T> {
|
||||
return typeof (manifest as ManifestLoaderType<T>).loader === 'function';
|
||||
}
|
||||
@@ -1,299 +0,0 @@
|
||||
import type { UmbExtensionCondition } from './condition/index.js';
|
||||
import type { UmbEntryPointModule } from './models/index.js';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type HTMLElementConstructor<T = HTMLElement> = new (...args: any[]) => T;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type ClassConstructor<T = object> = new (...args: any[]) => T;
|
||||
|
||||
export type ManifestTypeMap<ManifestTypes extends ManifestBase> = {
|
||||
[Manifest in ManifestTypes as Manifest['type']]: Manifest;
|
||||
} & {
|
||||
[key: string]: ManifestBase;
|
||||
};
|
||||
|
||||
export type SpecificManifestTypeOrManifestBase<
|
||||
ManifestTypes extends ManifestBase,
|
||||
T extends keyof ManifestTypeMap<ManifestTypes> | string
|
||||
> = T extends keyof ManifestTypeMap<ManifestTypes> ? ManifestTypeMap<ManifestTypes>[T] : ManifestBase;
|
||||
|
||||
export interface ManifestBase {
|
||||
/**
|
||||
* The type of extension such as dashboard etc...
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* The alias of the extension, ensure it is unique
|
||||
*/
|
||||
alias: string;
|
||||
|
||||
/**
|
||||
* The kind of the extension, used to group extensions together
|
||||
*
|
||||
* @examples ["button"]
|
||||
*/
|
||||
kind?: unknown; // I had to add the optional kind property set to undefined. To make the ManifestTypes recognize the Manifest Kind types. Notice that Kinds has to Omit the kind property when extending.
|
||||
|
||||
/**
|
||||
* The friendly name of the extension
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Extensions such as dashboards are ordered by weight with lower numbers being first in the list
|
||||
*/
|
||||
weight?: number;
|
||||
}
|
||||
|
||||
export interface ManifestKind<ManifestTypes> {
|
||||
type: 'kind';
|
||||
alias: string;
|
||||
matchType: string;
|
||||
matchKind: string;
|
||||
manifest: Partial<ManifestTypes>;
|
||||
}
|
||||
|
||||
// TODO: Get rid of this type and implements ManifestWithDynamicConditions instead.
|
||||
export interface ManifestWithConditions<ConditionType> {
|
||||
/**
|
||||
* Set the conditions for when the extension should be loaded
|
||||
*/
|
||||
conditions: ConditionType;
|
||||
}
|
||||
|
||||
export interface UmbConditionConfigBase<AliasType extends string = string> {
|
||||
alias: AliasType;
|
||||
}
|
||||
|
||||
export type ConditionTypeMap<ConditionTypes extends UmbConditionConfigBase> = {
|
||||
[Condition in ConditionTypes as Condition['alias']]: Condition;
|
||||
} & {
|
||||
[key: string]: UmbConditionConfigBase;
|
||||
};
|
||||
|
||||
export type SpecificConditionTypeOrUmbConditionConfigBase<
|
||||
ConditionTypes extends UmbConditionConfigBase,
|
||||
T extends keyof ConditionTypeMap<ConditionTypes> | string
|
||||
> = T extends keyof ConditionTypeMap<ConditionTypes> ? ConditionTypeMap<ConditionTypes>[T] : UmbConditionConfigBase;
|
||||
|
||||
export interface ManifestWithDynamicConditions<ConditionTypes extends UmbConditionConfigBase = UmbConditionConfigBase>
|
||||
extends ManifestBase {
|
||||
/**
|
||||
* Set the conditions for when the extension should be loaded
|
||||
*/
|
||||
conditions?: Array<ConditionTypes>;
|
||||
/**
|
||||
* Define one or more extension aliases that this extension should overwrite.
|
||||
*/
|
||||
overwrites?: string | Array<string>;
|
||||
}
|
||||
|
||||
export interface ManifestWithLoader<LoaderReturnType> extends ManifestBase {
|
||||
/**
|
||||
* @TJS-ignore
|
||||
*/
|
||||
loader?(): Promise<LoaderReturnType>;
|
||||
}
|
||||
|
||||
// TODO: Rename this to something more descriptive:
|
||||
export interface UmbApi {
|
||||
destroy?(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of extension such as dashboard etc...
|
||||
*/
|
||||
export interface ManifestApi<ApiType extends UmbApi = UmbApi>
|
||||
extends ManifestWithLoader<{ default: ClassConstructor<ApiType> } | { api: ClassConstructor<ApiType> }> {
|
||||
/**
|
||||
* @TJS-ignore
|
||||
*/
|
||||
readonly API_TYPE?: ApiType;
|
||||
|
||||
/**
|
||||
* The file location of the javascript file to load
|
||||
* @TJS-required
|
||||
*/
|
||||
js?: string;
|
||||
|
||||
/**
|
||||
* @TJS-ignore
|
||||
*/
|
||||
apiName?: string;
|
||||
|
||||
/**
|
||||
* @TJS-ignore
|
||||
*/
|
||||
api?: ClassConstructor<ApiType>;
|
||||
}
|
||||
|
||||
export interface ManifestWithLoaderIncludingDefaultExport<T = unknown>
|
||||
extends ManifestWithLoader<{ default: T } | { element: T } | Omit<object, 'default'>> {
|
||||
/**
|
||||
* The file location of the javascript file to load
|
||||
*/
|
||||
js?: string;
|
||||
}
|
||||
|
||||
export interface ManifestWithLoaderIncludingApiExport<ApiType extends UmbApi = UmbApi>
|
||||
extends ManifestWithLoader<{ api: ApiType } | Omit<object, 'api'>> {
|
||||
/**
|
||||
* The file location of the javascript file to load
|
||||
*/
|
||||
js?: string;
|
||||
/**
|
||||
* The file location of the API javascript file to load
|
||||
*/
|
||||
apiJs?: string;
|
||||
}
|
||||
|
||||
export interface ManifestWithLoaderIncludingElementExport<ElementType extends HTMLElement = HTMLElement>
|
||||
extends ManifestWithLoader<{ element: ElementType } | Omit<object, 'element'>> {
|
||||
/**
|
||||
* The file location of the javascript file to load
|
||||
*/
|
||||
js?: string;
|
||||
/**
|
||||
* The file location of the element javascript file to load
|
||||
*/
|
||||
elementJs?: string;
|
||||
}
|
||||
export interface ManifestWithLoaderOptionalApiOrElementExport<
|
||||
ElementType extends HTMLElement = HTMLElement,
|
||||
ApiType extends UmbApi = UmbApi,
|
||||
ClassType = { element: ElementType } | { api: ApiType } | { element: ElementType, api: ApiType } | Omit<Omit<object, 'element'>, 'api'>
|
||||
>
|
||||
extends ManifestWithLoader<ClassType> {
|
||||
/**
|
||||
* The file location of the javascript file to load
|
||||
*/
|
||||
js?: string;
|
||||
/**
|
||||
* The file location of the element javascript file to load
|
||||
*/
|
||||
elementJs?: string;
|
||||
/**
|
||||
* The file location of the API javascript file to load
|
||||
*/
|
||||
apiJs?: string;
|
||||
}
|
||||
|
||||
export interface ManifestElement<ElementType extends HTMLElement = HTMLElement>
|
||||
extends ManifestWithLoader<{ default: ClassConstructor<ElementType> } | Omit<object, 'default'>> {
|
||||
/**
|
||||
* @TJS-ignore
|
||||
*/
|
||||
readonly ELEMENT_TYPE?: ElementType;
|
||||
|
||||
/**
|
||||
* The file location of the javascript file to load
|
||||
*
|
||||
* @TJS-require
|
||||
*/
|
||||
js?: string;
|
||||
|
||||
/**
|
||||
* The HTML web component name to use such as 'my-dashboard'
|
||||
* Note it is NOT <my-dashboard></my-dashboard>, just the element name.
|
||||
*/
|
||||
elementName?: string;
|
||||
|
||||
/**
|
||||
* This contains properties specific to the type of extension
|
||||
*/
|
||||
meta?: unknown;
|
||||
}
|
||||
|
||||
export interface ManifestElementAndApi<ElementType extends HTMLElement = HTMLElement, ApiType extends UmbApi = UmbApi>
|
||||
extends ManifestWithLoaderOptionalApiOrElementExport<ElementType, ApiType> {
|
||||
/**
|
||||
* @TJS-ignore
|
||||
*/
|
||||
readonly API_TYPE?: ApiType;
|
||||
/**
|
||||
* @TJS-ignore
|
||||
*/
|
||||
readonly ELEMENT_TYPE?: ElementType;
|
||||
|
||||
|
||||
/**
|
||||
* @TJS-ignore
|
||||
*/
|
||||
apiName?: string;
|
||||
|
||||
/**
|
||||
* @TJS-ignore
|
||||
*/
|
||||
api?: ClassConstructor<ApiType>;
|
||||
|
||||
/**
|
||||
* The HTML web component name to use such as 'my-dashboard'
|
||||
* Note it is NOT <my-dashboard></my-dashboard>, just the element name.
|
||||
*/
|
||||
elementName?: string;
|
||||
}
|
||||
|
||||
export interface ManifestWithView<ElementType extends HTMLElement = HTMLElement> extends ManifestElement<ElementType> {
|
||||
meta: MetaManifestWithView;
|
||||
}
|
||||
|
||||
export interface MetaManifestWithView {
|
||||
pathname: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface ManifestElementWithElementName extends ManifestElement {
|
||||
/**
|
||||
* The HTML web component name to use such as 'my-dashboard'
|
||||
* Note it is NOT <my-dashboard></my-dashboard> but just the name
|
||||
*/
|
||||
elementName: string;
|
||||
}
|
||||
|
||||
export interface ManifestWithMeta extends ManifestBase {
|
||||
/**
|
||||
* This contains properties specific to the type of extension
|
||||
*/
|
||||
meta: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* This type of extension gives full control and will simply load the specified JS file
|
||||
* You could have custom logic to decide which extensions to load/register by using extensionRegistry
|
||||
*/
|
||||
export interface ManifestEntryPoint extends ManifestWithLoader<UmbEntryPointModule> {
|
||||
type: 'entryPoint';
|
||||
|
||||
/**
|
||||
* The file location of the javascript file to load in the backoffice
|
||||
*/
|
||||
js?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This type of extension takes a JS module and registers all exported manifests from the pointed JS file.
|
||||
*/
|
||||
export interface ManifestBundle<UmbManifestTypes extends ManifestBase = ManifestBase>
|
||||
extends ManifestWithLoader<{ [key: string]: Array<UmbManifestTypes> }> {
|
||||
type: 'bundle';
|
||||
|
||||
/**
|
||||
* The file location of the javascript file to load in the backoffice
|
||||
*/
|
||||
js?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This type of extension takes a JS module and registers all exported manifests from the pointed JS file.
|
||||
*/
|
||||
export interface ManifestCondition extends ManifestApi<UmbExtensionCondition> {
|
||||
type: 'condition';
|
||||
|
||||
/**
|
||||
* The file location of the javascript file to load in the backoffice
|
||||
*/
|
||||
js?: string;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { UmbApi } from '../models/index.js';
|
||||
import type { ManifestBase } from './manifest-base.interface.js';
|
||||
import type {
|
||||
ApiLoaderProperty,
|
||||
CssLoaderProperty,
|
||||
ElementAndApiLoaderProperty,
|
||||
ElementLoaderProperty,
|
||||
JsLoaderProperty,
|
||||
} from './utils.js';
|
||||
|
||||
export interface ManifestWithView<ElementType extends HTMLElement = HTMLElement> extends ManifestElement<ElementType> {
|
||||
meta: MetaManifestWithView;
|
||||
}
|
||||
|
||||
export interface MetaManifestWithView {
|
||||
pathname: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface ManifestElementWithElementName extends ManifestElement {
|
||||
/**
|
||||
* The HTML web component name to use such as 'my-dashboard'
|
||||
* Note it is NOT <my-dashboard></my-dashboard> but just the name
|
||||
*/
|
||||
elementName: string;
|
||||
}
|
||||
|
||||
export interface ManifestPlainCss<CssType = unknown> extends ManifestBase {
|
||||
/**
|
||||
* The file location of the stylesheet file to load
|
||||
* @TJS-type string
|
||||
*/
|
||||
css?: CssLoaderProperty<CssType>;
|
||||
}
|
||||
|
||||
export interface ManifestPlainJs<JsType> extends ManifestBase {
|
||||
/**
|
||||
* The file location of the javascript file to load
|
||||
* @TJS-type string
|
||||
*/
|
||||
js?: JsLoaderProperty<JsType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of extension such as dashboard etc...
|
||||
*/
|
||||
export interface ManifestApi<ApiType extends UmbApi = UmbApi> extends ManifestBase {
|
||||
/**
|
||||
* @TJS-ignore
|
||||
*/
|
||||
readonly API_TYPE?: ApiType;
|
||||
|
||||
/**
|
||||
* The file location of the javascript file to load
|
||||
* @TJS-type string
|
||||
*/
|
||||
js?: ApiLoaderProperty<ApiType>;
|
||||
|
||||
/**
|
||||
* @TJS-type string
|
||||
*/
|
||||
api?: ApiLoaderProperty<ApiType>;
|
||||
}
|
||||
|
||||
export interface ManifestElement<ElementType extends HTMLElement = HTMLElement> extends ManifestBase {
|
||||
/**
|
||||
* @TJS-ignore
|
||||
*/
|
||||
readonly ELEMENT_TYPE?: ElementType;
|
||||
|
||||
/**
|
||||
* The file location of the javascript file to load
|
||||
* @TJS-type string
|
||||
*/
|
||||
js?: ElementLoaderProperty<ElementType>;
|
||||
|
||||
/**
|
||||
* The file location of the element javascript file to load
|
||||
* @TJS-type string
|
||||
*/
|
||||
element?: ElementLoaderProperty<ElementType>;
|
||||
|
||||
/**
|
||||
* The HTML web component name to use such as 'my-dashboard'
|
||||
* Note it is NOT <my-dashboard></my-dashboard>, just the element name.
|
||||
*/
|
||||
elementName?: string;
|
||||
|
||||
/**
|
||||
* This contains properties specific to the type of extension
|
||||
*/
|
||||
meta?: unknown;
|
||||
}
|
||||
|
||||
export interface ManifestElementAndApi<ElementType extends HTMLElement = HTMLElement, ApiType extends UmbApi = UmbApi>
|
||||
extends ManifestBase {
|
||||
/**
|
||||
* @TJS-ignore
|
||||
*/
|
||||
readonly API_TYPE?: ApiType;
|
||||
/**
|
||||
* @TJS-ignore
|
||||
*/
|
||||
readonly ELEMENT_TYPE?: ElementType;
|
||||
|
||||
/**
|
||||
* The file location of the javascript file to load
|
||||
* @TJS-type string
|
||||
*/
|
||||
js?: ElementAndApiLoaderProperty<ElementType, ApiType>;
|
||||
|
||||
/**
|
||||
* The file location of the api javascript file to load
|
||||
* @TJS-type string
|
||||
*/
|
||||
api?: ApiLoaderProperty<ApiType>;
|
||||
|
||||
/**
|
||||
* The file location of the element javascript file to load
|
||||
* @TJS-type string
|
||||
*/
|
||||
element?: ElementLoaderProperty<ElementType>;
|
||||
|
||||
/**
|
||||
* The HTML web component name to use such as 'my-dashboard'
|
||||
* Note it is NOT <my-dashboard></my-dashboard>, just the element name.
|
||||
*/
|
||||
elementName?: string;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { ManifestBase } from "./manifest-base.interface.js";
|
||||
|
||||
export interface UmbConditionConfigBase<AliasType extends string = string> {
|
||||
alias: AliasType;
|
||||
}
|
||||
|
||||
export type ConditionTypeMap<ConditionTypes extends UmbConditionConfigBase> = {
|
||||
[Condition in ConditionTypes as Condition['alias']]: Condition;
|
||||
} & {
|
||||
[key: string]: UmbConditionConfigBase;
|
||||
};
|
||||
|
||||
export type SpecificConditionTypeOrUmbConditionConfigBase<
|
||||
ConditionTypes extends UmbConditionConfigBase,
|
||||
T extends keyof ConditionTypeMap<ConditionTypes> | string
|
||||
> = T extends keyof ConditionTypeMap<ConditionTypes> ? ConditionTypeMap<ConditionTypes>[T] : UmbConditionConfigBase;
|
||||
|
||||
export interface ManifestWithDynamicConditions<ConditionTypes extends UmbConditionConfigBase = UmbConditionConfigBase>
|
||||
extends ManifestBase {
|
||||
/**
|
||||
* Set the conditions for when the extension should be loaded
|
||||
*/
|
||||
conditions?: Array<ConditionTypes>;
|
||||
/**
|
||||
* Define one or more extension aliases that this extension should overwrite.
|
||||
*/
|
||||
overwrites?: string | Array<string>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './base.types.js';
|
||||
export * from './condition.types.js';
|
||||
export * from './manifest-base.interface.js';
|
||||
export * from './manifest-bundle.interface.js';
|
||||
export * from './manifest-condition.interface.js';
|
||||
export * from './manifest-entrypoint.interface.js';
|
||||
export * from './manifest-kind.interface.js';
|
||||
export * from './utils.js';
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
export interface ManifestBase {
|
||||
/**
|
||||
* The type of extension such as dashboard etc...
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* The alias of the extension, ensure it is unique
|
||||
*/
|
||||
alias: string;
|
||||
|
||||
/**
|
||||
* The kind of the extension, used to group extensions together
|
||||
*
|
||||
* @examples ["button"]
|
||||
*/
|
||||
kind?: unknown; // I had to add the optional kind property set to undefined. To make the ManifestTypes recognize the Manifest Kind types. Notice that Kinds has to Omit the kind property when extending.
|
||||
|
||||
/**
|
||||
* The friendly name of the extension
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Extensions such as dashboards are ordered by weight with lower numbers being first in the list
|
||||
*/
|
||||
weight?: number;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { ManifestPlainJs } from "./base.types.js";
|
||||
import type { ManifestBase } from "./manifest-base.interface.js";
|
||||
|
||||
/**
|
||||
* This type of extension takes a JS module and registers all exported manifests from the pointed JS file.
|
||||
*/
|
||||
export interface ManifestBundle<UmbManifestTypes extends ManifestBase = ManifestBase>
|
||||
extends ManifestPlainJs<{ [key: string]: Array<UmbManifestTypes> }> {
|
||||
type: 'bundle';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { UmbExtensionCondition } from "../condition/index.js";
|
||||
import type { ManifestApi } from "./base.types.js";
|
||||
|
||||
/**
|
||||
* This type of extension takes a JS module and registers all exported manifests from the pointed JS file.
|
||||
*/
|
||||
export interface ManifestCondition extends ManifestApi<UmbExtensionCondition> {
|
||||
type: 'condition';
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { UmbEntryPointModule } from "../models/index.js";
|
||||
import type { ManifestPlainJs } from "./base.types.js";
|
||||
|
||||
/**
|
||||
* This type of extension gives full control and will simply load the specified JS file
|
||||
* You could have custom logic to decide which extensions to load/register by using extensionRegistry
|
||||
*/
|
||||
export interface ManifestEntryPoint extends ManifestPlainJs<UmbEntryPointModule> {
|
||||
type: 'entryPoint';
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface ManifestKind<ManifestTypes> {
|
||||
type: 'kind';
|
||||
alias: string;
|
||||
matchType: string;
|
||||
matchKind: string;
|
||||
/**
|
||||
* Provide pre defined properties for the extension manifest.
|
||||
* Define the `type`-property and other properties you like to preset for implementations of this kind.
|
||||
*
|
||||
* @example {
|
||||
* type: 'section',
|
||||
* weight: 123,
|
||||
* }
|
||||
* @TJS-type object
|
||||
*/
|
||||
manifest: Partial<ManifestTypes>;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { ManifestBase } from "./manifest-base.interface.js";
|
||||
|
||||
export type ManifestTypeMap<ManifestTypes extends ManifestBase> = {
|
||||
[Manifest in ManifestTypes as Manifest['type']]: Manifest;
|
||||
} & {
|
||||
[key: string]: ManifestBase;
|
||||
};
|
||||
|
||||
export type SpecificManifestTypeOrManifestBase<
|
||||
ManifestTypes extends ManifestBase,
|
||||
T extends keyof ManifestTypeMap<ManifestTypes> | string
|
||||
> = T extends keyof ManifestTypeMap<ManifestTypes> ? ManifestTypeMap<ManifestTypes>[T] : ManifestBase;
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { UmbApi } from "../models/index.js";
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type HTMLElementConstructor<T = HTMLElement> = new (...args: any[]) => T;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type ClassConstructor<T = object> = new (...args: any[]) => T;
|
||||
|
||||
|
||||
|
||||
// Module Export Types:
|
||||
|
||||
export type ElementLoaderExports<
|
||||
ElementType extends HTMLElement = HTMLElement
|
||||
> = ({default: ClassConstructor<ElementType>} | {element: ClassConstructor<ElementType>})// | Omit<Omit<object, 'element'>, 'default'>
|
||||
|
||||
export type ApiLoaderExports<
|
||||
ApiType extends UmbApi = UmbApi
|
||||
> = ({default: ClassConstructor<ApiType>} | {api: ClassConstructor<ApiType>})//| Omit<Omit<object, 'api'>, 'default'>
|
||||
|
||||
export type ElementAndApiLoaderExports<
|
||||
ElementType extends HTMLElement = HTMLElement,
|
||||
ApiType extends UmbApi = UmbApi
|
||||
> = ({api: ClassConstructor<ApiType>} | {element: ClassConstructor<ElementType>} | {api: ClassConstructor<ApiType>, element: ClassConstructor<ElementType>})// | Omit<Omit<Omit<object, 'element'>, 'api'>, 'default'>
|
||||
|
||||
|
||||
// Promise Types:
|
||||
|
||||
export type CssLoaderPromise<
|
||||
CssType = unknown
|
||||
> = (() => Promise<CssType>)
|
||||
|
||||
export type JsLoaderPromise<
|
||||
JsType
|
||||
> = (() => Promise<JsType>)
|
||||
|
||||
export type ElementLoaderPromise<
|
||||
ElementType extends HTMLElement = HTMLElement
|
||||
> = (() => Promise<ElementLoaderExports<ElementType>>)
|
||||
|
||||
export type ApiLoaderPromise<
|
||||
ApiType extends UmbApi = UmbApi
|
||||
> = (() => Promise<ApiLoaderExports<ApiType>>)
|
||||
|
||||
export type ElementAndApiLoaderPromise<
|
||||
ElementType extends HTMLElement = HTMLElement,
|
||||
ApiType extends UmbApi = UmbApi
|
||||
> = (() => Promise<ElementAndApiLoaderExports<ElementType, ApiType>>)
|
||||
|
||||
|
||||
// Property Types:
|
||||
|
||||
export type CssLoaderProperty<CssType = string> = (
|
||||
string
|
||||
|
|
||||
CssLoaderPromise<CssType>
|
||||
);
|
||||
export type JsLoaderProperty<JsType> = (
|
||||
string
|
||||
|
|
||||
JsLoaderPromise<JsType>
|
||||
);
|
||||
export type ElementLoaderProperty<ElementType extends HTMLElement = HTMLElement> = (
|
||||
string
|
||||
|
|
||||
ElementLoaderPromise<ElementType>
|
||||
|
|
||||
ClassConstructor<ElementType>
|
||||
);
|
||||
export type ApiLoaderProperty<ApiType extends UmbApi = UmbApi> = (
|
||||
string
|
||||
|
|
||||
ApiLoaderPromise<ApiType>
|
||||
|
|
||||
ClassConstructor<ApiType>
|
||||
);
|
||||
export type ElementAndApiLoaderProperty<ElementType extends HTMLElement = HTMLElement, ApiType extends UmbApi = UmbApi> = (
|
||||
string
|
||||
|
|
||||
ElementAndApiLoaderPromise<ElementType, ApiType>
|
||||
|
|
||||
ElementLoaderPromise<ElementType>
|
||||
|
|
||||
ApiLoaderPromise<ApiType>
|
||||
);
|
||||
@@ -14,7 +14,7 @@ export class UmbObserverController<T = unknown> extends UmbObserver<T> implement
|
||||
host: UmbControllerHost,
|
||||
source: Observable<T>,
|
||||
callback: ObserverCallback<T>,
|
||||
alias?: UmbControllerAlias
|
||||
alias?: UmbControllerAlias,
|
||||
) {
|
||||
super(source, callback);
|
||||
this.#host = host;
|
||||
|
||||
@@ -7,10 +7,9 @@ import {
|
||||
UmbNumberState,
|
||||
UmbObjectState,
|
||||
} from '@umbraco-cms/backoffice/observable-api';
|
||||
import { createExtensionApi } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { UmbExtensionsManifestInitializer, createExtensionApi } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { ManifestCollectionView, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import type { UmbCollectionFilterModel } from '@umbraco-cms/backoffice/collection';
|
||||
import { map } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
import { UmbSelectionManager, UmbPaginationManager } from '@umbraco-cms/backoffice/utils';
|
||||
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
|
||||
|
||||
@@ -193,15 +192,10 @@ export class UmbCollectionContext<ItemType, FilterModelType extends UmbCollectio
|
||||
}
|
||||
|
||||
#observeViews() {
|
||||
return this.observe(umbExtensionsRegistry.extensionsOfType('collectionView').pipe(
|
||||
map((extensions) => {
|
||||
return extensions.filter((extension) => extension.conditions.entityType === this.getEntityType());
|
||||
}),
|
||||
),
|
||||
(views) => {
|
||||
this.#views.next(views);
|
||||
return new UmbExtensionsManifestInitializer(this, umbExtensionsRegistry, 'collectionView', null, (views) => {
|
||||
this.#views.next(views.map(view => view.manifest));
|
||||
this.#setCurrentView();
|
||||
}, 'umbCollectionViewsObserver');
|
||||
});
|
||||
}
|
||||
|
||||
#onPageChange = (event: UmbChangeEvent) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type ManifestTypes, umbExtensionsRegistry } from '../../extension-regis
|
||||
import { css, repeat, customElement, property, state, TemplateResult } from '@umbraco-cms/backoffice/external/lit';
|
||||
import {
|
||||
type UmbExtensionElementInitializer,
|
||||
UmbExtensionsElementController,
|
||||
UmbExtensionsElementInitializer,
|
||||
} from '@umbraco-cms/backoffice/extension-api';
|
||||
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
|
||||
|
||||
@@ -20,7 +20,7 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
|
||||
@customElement('umb-extension-slot')
|
||||
export class UmbExtensionSlotElement extends UmbLitElement {
|
||||
#attached = false;
|
||||
#extensionsController?: UmbExtensionsElementController<ManifestTypes>;
|
||||
#extensionsController?: UmbExtensionsElementInitializer<ManifestTypes>;
|
||||
|
||||
@state()
|
||||
private _permittedExts: Array<UmbExtensionElementInitializer> = [];
|
||||
@@ -106,7 +106,7 @@ export class UmbExtensionSlotElement extends UmbLitElement {
|
||||
private _observeExtensions() {
|
||||
this.#extensionsController?.destroy();
|
||||
if (this.#type) {
|
||||
this.#extensionsController = new UmbExtensionsElementController(
|
||||
this.#extensionsController = new UmbExtensionsElementInitializer(
|
||||
this,
|
||||
umbExtensionsRegistry,
|
||||
this.#type,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from '@umbraco-cms/backoffice/external/tinymce';
|
||||
import { UMB_CURRENT_USER_CONTEXT, UmbCurrentUser } from '@umbraco-cms/backoffice/current-user';
|
||||
import { TinyMcePluginArguments, UmbTinyMcePluginBase } from '@umbraco-cms/backoffice/components';
|
||||
import { ClassConstructor, hasDefaultExport, loadExtension } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { ClassConstructor, hasDefaultExport, loadManifestApi } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { ManifestTinyMcePlugin, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import {
|
||||
PropertyValueMap,
|
||||
@@ -99,12 +99,12 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
||||
*/
|
||||
async #loadPlugins() {
|
||||
const observable = umbExtensionsRegistry?.extensionsOfType('tinyMcePlugin');
|
||||
const plugins = (await firstValueFrom(observable)) as ManifestTinyMcePlugin[];
|
||||
const manifests = (await firstValueFrom(observable)) as ManifestTinyMcePlugin[];
|
||||
|
||||
for (const plugin of plugins) {
|
||||
const module = await loadExtension(plugin);
|
||||
if (hasDefaultExport<ClassConstructor<UmbTinyMcePluginBase>>(module)) {
|
||||
this.#plugins.push(module.default);
|
||||
for (const manifest of manifests) {
|
||||
const plugin = manifest.js ? await loadManifestApi(manifest.js) : manifest.api ? await loadManifestApi(manifest.api) : undefined;
|
||||
if (plugin) {
|
||||
this.#plugins.push(plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export class UmbInputDataTypeElement extends FormControlMixin(UmbLitElement) {
|
||||
* @param {string} dataTypeId
|
||||
* @default []
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
@property({ type: String, attribute: false })
|
||||
get value(): string {
|
||||
return super.value?.toString() ?? '';
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const entityActions: Array<ManifestTypes> = [
|
||||
type: 'modal',
|
||||
alias: 'Umb.Modal.DataTypeCreateOptions',
|
||||
name: 'Data Type Create Options Modal',
|
||||
loader: () => import('./modal/data-type-create-options-modal.element.js'),
|
||||
js: () => import('./modal/data-type-create-options-modal.element.js'),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -5,19 +5,19 @@ const modals: Array<ManifestModal> = [
|
||||
type: 'modal',
|
||||
alias: 'Umb.Modal.PropertyEditorUiPicker',
|
||||
name: 'Property Editor UI Picker Modal',
|
||||
loader: () => import('./property-editor-ui-picker/property-editor-ui-picker-modal.element.js'),
|
||||
js: () => import('./property-editor-ui-picker/property-editor-ui-picker-modal.element.js'),
|
||||
},
|
||||
{
|
||||
type: 'modal',
|
||||
alias: 'Umb.Modal.DataTypePickerFlow',
|
||||
name: 'Data Type Picker Flow Modal',
|
||||
loader: () => import('./data-type-picker-flow/data-type-picker-flow-modal.element.js'),
|
||||
js: () => import('./data-type-picker-flow/data-type-picker-flow-modal.element.js'),
|
||||
},
|
||||
{
|
||||
type: 'modal',
|
||||
alias: 'Umb.Modal.DataTypePickerFlowDataTypePicker',
|
||||
name: 'Data Type Picker Flow UI Picker Modal',
|
||||
loader: () => import('./data-type-picker-flow/data-type-picker-flow-data-type-picker-modal.element.js'),
|
||||
js: () => import('./data-type-picker-flow/data-type-picker-flow-data-type-picker-modal.element.js'),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { UmbDataTypeDetailRepository } from '../repository/detail/data-type-deta
|
||||
import { UmbDataTypeVariantContext } from '../variant-context/data-type-variant-context.js';
|
||||
import {
|
||||
UmbInvariantableWorkspaceContextInterface,
|
||||
UmbWorkspaceContext,
|
||||
UmbEditableWorkspaceContextBase,
|
||||
UmbWorkspaceContextInterface,
|
||||
} from '@umbraco-cms/backoffice/workspace';
|
||||
import type { DataTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
import { UMB_PROPERTY_EDITOR_SCHEMA_ALIAS_DEFAULT } from '@umbraco-cms/backoffice/property-editor';
|
||||
|
||||
export class UmbDataTypeWorkspaceContext
|
||||
extends UmbWorkspaceContext<UmbDataTypeDetailRepository, DataTypeResponseModel>
|
||||
extends UmbEditableWorkspaceContextBase<UmbDataTypeDetailRepository, DataTypeResponseModel>
|
||||
implements UmbInvariantableWorkspaceContextInterface<DataTypeResponseModel | undefined>
|
||||
{
|
||||
// TODO: revisit. temp solution because the create and response models are different.
|
||||
|
||||
@@ -11,7 +11,7 @@ const workspace: ManifestWorkspace = {
|
||||
type: 'workspace',
|
||||
alias: DATA_TYPE_WORKSPACE_ALIAS,
|
||||
name: 'Data Type Workspace',
|
||||
loader: () => import('./data-type-workspace.element.js'),
|
||||
js: () => import('./data-type-workspace.element.js'),
|
||||
meta: {
|
||||
entityType: 'data-type',
|
||||
},
|
||||
@@ -22,7 +22,7 @@ const workspaceViews: Array<ManifestWorkspaceEditorView> = [
|
||||
type: 'workspaceEditorView',
|
||||
alias: 'Umb.WorkspaceView.DataType.Edit',
|
||||
name: 'Data Type Workspace Edit View',
|
||||
loader: () => import('./views/details/data-type-details-workspace-view.element.js'),
|
||||
js: () => import('./views/details/data-type-details-workspace-view.element.js'),
|
||||
weight: 90,
|
||||
meta: {
|
||||
label: 'Details',
|
||||
@@ -40,7 +40,7 @@ const workspaceViews: Array<ManifestWorkspaceEditorView> = [
|
||||
type: 'workspaceEditorView',
|
||||
alias: 'Umb.WorkspaceView.DataType.Info',
|
||||
name: 'Data Type Workspace Info View',
|
||||
loader: () => import('./views/info/workspace-view-data-type-info.element.js'),
|
||||
js: () => import('./views/info/workspace-view-data-type-info.element.js'),
|
||||
weight: 90,
|
||||
meta: {
|
||||
label: 'Info',
|
||||
|
||||
@@ -5,7 +5,7 @@ const modals: Array<ManifestModal> = [
|
||||
type: 'modal',
|
||||
alias: 'Umb.Modal.ContextDebugger',
|
||||
name: 'Context Debugger Modal',
|
||||
loader: () => import('./modals/debug/debug-modal.element.js'),
|
||||
js: () => import('./modals/debug/debug-modal.element.js'),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UmbEntityBulkAction, UmbEntityBulkActionBase } from './entity-bulk-action.js';
|
||||
import { UmbEntityBulkActionBase } from './entity-bulk-action.js';
|
||||
import { UmbActionExecutedEvent } from '@umbraco-cms/backoffice/event';
|
||||
import { html, ifDefined, customElement, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { ManifestEntityBulkAction } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { UmbAction, UmbActionBase } from '@umbraco-cms/backoffice/action';
|
||||
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
export interface UmbEntityBulkAction<RepositoryType = unknown> extends UmbAction<RepositoryType> {
|
||||
selection: Array<string>;
|
||||
setSelection(selection: Array<string>): void;
|
||||
}
|
||||
|
||||
export abstract class UmbEntityBulkActionBase<RepositoryType = unknown> extends UmbActionBase<RepositoryType> implements UmbEntityBulkAction<RepositoryType> {
|
||||
export abstract class UmbEntityBulkActionBase<RepositoryType = unknown>
|
||||
extends UmbActionBase<RepositoryType>
|
||||
implements UmbEntityBulkAction<RepositoryType>
|
||||
{
|
||||
selection: Array<string>;
|
||||
|
||||
constructor(host: UmbControllerHostElement, repositoryAlias: string, selection: Array<string>) {
|
||||
|
||||
@@ -29,8 +29,7 @@ export type SectionAliasConditionConfig = UmbConditionConfigBase<'Umb.Condition.
|
||||
/**
|
||||
* Define the section that this extension should be available in
|
||||
*
|
||||
* @example
|
||||
* "Umb.Section.Content"
|
||||
* @example "Umb.Section.Content"
|
||||
*/
|
||||
match: string;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
|
||||
|
||||
export interface UmbPropertyEditorUiElement extends HTMLElement {
|
||||
value: unknown;
|
||||
value?: unknown;
|
||||
config?: UmbPropertyEditorConfigCollection;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ManifestElement, ManifestWithConditions } from '@umbraco-cms/backoffice/extension-api';
|
||||
import type { ConditionTypes } from '../conditions/types.js';
|
||||
import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
export interface ManifestCollectionView extends ManifestElement, ManifestWithConditions<ConditionsCollectionView> {
|
||||
export interface ManifestCollectionView extends ManifestElement, ManifestWithDynamicConditions<ConditionTypes> {
|
||||
type: 'collectionView';
|
||||
meta: MetaCollectionView;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import type { ManifestTreeItem } from './tree-item.model.js';
|
||||
import type { ManifestUserProfileApp } from './user-profile-app.model.js';
|
||||
import type { ManifestWorkspace } from './workspace.model.js';
|
||||
import type { ManifestWorkspaceAction } from './workspace-action.model.js';
|
||||
import type { ManifestWorkspaceContext } from './workspace-context.model.js';
|
||||
import type { ManifestWorkspaceEditorView } from './workspace-editor-view.model.js';
|
||||
import type { ManifestWorkspaceViewCollection } from './workspace-view-collection.model.js';
|
||||
import type { ManifestUserPermission } from './user-permission.model.js';
|
||||
@@ -46,6 +47,7 @@ export * from './external-login-provider.model.js';
|
||||
export * from './global-context.model.js';
|
||||
export * from './header-app.model.js';
|
||||
export * from './health-check.model.js';
|
||||
export * from './localization.model.js';
|
||||
export * from './menu-item.model.js';
|
||||
export * from './menu.model.js';
|
||||
export * from './modal.model.js';
|
||||
@@ -59,16 +61,16 @@ export * from './section.model.js';
|
||||
export * from './store.model.js';
|
||||
export * from './theme.model.js';
|
||||
export * from './tinymce-plugin.model.js';
|
||||
export * from './localization.model.js';
|
||||
export * from './tree-item.model.js';
|
||||
export * from './tree.model.js';
|
||||
export * from './user-granular-permission.model.js';
|
||||
export * from './user-permission.model.js';
|
||||
export * from './user-profile-app.model.js';
|
||||
export * from './workspace-action.model.js';
|
||||
export * from './workspace-view-collection.model.js';
|
||||
export * from './workspace-context.model.js';
|
||||
export * from './workspace-editor-view.model.js';
|
||||
export * from './workspace-view-collection.model.js';
|
||||
export * from './workspace.model.js';
|
||||
export * from './user-permission.model.js';
|
||||
export * from './user-granular-permission.model.js';
|
||||
|
||||
export type ManifestTypes =
|
||||
| ManifestBundle<ManifestTypes>
|
||||
@@ -108,6 +110,7 @@ export type ManifestTypes =
|
||||
| ManifestUserProfileApp
|
||||
| ManifestWorkspace
|
||||
| ManifestWorkspaceAction
|
||||
| ManifestWorkspaceContext
|
||||
| ManifestWorkspaceEditorView
|
||||
| ManifestWorkspaceViewCollection
|
||||
| ManifestUserPermission
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ManifestWithLoaderIncludingDefaultExport } from '@umbraco-cms/backoffice/extension-api';
|
||||
import type { ManifestPlainJs } from '@umbraco-cms/backoffice/extension-api';
|
||||
import type { UmbLocalizationDictionary } from '@umbraco-cms/backoffice/localization-api';
|
||||
|
||||
export interface ManifestLocalization extends ManifestWithLoaderIncludingDefaultExport<UmbLocalizationDictionary> {
|
||||
export interface ManifestLocalization extends ManifestPlainJs<{default: UmbLocalizationDictionary}> {
|
||||
type: 'localization';
|
||||
meta: MetaLocalization;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { UmbModalExtensionElement } from '../interfaces/modal-extension-element.interface.js';
|
||||
import type { ManifestElement } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
export interface ManifestModal extends ManifestElement {
|
||||
export interface ManifestModal extends ManifestElement<UmbModalExtensionElement<any, any> | UmbModalExtensionElement<any, undefined>> {
|
||||
type: 'modal';
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ConditionTypes } from '../conditions/types.js';
|
||||
import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
export interface ManifestPropertyAction extends ManifestElement, ManifestWithDynamicConditions<ConditionTypes> {
|
||||
export interface ManifestPropertyAction extends ManifestElement<HTMLElement>, ManifestWithDynamicConditions<ConditionTypes> {
|
||||
type: 'propertyAction';
|
||||
meta: MetaPropertyAction;
|
||||
}
|
||||
|
||||
@@ -14,21 +14,13 @@ export interface MetaPropertyEditorUi {
|
||||
* The group that this property editor UI belongs to, which will be used to group the property editor UIs in the property editor picker.
|
||||
* If not specified, the property editor UI will be grouped under "Common".
|
||||
* @default "Common"
|
||||
* @examples [
|
||||
* "Common",
|
||||
* "Content",
|
||||
* "Media"
|
||||
* ]
|
||||
* @examples ["Common", "Content", "Media"]
|
||||
*/
|
||||
group: string;
|
||||
/**
|
||||
* The alias of the property editor schema that this property editor UI is for.
|
||||
* If not specified, the property editor UI can only be used to configure other property editors.
|
||||
* @examples [
|
||||
* "Umbraco.TextBox",
|
||||
* "Umbraco.TextArea",
|
||||
* "Umbraco.Label",
|
||||
* ]
|
||||
* @examples ["Umbraco.TextBox", "Umbraco.TextArea", "Umbraco.Label"]
|
||||
*/
|
||||
propertyEditorSchemaAlias?: string;
|
||||
settings?: PropertyEditorSettings;
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
import type { ManifestWithLoader } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
// TODO: make or find type for JS Module with default export: Would be nice to support css file directly.
|
||||
|
||||
import type { ManifestPlainCss } from '@umbraco-cms/backoffice/extension-api';
|
||||
/**
|
||||
* Theme manifest for styling the backoffice of Umbraco such as dark, high contrast etc
|
||||
*/
|
||||
export interface ManifestTheme extends ManifestWithLoader<string> {
|
||||
export interface ManifestTheme extends ManifestPlainCss<string> {
|
||||
type: 'theme';
|
||||
|
||||
/**
|
||||
* File location of the CSS file of the theme
|
||||
*
|
||||
* @examples ["themes/dark.theme.css"]
|
||||
*/
|
||||
css?: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ManifestApi } from '@umbraco-cms/backoffice/extension-api';
|
||||
import type { UmbTinyMcePluginBase } from '@umbraco-cms/backoffice/components';
|
||||
import type { ManifestApi } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
export interface MetaTinyMcePlugin {
|
||||
/**
|
||||
@@ -36,7 +37,7 @@ export interface MetaTinyMcePlugin {
|
||||
*
|
||||
* @see [TinyMCE Plugin](https://www.tiny.cloud/docs/tinymce/6/apis/tinymce.plugin/) for more information.
|
||||
*/
|
||||
export interface ManifestTinyMcePlugin extends ManifestApi {
|
||||
export interface ManifestTinyMcePlugin extends ManifestApi<UmbTinyMcePluginBase> {
|
||||
type: 'tinyMcePlugin';
|
||||
meta?: MetaTinyMcePlugin;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user