UI: External angular modules for widget development
This commit is contained in:
parent
2ca577e9c5
commit
21d7350efc
@ -25,6 +25,10 @@ const PROXY_CONFIG = {
|
||||
"target": `http://${ruleNodeUiforwardHost}:${ruleNodeUiforwardPort}`,
|
||||
"secure": false,
|
||||
},
|
||||
"/static": {
|
||||
"target": "http://localhost:8080",
|
||||
"secure": false,
|
||||
},
|
||||
"/api/ws": {
|
||||
"target": "ws://localhost:8080",
|
||||
"ws": true,
|
||||
|
||||
@ -239,7 +239,7 @@ export class RuleChainService {
|
||||
});
|
||||
}
|
||||
if (moduleResource) {
|
||||
tasks.push(this.resourcesService.loadModule(moduleResource, ruleNodeConfigResourcesModulesMap).pipe(
|
||||
tasks.push(this.resourcesService.loadFactories(moduleResource, ruleNodeConfigResourcesModulesMap).pipe(
|
||||
map((res) => {
|
||||
if (nodeDefinition.configDirective && nodeDefinition.configDirective.length) {
|
||||
const selector = snakeCase(nodeDefinition.configDirective, '-');
|
||||
|
||||
@ -25,6 +25,8 @@ import {
|
||||
} from '@angular/core';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { objToBase64 } from '@core/utils';
|
||||
|
||||
declare const SystemJS;
|
||||
|
||||
@ -34,12 +36,14 @@ declare const SystemJS;
|
||||
export class ResourcesService {
|
||||
|
||||
private loadedResources: { [url: string]: ReplaySubject<any> } = {};
|
||||
private loadedModules: { [url: string]: ReplaySubject<ComponentFactory<any>[]> } = {};
|
||||
private loadedModules: { [url: string]: ReplaySubject<Type<any>[]> } = {};
|
||||
private loadedFactories: { [url: string]: ReplaySubject<ComponentFactory<any>[]> } = {};
|
||||
|
||||
private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0];
|
||||
|
||||
constructor(@Inject(DOCUMENT) private readonly document: any,
|
||||
private compiler: Compiler,
|
||||
private http: HttpClient,
|
||||
private injector: Injector) {}
|
||||
|
||||
public loadResource(url: string): Observable<any> {
|
||||
@ -60,12 +64,12 @@ export class ResourcesService {
|
||||
return this.loadResourceByType(fileType, url);
|
||||
}
|
||||
|
||||
public loadModule(url: string, modulesMap: {[key: string]: any}): Observable<ComponentFactory<any>[]> {
|
||||
if (this.loadedModules[url]) {
|
||||
return this.loadedModules[url].asObservable();
|
||||
public loadFactories(url: string, modulesMap: {[key: string]: any}): Observable<ComponentFactory<any>[]> {
|
||||
if (this.loadedFactories[url]) {
|
||||
return this.loadedFactories[url].asObservable();
|
||||
}
|
||||
const subject = new ReplaySubject<ComponentFactory<any>[]>();
|
||||
this.loadedModules[url] = subject;
|
||||
this.loadedFactories[url] = subject;
|
||||
if (modulesMap) {
|
||||
for (const moduleId of Object.keys(modulesMap)) {
|
||||
SystemJS.set(moduleId, modulesMap[moduleId]);
|
||||
@ -86,7 +90,58 @@ export class ResourcesService {
|
||||
c.ngModuleFactory.create(this.injector);
|
||||
componentFactories.push(...c.componentFactories);
|
||||
}
|
||||
this.loadedModules[url].next(componentFactories);
|
||||
this.loadedFactories[url].next(componentFactories);
|
||||
this.loadedFactories[url].complete();
|
||||
} catch (e) {
|
||||
this.loadedFactories[url].error(new Error(`Unable to init module from url: ${url}`));
|
||||
delete this.loadedFactories[url];
|
||||
}
|
||||
},
|
||||
(e) => {
|
||||
this.loadedFactories[url].error(new Error(`Unable to compile module from url: ${url}`));
|
||||
delete this.loadedFactories[url];
|
||||
});
|
||||
} else {
|
||||
this.loadedFactories[url].error(new Error(`Module '${url}' doesn't have default export!`));
|
||||
delete this.loadedFactories[url];
|
||||
}
|
||||
},
|
||||
(e) => {
|
||||
this.loadedFactories[url].error(new Error(`Unable to load module from url: ${url}`));
|
||||
delete this.loadedFactories[url];
|
||||
}
|
||||
);
|
||||
return subject.asObservable();
|
||||
}
|
||||
|
||||
public loadModules(url: string, modulesMap: {[key: string]: any}): Observable<Type<any>[]> {
|
||||
if (this.loadedModules[url]) {
|
||||
return this.loadedModules[url].asObservable();
|
||||
}
|
||||
const subject = new ReplaySubject<Type<any>[]>();
|
||||
this.loadedModules[url] = subject;
|
||||
if (modulesMap) {
|
||||
for (const moduleId of Object.keys(modulesMap)) {
|
||||
SystemJS.set(moduleId, modulesMap[moduleId]);
|
||||
}
|
||||
}
|
||||
SystemJS.import(url).then(
|
||||
(module) => {
|
||||
let modules;
|
||||
try {
|
||||
modules = this.extractNgModules(module);
|
||||
} catch (e) {}
|
||||
if (modules && modules.length) {
|
||||
const tasks: Promise<ModuleWithComponentFactories<any>>[] = [];
|
||||
for (const m of modules) {
|
||||
tasks.push(this.compiler.compileModuleAndAllComponentsAsync(m));
|
||||
}
|
||||
forkJoin(tasks).subscribe((compiled) => {
|
||||
try {
|
||||
for (const c of compiled) {
|
||||
c.ngModuleFactory.create(this.injector);
|
||||
}
|
||||
this.loadedModules[url].next(modules);
|
||||
this.loadedModules[url].complete();
|
||||
} catch (e) {
|
||||
this.loadedModules[url].error(new Error(`Unable to init module from url: ${url}`));
|
||||
@ -98,7 +153,7 @@ export class ResourcesService {
|
||||
delete this.loadedModules[url];
|
||||
});
|
||||
} else {
|
||||
this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export!`));
|
||||
this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export or not NgModule!`));
|
||||
delete this.loadedModules[url];
|
||||
}
|
||||
},
|
||||
|
||||
@ -41,6 +41,43 @@ import { NULL_UUID } from '@shared/models/id/has-uuid';
|
||||
import { WidgetTypeId } from '@app/shared/models/id/widget-type-id';
|
||||
import { TenantId } from '@app/shared/models/id/tenant-id';
|
||||
import { SharedModule } from '@shared/shared.module';
|
||||
import * as AngularCore from '@angular/core';
|
||||
import * as AngularCommon from '@angular/common';
|
||||
import * as AngularForms from '@angular/forms';
|
||||
import * as AngularRouter from '@angular/router';
|
||||
import * as AngularCdkKeycodes from '@angular/cdk/keycodes';
|
||||
import * as AngularCdkCoercion from '@angular/cdk/coercion';
|
||||
import * as AngularMaterialChips from '@angular/material/chips';
|
||||
import * as AngularMaterialAutocomplete from '@angular/material/autocomplete';
|
||||
import * as AngularMaterialDialog from '@angular/material/dialog';
|
||||
import * as NgrxStore from '@ngrx/store';
|
||||
import * as RxJs from 'rxjs';
|
||||
import * as RxJsOperators from 'rxjs/operators';
|
||||
import * as TranslateCore from '@ngx-translate/core';
|
||||
import * as TbCore from '@core/public-api';
|
||||
import * as TbShared from '@shared/public-api';
|
||||
import * as _moment from 'moment';
|
||||
|
||||
declare const SystemJS;
|
||||
|
||||
const widgetResourcesModulesMap = {
|
||||
'@angular/core': SystemJS.newModule(AngularCore),
|
||||
'@angular/common': SystemJS.newModule(AngularCommon),
|
||||
'@angular/forms': SystemJS.newModule(AngularForms),
|
||||
'@angular/router': SystemJS.newModule(AngularRouter),
|
||||
'@angular/cdk/keycodes': SystemJS.newModule(AngularCdkKeycodes),
|
||||
'@angular/cdk/coercion': SystemJS.newModule(AngularCdkCoercion),
|
||||
'@angular/material/chips': SystemJS.newModule(AngularMaterialChips),
|
||||
'@angular/material/autocomplete': SystemJS.newModule(AngularMaterialAutocomplete),
|
||||
'@angular/material/dialog': SystemJS.newModule(AngularMaterialDialog),
|
||||
'@ngrx/store': SystemJS.newModule(NgrxStore),
|
||||
rxjs: SystemJS.newModule(RxJs),
|
||||
'rxjs/operators': SystemJS.newModule(RxJsOperators),
|
||||
'@ngx-translate/core': SystemJS.newModule(TranslateCore),
|
||||
'@core/public-api': SystemJS.newModule(TbCore),
|
||||
'@shared/public-api': SystemJS.newModule(TbShared),
|
||||
moment: SystemJS.newModule(_moment)
|
||||
};
|
||||
|
||||
// @dynamic
|
||||
@Injectable()
|
||||
@ -105,8 +142,8 @@ export class WidgetComponentService {
|
||||
const initSubject = new ReplaySubject();
|
||||
this.init$ = initSubject.asObservable();
|
||||
const loadDefaultWidgetInfoTasks = [
|
||||
this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type', [SharedModule]),
|
||||
this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type', [SharedModule]),
|
||||
this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type', [SharedModule, WidgetComponentsModule]),
|
||||
this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type', [SharedModule, WidgetComponentsModule]),
|
||||
];
|
||||
forkJoin(loadDefaultWidgetInfoTasks).subscribe(
|
||||
() => {
|
||||
@ -218,20 +255,58 @@ export class WidgetComponentService {
|
||||
this.cssParser.cssPreviewNamespace = widgetNamespace;
|
||||
this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss);
|
||||
const resourceTasks: Observable<string>[] = [];
|
||||
const modulesTasks: Observable<Type<any>[] | string>[] = [];
|
||||
if (widgetInfo.resources.length > 0) {
|
||||
widgetInfo.resources.forEach((resource) => {
|
||||
widgetInfo.resources.filter(r => r.isModule).forEach(
|
||||
(resource) => {
|
||||
modulesTasks.push(
|
||||
this.resources.loadModules(resource.url, widgetResourcesModulesMap).pipe(
|
||||
catchError((e: Error) => of(e?.message ? e.message : `Failed to load widget resource module: '${resource.url}'`))
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
widgetInfo.resources.filter(r => !r.isModule).forEach(
|
||||
(resource) => {
|
||||
resourceTasks.push(
|
||||
this.resources.loadResource(resource.url).pipe(
|
||||
catchError(e => of(`Failed to load widget resource: '${resource.url}'`))
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
let modulesObservable: Observable<string | Type<any>[]>;
|
||||
if (modulesTasks.length) {
|
||||
modulesObservable = forkJoin(modulesTasks).pipe(
|
||||
map(res => {
|
||||
const msg = res.find(r => typeof r === 'string');
|
||||
if (msg) {
|
||||
return msg as string;
|
||||
} else {
|
||||
let resModules = (res as Type<any>[][]).flat();
|
||||
if (modules && modules.length) {
|
||||
resModules = resModules.concat(modules);
|
||||
}
|
||||
return resModules;
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
modulesObservable = modules && modules.length ? of(modules) : of([]);
|
||||
}
|
||||
|
||||
resourceTasks.push(
|
||||
this.dynamicComponentFactoryService.createDynamicComponentFactory(
|
||||
modulesObservable.pipe(
|
||||
mergeMap((resolvedModules) => {
|
||||
if (typeof resolvedModules === 'string') {
|
||||
return of(resolvedModules);
|
||||
} else {
|
||||
return this.dynamicComponentFactoryService.createDynamicComponentFactory(
|
||||
class DynamicWidgetComponentInstance extends DynamicWidgetComponent {},
|
||||
widgetInfo.templateHtml,
|
||||
modules
|
||||
resolvedModules
|
||||
).pipe(
|
||||
map((factory) => {
|
||||
widgetInfo.componentFactory = factory;
|
||||
@ -243,6 +318,8 @@ export class WidgetComponentService {
|
||||
return of(errorMessage);
|
||||
})
|
||||
)
|
||||
}
|
||||
}))
|
||||
);
|
||||
return forkJoin(resourceTasks).pipe(
|
||||
switchMap(msgs => {
|
||||
|
||||
@ -129,6 +129,10 @@
|
||||
(ngModelChange)="isDirty = true"
|
||||
placeholder="{{ 'widget.resource-url' | translate }}"/>
|
||||
</mat-form-field>
|
||||
<mat-checkbox [(ngModel)]="resource.isModule"
|
||||
(ngModelChange)="isDirty = true">
|
||||
{{ 'widget.resource-is-module' | translate }}
|
||||
</mat-checkbox>
|
||||
<button mat-icon-button color="primary"
|
||||
[disabled]="isLoading$ | async"
|
||||
(click)="removeResource(i)"
|
||||
|
||||
@ -53,6 +53,7 @@ export interface NodeScriptTestDialogData {
|
||||
msgType?: string;
|
||||
}
|
||||
|
||||
// @dynamic
|
||||
@Component({
|
||||
selector: 'tb-node-script-test-dialog',
|
||||
templateUrl: './node-script-test-dialog.component.html',
|
||||
|
||||
@ -114,6 +114,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
|
||||
|
||||
export interface WidgetResource {
|
||||
url: string;
|
||||
isModule?: boolean;
|
||||
}
|
||||
|
||||
export interface WidgetActionSource {
|
||||
|
||||
@ -1787,6 +1787,7 @@
|
||||
"type": "Widget type",
|
||||
"resources": "Resources",
|
||||
"resource-url": "JavaScript/CSS URL",
|
||||
"resource-is-module": "Is module",
|
||||
"remove-resource": "Remove resource",
|
||||
"add-resource": "Add resource",
|
||||
"html": "HTML",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user