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}`,
|
"target": `http://${ruleNodeUiforwardHost}:${ruleNodeUiforwardPort}`,
|
||||||
"secure": false,
|
"secure": false,
|
||||||
},
|
},
|
||||||
|
"/static": {
|
||||||
|
"target": "http://localhost:8080",
|
||||||
|
"secure": false,
|
||||||
|
},
|
||||||
"/api/ws": {
|
"/api/ws": {
|
||||||
"target": "ws://localhost:8080",
|
"target": "ws://localhost:8080",
|
||||||
"ws": true,
|
"ws": true,
|
||||||
|
|||||||
@ -239,7 +239,7 @@ export class RuleChainService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (moduleResource) {
|
if (moduleResource) {
|
||||||
tasks.push(this.resourcesService.loadModule(moduleResource, ruleNodeConfigResourcesModulesMap).pipe(
|
tasks.push(this.resourcesService.loadFactories(moduleResource, ruleNodeConfigResourcesModulesMap).pipe(
|
||||||
map((res) => {
|
map((res) => {
|
||||||
if (nodeDefinition.configDirective && nodeDefinition.configDirective.length) {
|
if (nodeDefinition.configDirective && nodeDefinition.configDirective.length) {
|
||||||
const selector = snakeCase(nodeDefinition.configDirective, '-');
|
const selector = snakeCase(nodeDefinition.configDirective, '-');
|
||||||
|
|||||||
@ -25,6 +25,8 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs';
|
import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { objToBase64 } from '@core/utils';
|
||||||
|
|
||||||
declare const SystemJS;
|
declare const SystemJS;
|
||||||
|
|
||||||
@ -34,12 +36,14 @@ declare const SystemJS;
|
|||||||
export class ResourcesService {
|
export class ResourcesService {
|
||||||
|
|
||||||
private loadedResources: { [url: string]: ReplaySubject<any> } = {};
|
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];
|
private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0];
|
||||||
|
|
||||||
constructor(@Inject(DOCUMENT) private readonly document: any,
|
constructor(@Inject(DOCUMENT) private readonly document: any,
|
||||||
private compiler: Compiler,
|
private compiler: Compiler,
|
||||||
|
private http: HttpClient,
|
||||||
private injector: Injector) {}
|
private injector: Injector) {}
|
||||||
|
|
||||||
public loadResource(url: string): Observable<any> {
|
public loadResource(url: string): Observable<any> {
|
||||||
@ -60,12 +64,12 @@ export class ResourcesService {
|
|||||||
return this.loadResourceByType(fileType, url);
|
return this.loadResourceByType(fileType, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public loadModule(url: string, modulesMap: {[key: string]: any}): Observable<ComponentFactory<any>[]> {
|
public loadFactories(url: string, modulesMap: {[key: string]: any}): Observable<ComponentFactory<any>[]> {
|
||||||
if (this.loadedModules[url]) {
|
if (this.loadedFactories[url]) {
|
||||||
return this.loadedModules[url].asObservable();
|
return this.loadedFactories[url].asObservable();
|
||||||
}
|
}
|
||||||
const subject = new ReplaySubject<ComponentFactory<any>[]>();
|
const subject = new ReplaySubject<ComponentFactory<any>[]>();
|
||||||
this.loadedModules[url] = subject;
|
this.loadedFactories[url] = subject;
|
||||||
if (modulesMap) {
|
if (modulesMap) {
|
||||||
for (const moduleId of Object.keys(modulesMap)) {
|
for (const moduleId of Object.keys(modulesMap)) {
|
||||||
SystemJS.set(moduleId, modulesMap[moduleId]);
|
SystemJS.set(moduleId, modulesMap[moduleId]);
|
||||||
@ -86,19 +90,70 @@ export class ResourcesService {
|
|||||||
c.ngModuleFactory.create(this.injector);
|
c.ngModuleFactory.create(this.injector);
|
||||||
componentFactories.push(...c.componentFactories);
|
componentFactories.push(...c.componentFactories);
|
||||||
}
|
}
|
||||||
this.loadedModules[url].next(componentFactories);
|
this.loadedFactories[url].next(componentFactories);
|
||||||
this.loadedModules[url].complete();
|
this.loadedFactories[url].complete();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.loadedModules[url].error(new Error(`Unable to init module from url: ${url}`));
|
this.loadedFactories[url].error(new Error(`Unable to init module from url: ${url}`));
|
||||||
delete this.loadedModules[url];
|
delete this.loadedFactories[url];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(e) => {
|
(e) => {
|
||||||
this.loadedModules[url].error(new Error(`Unable to compile module from url: ${url}`));
|
this.loadedFactories[url].error(new Error(`Unable to compile module from url: ${url}`));
|
||||||
delete this.loadedModules[url];
|
delete this.loadedFactories[url];
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export!`));
|
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}`));
|
||||||
|
delete this.loadedModules[url];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(e) => {
|
||||||
|
this.loadedModules[url].error(new Error(`Unable to compile module from url: ${url}`));
|
||||||
|
delete this.loadedModules[url];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export or not NgModule!`));
|
||||||
delete this.loadedModules[url];
|
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 { WidgetTypeId } from '@app/shared/models/id/widget-type-id';
|
||||||
import { TenantId } from '@app/shared/models/id/tenant-id';
|
import { TenantId } from '@app/shared/models/id/tenant-id';
|
||||||
import { SharedModule } from '@shared/shared.module';
|
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
|
// @dynamic
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -105,8 +142,8 @@ export class WidgetComponentService {
|
|||||||
const initSubject = new ReplaySubject();
|
const initSubject = new ReplaySubject();
|
||||||
this.init$ = initSubject.asObservable();
|
this.init$ = initSubject.asObservable();
|
||||||
const loadDefaultWidgetInfoTasks = [
|
const loadDefaultWidgetInfoTasks = [
|
||||||
this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type', [SharedModule]),
|
this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type', [SharedModule, WidgetComponentsModule]),
|
||||||
this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type', [SharedModule]),
|
this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type', [SharedModule, WidgetComponentsModule]),
|
||||||
];
|
];
|
||||||
forkJoin(loadDefaultWidgetInfoTasks).subscribe(
|
forkJoin(loadDefaultWidgetInfoTasks).subscribe(
|
||||||
() => {
|
() => {
|
||||||
@ -218,31 +255,71 @@ export class WidgetComponentService {
|
|||||||
this.cssParser.cssPreviewNamespace = widgetNamespace;
|
this.cssParser.cssPreviewNamespace = widgetNamespace;
|
||||||
this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss);
|
this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss);
|
||||||
const resourceTasks: Observable<string>[] = [];
|
const resourceTasks: Observable<string>[] = [];
|
||||||
|
const modulesTasks: Observable<Type<any>[] | string>[] = [];
|
||||||
if (widgetInfo.resources.length > 0) {
|
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(
|
resourceTasks.push(
|
||||||
this.resources.loadResource(resource.url).pipe(
|
this.resources.loadResource(resource.url).pipe(
|
||||||
catchError(e => of(`Failed to load widget resource: '${resource.url}'`))
|
catchError(e => of(`Failed to load widget resource: '${resource.url}'`))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
}
|
);
|
||||||
resourceTasks.push(
|
|
||||||
this.dynamicComponentFactoryService.createDynamicComponentFactory(
|
let modulesObservable: Observable<string | Type<any>[]>;
|
||||||
class DynamicWidgetComponentInstance extends DynamicWidgetComponent {},
|
if (modulesTasks.length) {
|
||||||
widgetInfo.templateHtml,
|
modulesObservable = forkJoin(modulesTasks).pipe(
|
||||||
modules
|
map(res => {
|
||||||
).pipe(
|
const msg = res.find(r => typeof r === 'string');
|
||||||
map((factory) => {
|
if (msg) {
|
||||||
widgetInfo.componentFactory = factory;
|
return msg as string;
|
||||||
return null;
|
} else {
|
||||||
}),
|
let resModules = (res as Type<any>[][]).flat();
|
||||||
catchError(e => {
|
if (modules && modules.length) {
|
||||||
const details = this.utils.parseException(e);
|
resModules = resModules.concat(modules);
|
||||||
const errorMessage = `Failed to compile widget html. \n Error: ${details.message}`;
|
}
|
||||||
return of(errorMessage);
|
return resModules;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
|
} else {
|
||||||
|
modulesObservable = modules && modules.length ? of(modules) : of([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceTasks.push(
|
||||||
|
modulesObservable.pipe(
|
||||||
|
mergeMap((resolvedModules) => {
|
||||||
|
if (typeof resolvedModules === 'string') {
|
||||||
|
return of(resolvedModules);
|
||||||
|
} else {
|
||||||
|
return this.dynamicComponentFactoryService.createDynamicComponentFactory(
|
||||||
|
class DynamicWidgetComponentInstance extends DynamicWidgetComponent {},
|
||||||
|
widgetInfo.templateHtml,
|
||||||
|
resolvedModules
|
||||||
|
).pipe(
|
||||||
|
map((factory) => {
|
||||||
|
widgetInfo.componentFactory = factory;
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
catchError(e => {
|
||||||
|
const details = this.utils.parseException(e);
|
||||||
|
const errorMessage = `Failed to compile widget html. \n Error: ${details.message}`;
|
||||||
|
return of(errorMessage);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
return forkJoin(resourceTasks).pipe(
|
return forkJoin(resourceTasks).pipe(
|
||||||
switchMap(msgs => {
|
switchMap(msgs => {
|
||||||
|
|||||||
@ -129,6 +129,10 @@
|
|||||||
(ngModelChange)="isDirty = true"
|
(ngModelChange)="isDirty = true"
|
||||||
placeholder="{{ 'widget.resource-url' | translate }}"/>
|
placeholder="{{ 'widget.resource-url' | translate }}"/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
<mat-checkbox [(ngModel)]="resource.isModule"
|
||||||
|
(ngModelChange)="isDirty = true">
|
||||||
|
{{ 'widget.resource-is-module' | translate }}
|
||||||
|
</mat-checkbox>
|
||||||
<button mat-icon-button color="primary"
|
<button mat-icon-button color="primary"
|
||||||
[disabled]="isLoading$ | async"
|
[disabled]="isLoading$ | async"
|
||||||
(click)="removeResource(i)"
|
(click)="removeResource(i)"
|
||||||
|
|||||||
@ -53,6 +53,7 @@ export interface NodeScriptTestDialogData {
|
|||||||
msgType?: string;
|
msgType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @dynamic
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-node-script-test-dialog',
|
selector: 'tb-node-script-test-dialog',
|
||||||
templateUrl: './node-script-test-dialog.component.html',
|
templateUrl: './node-script-test-dialog.component.html',
|
||||||
|
|||||||
@ -114,6 +114,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
|
|||||||
|
|
||||||
export interface WidgetResource {
|
export interface WidgetResource {
|
||||||
url: string;
|
url: string;
|
||||||
|
isModule?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WidgetActionSource {
|
export interface WidgetActionSource {
|
||||||
|
|||||||
@ -1787,6 +1787,7 @@
|
|||||||
"type": "Widget type",
|
"type": "Widget type",
|
||||||
"resources": "Resources",
|
"resources": "Resources",
|
||||||
"resource-url": "JavaScript/CSS URL",
|
"resource-url": "JavaScript/CSS URL",
|
||||||
|
"resource-is-module": "Is module",
|
||||||
"remove-resource": "Remove resource",
|
"remove-resource": "Remove resource",
|
||||||
"add-resource": "Add resource",
|
"add-resource": "Add resource",
|
||||||
"html": "HTML",
|
"html": "HTML",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user