Commit b4f230dbc8dca59d43a831ab22d20d5a562475aa

Authored by Igor Kulikov
1 parent e264f7b8

Improve markdown component. Add new helps.

Showing 39 changed files with 1724 additions and 247 deletions
@@ -73,7 +73,7 @@ @@ -73,7 +73,7 @@
73 "ngx-drag-drop": "^2.0.0", 73 "ngx-drag-drop": "^2.0.0",
74 "ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master", 74 "ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master",
75 "ngx-hm-carousel": "^2.0.0-rc.1", 75 "ngx-hm-carousel": "^2.0.0-rc.1",
76 - "ngx-markdown": "^10.1.1", 76 + "ngx-markdown": "^11.1.3",
77 "ngx-sharebuttons": "^8.0.5", 77 "ngx-sharebuttons": "^8.0.5",
78 "ngx-translate-messageformat-compiler": "^4.9.0", 78 "ngx-translate-messageformat-compiler": "^4.9.0",
79 "objectpath": "^2.0.0", 79 "objectpath": "^2.0.0",
@@ -55,11 +55,12 @@ export class DynamicComponentFactoryService { @@ -55,11 +55,12 @@ export class DynamicComponentFactoryService {
55 public createDynamicComponentFactory<T>( 55 public createDynamicComponentFactory<T>(
56 componentType: Type<T>, 56 componentType: Type<T>,
57 template: string, 57 template: string,
58 - modules?: Type<any>[]): Observable<ComponentFactory<T>> { 58 + modules?: Type<any>[],
  59 + preserveWhitespaces?: boolean): Observable<ComponentFactory<T>> {
59 const dymamicComponentFactorySubject = new ReplaySubject<ComponentFactory<T>>(); 60 const dymamicComponentFactorySubject = new ReplaySubject<ComponentFactory<T>>();
60 import('@angular/compiler').then( 61 import('@angular/compiler').then(
61 () => { 62 () => {
62 - const comp = this.createDynamicComponent(componentType, template); 63 + const comp = this.createDynamicComponent(componentType, template, preserveWhitespaces);
63 let moduleImports: Type<any>[] = [CommonModule]; 64 let moduleImports: Type<any>[] = [CommonModule];
64 if (modules) { 65 if (modules) {
65 moduleImports = [...moduleImports, ...modules]; 66 moduleImports = [...moduleImports, ...modules];
@@ -103,10 +104,11 @@ export class DynamicComponentFactoryService { @@ -103,10 +104,11 @@ export class DynamicComponentFactoryService {
103 } 104 }
104 } 105 }
105 106
106 - private createDynamicComponent<T>(componentType: Type<T>, template: string): Type<T> { 107 + private createDynamicComponent<T>(componentType: Type<T>, template: string, preserveWhitespaces?: boolean): Type<T> {
107 // noinspection AngularMissingOrInvalidDeclarationInModule 108 // noinspection AngularMissingOrInvalidDeclarationInModule
108 return Component({ 109 return Component({
109 - template 110 + template,
  111 + preserveWhitespaces
110 })(componentType); 112 })(componentType);
111 } 113 }
112 114
@@ -18,7 +18,8 @@ import { Injectable } from '@angular/core'; @@ -18,7 +18,8 @@ import { Injectable } from '@angular/core';
18 import { HttpClient } from '@angular/common/http'; 18 import { HttpClient } from '@angular/common/http';
19 import { TranslateService } from '@ngx-translate/core'; 19 import { TranslateService } from '@ngx-translate/core';
20 import { Observable, of } from 'rxjs'; 20 import { Observable, of } from 'rxjs';
21 -import { catchError, tap } from 'rxjs/operators'; 21 +import { catchError, mergeMap, tap } from 'rxjs/operators';
  22 +import { helpBaseUrl } from '@shared/models/constants';
22 23
23 const NOT_FOUND_CONTENT = '## Not found'; 24 const NOT_FOUND_CONTENT = '## Not found';
24 25
@@ -27,6 +28,8 @@ const NOT_FOUND_CONTENT = '## Not found'; @@ -27,6 +28,8 @@ const NOT_FOUND_CONTENT = '## Not found';
27 }) 28 })
28 export class HelpService { 29 export class HelpService {
29 30
  31 + private helpBaseUrl = helpBaseUrl;
  32 +
30 private helpCache: {[lang: string]: {[key: string]: string}} = {}; 33 private helpCache: {[lang: string]: {[key: string]: string}} = {};
31 34
32 constructor( 35 constructor(
@@ -52,6 +55,9 @@ export class HelpService { @@ -52,6 +55,9 @@ export class HelpService {
52 return of(NOT_FOUND_CONTENT); 55 return of(NOT_FOUND_CONTENT);
53 } 56 }
54 }), 57 }),
  58 + mergeMap((content) => {
  59 + return this.processIncludes(this.processVariables(content));
  60 + }),
55 tap((content) => { 61 tap((content) => {
56 let langContent = this.helpCache[lang]; 62 let langContent = this.helpCache[lang];
57 if (!langContent) { 63 if (!langContent) {
@@ -68,4 +74,25 @@ export class HelpService { @@ -68,4 +74,25 @@ export class HelpService {
68 return this.http.get(`/assets/help/${lang}/${key}.md`, {responseType: 'text'} ); 74 return this.http.get(`/assets/help/${lang}/${key}.md`, {responseType: 'text'} );
69 } 75 }
70 76
  77 + private processVariables(content: string): string {
  78 + const baseUrlReg = /\${baseUrl}/g;
  79 + return content.replace(baseUrlReg, this.helpBaseUrl);
  80 + }
  81 +
  82 + private processIncludes(content: string): Observable<string> {
  83 + const includesRule = /{% include (.*) %}/;
  84 + const match = includesRule.exec(content);
  85 + if (match) {
  86 + const key = match[1];
  87 + return this.getHelpContent(key).pipe(
  88 + mergeMap((include) => {
  89 + content = content.replace(match[0], include);
  90 + return this.processIncludes(content);
  91 + })
  92 + );
  93 + } else {
  94 + return of(content);
  95 + }
  96 + }
  97 +
71 } 98 }
@@ -46,7 +46,8 @@ @@ -46,7 +46,8 @@
46 [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']" 46 [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']"
47 [disableUndefinedCheck]="true" 47 [disableUndefinedCheck]="true"
48 [validationArgs]="[]" 48 [validationArgs]="[]"
49 - [editorCompleter]="customPrettyActionEditorCompleter"> 49 + [editorCompleter]="customPrettyActionEditorCompleter"
  50 + helpId="widget/action/custom_pretty_action_fn">
50 </tb-js-func> 51 </tb-js-func>
51 </div> 52 </div>
52 </div> 53 </div>
@@ -96,7 +96,8 @@ @@ -96,7 +96,8 @@
96 [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']" 96 [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']"
97 [disableUndefinedCheck]="true" 97 [disableUndefinedCheck]="true"
98 [validationArgs]="[]" 98 [validationArgs]="[]"
99 - [editorCompleter]="customPrettyActionEditorCompleter"> 99 + [editorCompleter]="customPrettyActionEditorCompleter"
  100 + helpId="widget/action/custom_pretty_action_fn">
100 </tb-js-func> 101 </tb-js-func>
101 </mat-tab> 102 </mat-tab>
102 </mat-tab-group> 103 </mat-tab-group>
@@ -224,6 +224,7 @@ @@ -224,6 +224,7 @@
224 [globalVariables]="functionScopeVariables" 224 [globalVariables]="functionScopeVariables"
225 [validationArgs]="[]" 225 [validationArgs]="[]"
226 [editorCompleter]="customActionEditorCompleter" 226 [editorCompleter]="customActionEditorCompleter"
  227 + helpId="widget/action/custom_action_fn"
227 ></tb-js-func> 228 ></tb-js-func>
228 </ng-template> 229 </ng-template>
229 <ng-template [ngSwitchCase]="widgetActionType.customPretty"> 230 <ng-template [ngSwitchCase]="widgetActionType.customPretty">
@@ -70,6 +70,7 @@ @@ -70,6 +70,7 @@
70 [globalVariables]="functionScopeVariables" 70 [globalVariables]="functionScopeVariables"
71 [validationArgs]="[[1, 1],[1, '1']]" 71 [validationArgs]="[[1, 1],[1, '1']]"
72 resultType="any" 72 resultType="any"
  73 + helpId="widget/config/datakey_generation_fn"
73 formControlName="funcBody"> 74 formControlName="funcBody">
74 </tb-js-func> 75 </tb-js-func>
75 </section> 76 </section>
@@ -82,6 +83,7 @@ @@ -82,6 +83,7 @@
82 [globalVariables]="functionScopeVariables" 83 [globalVariables]="functionScopeVariables"
83 [validationArgs]="[[1, 1, 1, 1, 1],[1, '1', '1', 1, '1']]" 84 [validationArgs]="[[1, 1, 1, 1, 1],[1, '1', '1', 1, '1']]"
84 resultType="any" 85 resultType="any"
  86 + helpId="widget/config/datakey_postprocess_fn"
85 formControlName="postFuncBody"> 87 formControlName="postFuncBody">
86 </tb-js-func> 88 </tb-js-func>
87 <label *ngIf="dataKeyFormGroup.get('usePostProcessing').value" class="tb-title" style="margin-left: 15px;"> 89 <label *ngIf="dataKeyFormGroup.get('usePostProcessing').value" class="tb-title" style="margin-left: 15px;">
@@ -15,4 +15,4 @@ @@ -15,4 +15,4 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<markdown [data]="markdownText" lineNumbers class="tb-markdown-view" (click)="markdownClick($event)"></markdown> 18 +<tb-markdown [data]="markdownText" lineNumbers (click)="markdownClick($event)"></tb-markdown>
@@ -107,9 +107,9 @@ import { ComponentType } from '@angular/cdk/portal'; @@ -107,9 +107,9 @@ import { ComponentType } from '@angular/cdk/portal';
107 import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token'; 107 import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token';
108 import { MobileService } from '@core/services/mobile.service'; 108 import { MobileService } from '@core/services/mobile.service';
109 import { DialogService } from '@core/services/dialog.service'; 109 import { DialogService } from '@core/services/dialog.service';
110 -import { TbPopoverService } from '@shared/components/popover.component';  
111 import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component'; 110 import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component';
112 import { PopoverPlacement } from '@shared/components/popover.models'; 111 import { PopoverPlacement } from '@shared/components/popover.models';
  112 +import { TbPopoverService } from '@shared/components/popover.service';
113 113
114 @Component({ 114 @Component({
115 selector: 'tb-widget', 115 selector: 'tb-widget',
@@ -16,5 +16,5 @@ @@ -16,5 +16,5 @@
16 16
17 --> 17 -->
18 <ng-container *ngIf="markdownText$ | async as text;"> 18 <ng-container *ngIf="markdownText$ | async as text;">
19 - <markdown [data]="text" lineNumbers (ready)="onMarkdownReady()" class="tb-help-markdown tb-markdown-view" (click)="markdownClick($event)"></markdown> 19 + <tb-markdown [style]="style" [data]="text" lineNumbers (ready)="onMarkdownReady()" markdownClass="tb-help-markdown" (click)="markdownClick($event)"></tb-markdown>
20 </ng-container> 20 </ng-container>
@@ -13,16 +13,14 @@ @@ -13,16 +13,14 @@
13 * See the License for the specific language governing permissions and 13 * See the License for the specific language governing permissions and
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 -:host { 16 +
  17 +:host ::ng-deep {
17 .tb-help-markdown { 18 .tb-help-markdown {
18 overflow: auto; 19 overflow: auto;
19 max-width: 80vw; 20 max-width: 80vw;
20 max-height: 80vh; 21 max-height: 80vh;
21 margin-top: 30px; 22 margin-top: 30px;
22 } 23 }
23 -}  
24 -  
25 -:host ::ng-deep {  
26 .tb-help-markdown.tb-markdown-view { 24 .tb-help-markdown.tb-markdown-view {
27 h1, h2, h3, h4, h5, h6 { 25 h1, h2, h3, h4, h5, h6 {
28 &:first-child { 26 &:first-child {
@@ -22,7 +22,7 @@ import { @@ -22,7 +22,7 @@ import {
22 Output, SimpleChanges 22 Output, SimpleChanges
23 } from '@angular/core'; 23 } from '@angular/core';
24 import { BehaviorSubject } from 'rxjs'; 24 import { BehaviorSubject } from 'rxjs';
25 -import { delay, share } from 'rxjs/operators'; 25 +import { share } from 'rxjs/operators';
26 import { HelpService } from '@core/services/help.service'; 26 import { HelpService } from '@core/services/help.service';
27 27
28 @Component({ 28 @Component({
@@ -34,8 +34,12 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { @@ -34,8 +34,12 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges {
34 34
35 @Input() helpId: string; 35 @Input() helpId: string;
36 36
  37 + @Input() helpContent: string;
  38 +
37 @Input() visible: boolean; 39 @Input() visible: boolean;
38 40
  41 + @Input() style: { [klass: string]: any } = {};
  42 +
39 @Output() markdownReady = new EventEmitter<void>(); 43 @Output() markdownReady = new EventEmitter<void>();
40 44
41 markdownText = new BehaviorSubject<string>(null); 45 markdownText = new BehaviorSubject<string>(null);
@@ -44,8 +48,6 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { @@ -44,8 +48,6 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges {
44 share() 48 share()
45 ); 49 );
46 50
47 - isMarkdownReady = false;  
48 -  
49 private loadHelpPending = false; 51 private loadHelpPending = false;
50 52
51 constructor(private help: HelpService) {} 53 constructor(private help: HelpService) {}
@@ -68,7 +70,7 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { @@ -68,7 +70,7 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges {
68 this.loadHelp(); 70 this.loadHelp();
69 } 71 }
70 } 72 }
71 - if (propName === 'helpId') { 73 + if (propName === 'helpId' || propName === 'helpContent') {
72 this.markdownText.next(null); 74 this.markdownText.next(null);
73 this.loadHelpWhenVisible(); 75 this.loadHelpWhenVisible();
74 } 76 }
@@ -89,16 +91,16 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { @@ -89,16 +91,16 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges {
89 this.help.getHelpContent(this.helpId).subscribe((content) => { 91 this.help.getHelpContent(this.helpId).subscribe((content) => {
90 this.markdownText.next(content); 92 this.markdownText.next(content);
91 }); 93 });
  94 + } else if (this.helpContent) {
  95 + this.markdownText.next(this.helpContent);
92 } 96 }
93 } 97 }
94 98
95 onMarkdownReady() { 99 onMarkdownReady() {
96 - this.isMarkdownReady = true;  
97 this.markdownReady.next(); 100 this.markdownReady.next();
98 } 101 }
99 102
100 markdownClick($event: MouseEvent) { 103 markdownClick($event: MouseEvent) {
101 -  
102 } 104 }
103 105
104 } 106 }
@@ -15,16 +15,22 @@ @@ -15,16 +15,22 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div style="position: relative;">  
19 - <button color="primary" mat-button mat-icon-button  
20 - class="tb-help-popup-button tb-mat-32"  
21 - type="button"  
22 - (click)="toggleHelp()"  
23 - matTooltip="{{'help.show-help' | translate}}"  
24 - matTooltipPosition="above">  
25 - <mat-icon class="material-icons">{{popoverVisible ? 'help' : 'help_outline'}}</mat-icon>  
26 - </button>  
27 - <div *ngIf="popoverVisible && !popoverReady" fxFlex fxLayoutAlign="center center" class="tb-absolute-fill tb-help-popup-button-loading">  
28 - <mat-spinner mode="indeterminate" diameter="20" strokeWidth="2"></mat-spinner>  
29 - </div>  
30 -</div> 18 +<button #toggleHelpButton
  19 + *ngIf="!triggerText"
  20 + color="primary" mat-button mat-icon-button
  21 + class="tb-help-popup-button tb-mat-32"
  22 + type="button"
  23 + (click)="toggleHelp()"
  24 + matTooltip="{{'help.show-help' | translate}}"
  25 + matTooltipPosition="above">
  26 + <mat-icon class="material-icons">{{popoverVisible ? 'help' : 'help_outline'}}</mat-icon>
  27 + <mat-spinner *ngIf="popoverVisible && !popoverReady" class="tb-help-popup-button-loading" mode="indeterminate" diameter="20" strokeWidth="2"></mat-spinner>
  28 +</button>
  29 +<span #toggleHelpSpan
  30 + *ngIf="triggerText"
  31 + class="tb-help-popup-span"
  32 + (click)="toggleHelp()"
  33 + [ngClass]="{'active': popoverVisible, 'ready': popoverReady}">
  34 + <span>{{triggerText}}</span>
  35 + <mat-progress-bar *ngIf="popoverVisible && !popoverReady" color="primary" mode="indeterminate"></mat-progress-bar>
  36 +</span>
@@ -14,8 +14,54 @@ @@ -14,8 +14,54 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 .tb-help-popup-button { 16 .tb-help-popup-button {
  17 + position: relative;
17 } 18 }
18 .tb-help-popup-button-loading { 19 .tb-help-popup-button-loading {
19 background: #fff; 20 background: #fff;
20 border-radius: 50%; 21 border-radius: 50%;
  22 + width: 32px !important;
  23 + height: 32px !important;
  24 + position: absolute;
  25 + top: 0;
  26 + left: 0;
  27 + &.mat-progress-spinner {
  28 + svg {
  29 + top: 6px;
  30 + left: 6px;
  31 + }
  32 + }
  33 +}
  34 +
  35 +.tb-help-popup-span {
  36 + position: relative;
  37 + cursor: pointer;
  38 + -webkit-user-select: none;
  39 + -moz-user-select: none;
  40 + -ms-user-select: none;
  41 + user-select: none;
  42 + color: #2a7dec;
  43 + font-weight: 500;
  44 + &.active.ready {
  45 + text-decoration: underline;
  46 + }
  47 + &:hover {
  48 + &:not(.active) {
  49 + text-decoration: underline;
  50 + }
  51 + }
  52 + &:after {
  53 + display: inline-block;
  54 + font-family: "FontAwesome";
  55 + content: '\f08e';
  56 + padding-left: 5px;
  57 + font-size: 14px;
  58 + font-weight: 600;
  59 + }
  60 + .mat-progress-bar {
  61 + position: absolute;
  62 + height: 2px;
  63 + left: 0;
  64 + right: 0;
  65 + bottom: 0;
  66 + }
21 } 67 }
@@ -14,8 +14,18 @@ @@ -14,8 +14,18 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, ElementRef, Input, OnDestroy, Renderer2, ViewContainerRef, ViewEncapsulation } from '@angular/core';  
18 -import { TbPopoverService } from '@shared/components/popover.component'; 17 +import {
  18 + Component,
  19 + ElementRef,
  20 + Input,
  21 + OnDestroy,
  22 + Renderer2,
  23 + ViewChild,
  24 + ViewContainerRef,
  25 + ViewEncapsulation
  26 +} from '@angular/core';
  27 +import { TbPopoverService } from '@shared/components/popover.service';
  28 +import { PopoverPlacement } from '@shared/components/popover.models';
19 29
20 @Component({ 30 @Component({
21 // tslint:disable-next-line:component-selector 31 // tslint:disable-next-line:component-selector
@@ -26,25 +36,41 @@ import { TbPopoverService } from '@shared/components/popover.component'; @@ -26,25 +36,41 @@ import { TbPopoverService } from '@shared/components/popover.component';
26 }) 36 })
27 export class HelpPopupComponent implements OnDestroy { 37 export class HelpPopupComponent implements OnDestroy {
28 38
  39 + @ViewChild('toggleHelpButton', {read: ElementRef, static: false}) toggleHelpButton: ElementRef;
  40 + @ViewChild('toggleHelpSpan', {read: ElementRef, static: false}) toggleHelpSpan: ElementRef;
  41 +
29 // tslint:disable-next-line:no-input-rename 42 // tslint:disable-next-line:no-input-rename
30 @Input('tb-help-popup') helpId: string; 43 @Input('tb-help-popup') helpId: string;
31 44
  45 + // tslint:disable-next-line:no-input-rename
  46 + @Input('trigger-text') triggerText: string;
  47 +
  48 + // tslint:disable-next-line:no-input-rename
  49 + @Input('tb-help-popup-placement') helpPopupPlacement: PopoverPlacement;
  50 +
  51 + // tslint:disable-next-line:no-input-rename
  52 + @Input('tb-help-popup-style') helpPopupStyle: { [klass: string]: any } = {};
  53 +
32 popoverVisible = false; 54 popoverVisible = false;
33 popoverReady = true; 55 popoverReady = true;
34 56
35 - constructor(private elementRef: ElementRef,  
36 - private viewContainerRef: ViewContainerRef, 57 + constructor(private viewContainerRef: ViewContainerRef,
37 private renderer: Renderer2, 58 private renderer: Renderer2,
38 private popoverService: TbPopoverService) {} 59 private popoverService: TbPopoverService) {}
39 60
40 toggleHelp() { 61 toggleHelp() {
41 - this.popoverService.toggleHelpPopover(this.elementRef.nativeElement, this.renderer, this.viewContainerRef, 62 + const trigger = this.triggerText ? this.toggleHelpSpan.nativeElement : this.toggleHelpButton.nativeElement;
  63 + this.popoverService.toggleHelpPopover(trigger, this.renderer, this.viewContainerRef,
42 this.helpId, 64 this.helpId,
  65 + '',
43 (visible) => { 66 (visible) => {
44 this.popoverVisible = visible; 67 this.popoverVisible = visible;
45 }, (ready => { 68 }, (ready => {
46 this.popoverReady = ready; 69 this.popoverReady = ready;
47 - })); 70 + }),
  71 + this.helpPopupPlacement,
  72 + {},
  73 + this.helpPopupStyle);
48 } 74 }
49 75
50 ngOnDestroy(): void { 76 ngOnDestroy(): void {
@@ -46,8 +46,7 @@ import { GroupInfo } from '@shared/models/widget.models'; @@ -46,8 +46,7 @@ import { GroupInfo } from '@shared/models/widget.models';
46 import { Observable } from 'rxjs/internal/Observable'; 46 import { Observable } from 'rxjs/internal/Observable';
47 import { forkJoin, from } from 'rxjs'; 47 import { forkJoin, from } from 'rxjs';
48 import { MouseEvent } from 'react'; 48 import { MouseEvent } from 'react';
49 -import { TbPopoverService } from '@shared/components/popover.component';  
50 -import { HelpMarkdownComponent } from '@shared/components/help-markdown.component'; 49 +import { TbPopoverService } from '@shared/components/popover.service';
51 50
52 const tinycolor = tinycolor_; 51 const tinycolor = tinycolor_;
53 52
@@ -252,7 +251,7 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato @@ -252,7 +251,7 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
252 251
253 private onHelpClick(event: MouseEvent, helpId: string, helpVisibleFn: (visible: boolean) => void, helpReadyFn: (ready: boolean) => void) { 252 private onHelpClick(event: MouseEvent, helpId: string, helpVisibleFn: (visible: boolean) => void, helpReadyFn: (ready: boolean) => void) {
254 const trigger = event.currentTarget as Element; 253 const trigger = event.currentTarget as Element;
255 - this.popoverService.toggleHelpPopover(trigger, this.renderer, this.viewContainerRef, helpId, helpVisibleFn, helpReadyFn); 254 + this.popoverService.toggleHelpPopover(trigger, this.renderer, this.viewContainerRef, helpId, '', helpVisibleFn, helpReadyFn);
256 } 255 }
257 256
258 private updateAndRender() { 257 private updateAndRender() {
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<ng-container #markdownContainer>
  19 + <markdown *ngIf="!isMarkdownReady" #markdownComponent [data]="data"
  20 + [lineNumbers]="lineNumbers" (ready)="onMarkdownReady()"></markdown>
  21 +</ng-container>
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + markdown {
  18 + display: none;
  19 + }
  20 +}
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + Component,
  19 + ComponentFactory,
  20 + ComponentRef,
  21 + EventEmitter,
  22 + Inject,
  23 + Injector,
  24 + Input, OnChanges,
  25 + OnDestroy,
  26 + OnInit,
  27 + Output,
  28 + Renderer2, SimpleChanges,
  29 + Type,
  30 + ViewChild,
  31 + ViewContainerRef
  32 +} from '@angular/core';
  33 +import { HelpService } from '@core/services/help.service';
  34 +import { MarkdownComponent } from 'ngx-markdown';
  35 +import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service';
  36 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  37 +import { SHARED_MODULE_TOKEN } from '@shared/components/tokens';
  38 +
  39 +@Component({
  40 + selector: 'tb-markdown',
  41 + templateUrl: './markdown.component.html',
  42 + styleUrls: ['./markdown.component.scss']
  43 +})
  44 +export class TbMarkdownComponent implements OnDestroy, OnInit, OnChanges {
  45 +
  46 + private markdownComponent: MarkdownComponent;
  47 +
  48 + @ViewChild('markdownContainer', {read: ViewContainerRef, static: true}) markdownContainer: ViewContainerRef;
  49 +
  50 + @ViewChild('markdownComponent', {static: false}) set content(content: MarkdownComponent) {
  51 + this.markdownComponent = content;
  52 + if (this.isMarkdownReady && this.markdownComponent) {
  53 + this.processMarkdownComponent();
  54 + }
  55 + }
  56 +
  57 + @Input() data: string | undefined;
  58 +
  59 + @Input() markdownClass: string | undefined;
  60 +
  61 + @Input() style: { [klass: string]: any } = {};
  62 +
  63 + @Input()
  64 + get lineNumbers(): boolean { return this.lineNumbersValue; }
  65 + set lineNumbers(value: boolean) { this.lineNumbersValue = coerceBooleanProperty(value); }
  66 +
  67 + @Output() ready = new EventEmitter<void>();
  68 +
  69 + private lineNumbersValue = false;
  70 +
  71 + isMarkdownReady = false;
  72 +
  73 + private tbMarkdownInstanceComponentRef: ComponentRef<any>;
  74 + private tbMarkdownInstanceComponentFactory: ComponentFactory<any>;
  75 +
  76 + constructor(private help: HelpService,
  77 + private renderer: Renderer2,
  78 + @Inject(SHARED_MODULE_TOKEN) private sharedModule: Type<any>,
  79 + private dynamicComponentFactoryService: DynamicComponentFactoryService) {}
  80 +
  81 + ngOnInit(): void {
  82 + }
  83 +
  84 + ngOnDestroy(): void {
  85 + }
  86 +
  87 + ngOnChanges(changes: SimpleChanges): void {
  88 + for (const propName of Object.keys(changes)) {
  89 + const change = changes[propName];
  90 + if (!change.firstChange && change.currentValue !== change.previousValue) {
  91 + if (propName === 'data') {
  92 + if (this.data) {
  93 + this.isMarkdownReady = false;
  94 + }
  95 + }
  96 + }
  97 + }
  98 + }
  99 +
  100 + private processMarkdownComponent() {
  101 + let template = this.markdownComponent.element.nativeElement.innerHTML;
  102 + template = this.sanitizeCurlyBraces(template);
  103 + let markdownClass = 'tb-markdown-view';
  104 + if (this.markdownClass) {
  105 + markdownClass += ` ${this.markdownClass}`;
  106 + }
  107 + template = `<div [ngStyle]="style" class="${markdownClass}">${template}</div>`;
  108 + this.markdownContainer.clear();
  109 + this.markdownComponent = null;
  110 + const parent = this;
  111 + this.dynamicComponentFactoryService.createDynamicComponentFactory(
  112 + class TbMarkdownInstance {
  113 + ngOnDestroy(): void {
  114 + parent.destroyMarkdownInstanceResources();
  115 + }
  116 + },
  117 + template,
  118 + [this.sharedModule],
  119 + true
  120 + ).subscribe((factory) => {
  121 + this.tbMarkdownInstanceComponentFactory = factory;
  122 + const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector});
  123 + try {
  124 + this.tbMarkdownInstanceComponentRef =
  125 + this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentFactory, 0, injector);
  126 + this.tbMarkdownInstanceComponentRef.instance.style = this.style;
  127 + } catch (e) {
  128 + this.destroyMarkdownInstanceResources();
  129 + }
  130 + this.ready.emit();
  131 + });
  132 + }
  133 +
  134 + private sanitizeCurlyBraces(template: string): string {
  135 + return template.replace(/{/g, '&#123;').replace(/}/g, '&#125;');
  136 + }
  137 +
  138 + private destroyMarkdownInstanceResources() {
  139 + if (this.tbMarkdownInstanceComponentFactory) {
  140 + this.dynamicComponentFactoryService.destroyDynamicComponentFactory(this.tbMarkdownInstanceComponentFactory);
  141 + this.tbMarkdownInstanceComponentFactory = null;
  142 + }
  143 + this.tbMarkdownInstanceComponentRef = null;
  144 + }
  145 +
  146 + onMarkdownReady() {
  147 + if (this.data) {
  148 + this.isMarkdownReady = true;
  149 + if (this.markdownComponent) {
  150 + this.processMarkdownComponent();
  151 + }
  152 + }
  153 + }
  154 +}
@@ -85,7 +85,7 @@ export class MarkedOptionsService extends MarkedOptions { @@ -85,7 +85,7 @@ export class MarkedOptionsService extends MarkedOptions {
85 85
86 private wrapCopyCode(id: number, content: string, code: string): string { 86 private wrapCopyCode(id: number, content: string, code: string): string {
87 return `<div class="code-wrapper noChars" id="codeWrapper${id}" onClick="markdownCopyCode(${id})">${content}` + 87 return `<div class="code-wrapper noChars" id="codeWrapper${id}" onClick="markdownCopyCode(${id})">${content}` +
88 - `<span id="copyCodeId${id}" style="display: none;">${code}</span>` + 88 + `<span id="copyCodeId${id}" style="display: none;">${encodeURIComponent(code)}</span>` +
89 `<button class="clipboard-btn">\n` + 89 `<button class="clipboard-btn">\n` +
90 ` <p>${this.translate.instant('markdown.copy-code')}</p>\n` + 90 ` <p>${this.translate.instant('markdown.copy-code')}</p>\n` +
91 ` <div>\n` + 91 ` <div>\n` +
@@ -119,7 +119,7 @@ export class MarkedOptionsService extends MarkedOptions { @@ -119,7 +119,7 @@ export class MarkedOptionsService extends MarkedOptions {
119 private markdownCopyCode(id: number) { 119 private markdownCopyCode(id: number) {
120 const copyWrapper = $('#codeWrapper' + id); 120 const copyWrapper = $('#codeWrapper' + id);
121 if (copyWrapper.hasClass('noChars')) { 121 if (copyWrapper.hasClass('noChars')) {
122 - const text = $('#copyCodeId' + id).text(); 122 + const text = decodeURIComponent($('#copyCodeId' + id).text());
123 this.window.navigator.clipboard.writeText(text).then(() => { 123 this.window.navigator.clipboard.writeText(text).then(() => {
124 import('tooltipster').then( 124 import('tooltipster').then(
125 () => { 125 () => {
@@ -25,7 +25,6 @@ import { @@ -25,7 +25,6 @@ import {
25 Directive, 25 Directive,
26 ElementRef, 26 ElementRef,
27 EventEmitter, 27 EventEmitter,
28 - Injectable,  
29 Injector, 28 Injector,
30 Input, 29 Input,
31 OnChanges, 30 OnChanges,
@@ -36,7 +35,6 @@ import { @@ -36,7 +35,6 @@ import {
36 Renderer2, 35 Renderer2,
37 SimpleChanges, 36 SimpleChanges,
38 TemplateRef, 37 TemplateRef,
39 - Type,  
40 ViewChild, 38 ViewChild,
41 ViewContainerRef, 39 ViewContainerRef,
42 ViewEncapsulation 40 ViewEncapsulation
@@ -54,13 +52,11 @@ import { @@ -54,13 +52,11 @@ import {
54 getPlacementName, 52 getPlacementName,
55 popoverMotion, 53 popoverMotion,
56 PopoverPlacement, 54 PopoverPlacement,
57 - PopoverWithTrigger,  
58 POSITION_MAP, 55 POSITION_MAP,
59 PropertyMapping 56 PropertyMapping
60 } from '@shared/components/popover.models'; 57 } from '@shared/components/popover.models';
61 import { distinctUntilChanged, takeUntil } from 'rxjs/operators'; 58 import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
62 import { isNotEmptyStr, onParentScrollOrWindowResize } from '@core/utils'; 59 import { isNotEmptyStr, onParentScrollOrWindowResize } from '@core/utils';
63 -import { HelpMarkdownComponent } from '@shared/components/help-markdown.component';  
64 60
65 export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null; 61 export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null;
66 62
@@ -285,162 +281,6 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit { @@ -285,162 +281,6 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
285 } 281 }
286 } 282 }
287 283
288 -@Injectable()  
289 -export class TbPopoverService {  
290 -  
291 - private popoverWithTriggers: PopoverWithTrigger[] = [];  
292 -  
293 - componentFactory: ComponentFactory<TbPopoverComponent> = this.resolver.resolveComponentFactory(TbPopoverComponent);  
294 -  
295 - constructor(private resolver: ComponentFactoryResolver) {  
296 - }  
297 -  
298 - hasPopover(trigger: Element): boolean {  
299 - const res = this.findPopoverByTrigger(trigger);  
300 - return res !== null;  
301 - }  
302 -  
303 - hidePopover(trigger: Element): boolean {  
304 - const component: TbPopoverComponent = this.findPopoverByTrigger(trigger);  
305 - if (component && component.tbVisible) {  
306 - component.hide();  
307 - return true;  
308 - } else {  
309 - return false;  
310 - }  
311 - }  
312 -  
313 - displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,  
314 - componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true,  
315 - injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any): TbPopoverComponent {  
316 - const componentRef = hostView.createComponent(this.componentFactory);  
317 - const component = componentRef.instance;  
318 - this.popoverWithTriggers.push({  
319 - trigger,  
320 - popoverComponent: component  
321 - });  
322 - renderer.removeChild(  
323 - renderer.parentNode(trigger),  
324 - componentRef.location.nativeElement  
325 - );  
326 - const originElementRef = new ElementRef(trigger);  
327 - component.setOverlayOrigin({ elementRef: originElementRef });  
328 - component.tbPlacement = preferredPlacement;  
329 - component.tbComponentFactory = this.resolver.resolveComponentFactory(componentType);  
330 - component.tbComponentInjector = injector;  
331 - component.tbComponentContext = context;  
332 - component.tbOverlayStyle = overlayStyle;  
333 - component.tbPopoverInnerStyle = popoverStyle;  
334 - component.tbComponentStyle = style;  
335 - component.tbHideOnClickOutside = hideOnClickOutside;  
336 - component.tbVisibleChange.subscribe((visible: boolean) => {  
337 - if (!visible) {  
338 - component.tbAnimationDone.subscribe(() => {  
339 - componentRef.destroy();  
340 - });  
341 - }  
342 - });  
343 - component.tbDestroy.subscribe(() => {  
344 - this.removePopoverByComponent(component);  
345 - });  
346 - component.show();  
347 - return component;  
348 - }  
349 -  
350 - toggleHelpPopover(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, helpId: string,  
351 - visibleFn: (visible: boolean) => void, readyFn: (ready: boolean) => void) {  
352 - if (this.hasPopover(trigger)) {  
353 - this.hidePopover(trigger);  
354 - } else {  
355 - readyFn(false);  
356 - const injector = Injector.create({  
357 - parent: hostView.injector, providers: []  
358 - });  
359 - const componentRef = hostView.createComponent(this.componentFactory);  
360 - const component = componentRef.instance;  
361 - this.popoverWithTriggers.push({  
362 - trigger,  
363 - popoverComponent: component  
364 - });  
365 - renderer.removeChild(  
366 - renderer.parentNode(trigger),  
367 - componentRef.location.nativeElement  
368 - );  
369 - const originElementRef = new ElementRef(trigger);  
370 - component.tbAnimationState = 'void';  
371 - component.tbOverlayStyle = { opacity: '0' };  
372 - component.setOverlayOrigin({ elementRef: originElementRef });  
373 - component.tbPlacement = 'bottom';  
374 - component.tbComponentFactory = this.resolver.resolveComponentFactory(HelpMarkdownComponent);  
375 - component.tbComponentInjector = injector;  
376 - component.tbComponentContext = {  
377 - helpId,  
378 - visible: true  
379 - };  
380 - component.tbHideOnClickOutside = true;  
381 - component.tbVisibleChange.subscribe((visible: boolean) => {  
382 - if (!visible) {  
383 - visibleFn(false);  
384 - component.tbAnimationDone.subscribe(() => {  
385 - componentRef.destroy();  
386 - });  
387 - }  
388 - });  
389 - component.tbDestroy.subscribe(() => {  
390 - this.removePopoverByComponent(component);  
391 - });  
392 - const showHelpMarkdownComponent = () => {  
393 - component.tbOverlayStyle = { opacity: '1' };  
394 - component.tbAnimationState = 'active';  
395 - component.updatePosition();  
396 - readyFn(true);  
397 - setTimeout(() => {  
398 - component.updatePosition();  
399 - });  
400 - };  
401 - const setupHelpMarkdownComponent = (helpMarkdownComponent: HelpMarkdownComponent) => {  
402 - if (helpMarkdownComponent.isMarkdownReady) {  
403 - showHelpMarkdownComponent();  
404 - } else {  
405 - helpMarkdownComponent.markdownReady.subscribe(() => {  
406 - showHelpMarkdownComponent();  
407 - });  
408 - }  
409 - };  
410 - if (component.tbComponentRef) {  
411 - setupHelpMarkdownComponent(component.tbComponentRef.instance);  
412 - } else {  
413 - component.tbComponentChange.subscribe((helpMarkdownComponentRef) => {  
414 - setupHelpMarkdownComponent(helpMarkdownComponentRef.instance);  
415 - });  
416 - }  
417 - component.show();  
418 - visibleFn(true);  
419 - }  
420 - }  
421 -  
422 - private findPopoverByTrigger(trigger: Element): TbPopoverComponent | null {  
423 - const res = this.popoverWithTriggers.find(val => this.elementsAreEqualOrDescendant(trigger, val.trigger));  
424 - if (res) {  
425 - return res.popoverComponent;  
426 - } else {  
427 - return null;  
428 - }  
429 - }  
430 -  
431 - private removePopoverByComponent(component: TbPopoverComponent): void {  
432 - const index = this.popoverWithTriggers.findIndex(val => val.popoverComponent === component);  
433 - if (index > -1) {  
434 - this.popoverWithTriggers.splice(index, 1);  
435 - }  
436 - }  
437 -  
438 - private elementsAreEqualOrDescendant(element1: Element, element2: Element): boolean {  
439 - return element1 === element2 || element1.contains(element2) || element2.contains(element1);  
440 - }  
441 -}  
442 -  
443 -  
444 @Component({ 284 @Component({
445 selector: 'tb-popover', 285 selector: 'tb-popover',
446 exportAs: 'tbPopoverComponent', 286 exportAs: 'tbPopoverComponent',
@@ -703,10 +543,12 @@ export class TbPopoverComponent implements OnDestroy, OnInit { @@ -703,10 +543,12 @@ export class TbPopoverComponent implements OnDestroy, OnInit {
703 543
704 updateStyles(): void { 544 updateStyles(): void {
705 this.classMap = { 545 this.classMap = {
706 - [this.tbOverlayClassName]: true,  
707 [`tb-popover-placement-${this.preferredPlacement}`]: true, 546 [`tb-popover-placement-${this.preferredPlacement}`]: true,
708 ['tb-popover-hidden']: this.tbHidden || !this.lastIsIntersecting 547 ['tb-popover-hidden']: this.tbHidden || !this.lastIsIntersecting
709 }; 548 };
  549 + if (this.tbOverlayClassName) {
  550 + this.classMap[this.tbOverlayClassName] = true;
  551 + }
710 } 552 }
711 553
712 setOverlayOrigin(origin: CdkOverlayOrigin): void { 554 setOverlayOrigin(origin: CdkOverlayOrigin): void {
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + ComponentFactory,
  19 + ComponentFactoryResolver, ElementRef, Inject,
  20 + Injectable, Injector,
  21 + Renderer2,
  22 + Type,
  23 + ViewContainerRef
  24 +} from '@angular/core';
  25 +import { PopoverPlacement, PopoverWithTrigger } from '@shared/components/popover.models';
  26 +import { TbPopoverComponent } from '@shared/components/popover.component';
  27 +import { ComponentType } from '@angular/cdk/portal';
  28 +import { HELP_MARKDOWN_COMPONENT_TOKEN } from '@shared/components/tokens';
  29 +
  30 +@Injectable()
  31 +export class TbPopoverService {
  32 +
  33 + private popoverWithTriggers: PopoverWithTrigger[] = [];
  34 +
  35 + componentFactory: ComponentFactory<TbPopoverComponent> = this.resolver.resolveComponentFactory(TbPopoverComponent);
  36 +
  37 + constructor(private resolver: ComponentFactoryResolver,
  38 + @Inject(HELP_MARKDOWN_COMPONENT_TOKEN) private helpMarkdownComponent: ComponentType<any>) {
  39 + }
  40 +
  41 + hasPopover(trigger: Element): boolean {
  42 + const res = this.findPopoverByTrigger(trigger);
  43 + return res !== null;
  44 + }
  45 +
  46 + hidePopover(trigger: Element): boolean {
  47 + const component: TbPopoverComponent = this.findPopoverByTrigger(trigger);
  48 + if (component && component.tbVisible) {
  49 + component.hide();
  50 + return true;
  51 + } else {
  52 + return false;
  53 + }
  54 + }
  55 +
  56 + displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,
  57 + componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true,
  58 + injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any): TbPopoverComponent {
  59 + const componentRef = hostView.createComponent(this.componentFactory);
  60 + const component = componentRef.instance;
  61 + this.popoverWithTriggers.push({
  62 + trigger,
  63 + popoverComponent: component
  64 + });
  65 + renderer.removeChild(
  66 + renderer.parentNode(trigger),
  67 + componentRef.location.nativeElement
  68 + );
  69 + const originElementRef = new ElementRef(trigger);
  70 + component.setOverlayOrigin({ elementRef: originElementRef });
  71 + component.tbPlacement = preferredPlacement;
  72 + component.tbComponentFactory = this.resolver.resolveComponentFactory(componentType);
  73 + component.tbComponentInjector = injector;
  74 + component.tbComponentContext = context;
  75 + component.tbOverlayStyle = overlayStyle;
  76 + component.tbPopoverInnerStyle = popoverStyle;
  77 + component.tbComponentStyle = style;
  78 + component.tbHideOnClickOutside = hideOnClickOutside;
  79 + component.tbVisibleChange.subscribe((visible: boolean) => {
  80 + if (!visible) {
  81 + component.tbAnimationDone.subscribe(() => {
  82 + componentRef.destroy();
  83 + });
  84 + }
  85 + });
  86 + component.tbDestroy.subscribe(() => {
  87 + this.removePopoverByComponent(component);
  88 + });
  89 + component.show();
  90 + return component;
  91 + }
  92 +
  93 + toggleHelpPopover(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, helpId = '',
  94 + helpContent = '',
  95 + visibleFn: (visible: boolean) => void = () => {},
  96 + readyFn: (ready: boolean) => void = () => {},
  97 + preferredPlacement: PopoverPlacement = 'bottom',
  98 + overlayStyle: any = {}, helpStyle: any = {}) {
  99 + if (this.hasPopover(trigger)) {
  100 + this.hidePopover(trigger);
  101 + } else {
  102 + readyFn(false);
  103 + const injector = Injector.create({
  104 + parent: hostView.injector, providers: []
  105 + });
  106 + const componentRef = hostView.createComponent(this.componentFactory);
  107 + const component = componentRef.instance;
  108 + this.popoverWithTriggers.push({
  109 + trigger,
  110 + popoverComponent: component
  111 + });
  112 + renderer.removeChild(
  113 + renderer.parentNode(trigger),
  114 + componentRef.location.nativeElement
  115 + );
  116 + const originElementRef = new ElementRef(trigger);
  117 + component.tbAnimationState = 'void';
  118 + component.tbOverlayStyle = {...overlayStyle, opacity: '0' };
  119 + component.setOverlayOrigin({ elementRef: originElementRef });
  120 + component.tbPlacement = preferredPlacement;
  121 + component.tbComponentFactory = this.resolver.resolveComponentFactory(this.helpMarkdownComponent);
  122 + component.tbComponentInjector = injector;
  123 + component.tbComponentContext = {
  124 + helpId,
  125 + helpContent,
  126 + style: helpStyle,
  127 + visible: true
  128 + };
  129 + component.tbHideOnClickOutside = true;
  130 + component.tbVisibleChange.subscribe((visible: boolean) => {
  131 + if (!visible) {
  132 + visibleFn(false);
  133 + component.tbAnimationDone.subscribe(() => {
  134 + componentRef.destroy();
  135 + });
  136 + }
  137 + });
  138 + component.tbDestroy.subscribe(() => {
  139 + this.removePopoverByComponent(component);
  140 + });
  141 + const showHelpMarkdownComponent = () => {
  142 + component.tbOverlayStyle = {...component.tbOverlayStyle, opacity: '1' };
  143 + component.tbAnimationState = 'active';
  144 + component.updatePosition();
  145 + readyFn(true);
  146 + setTimeout(() => {
  147 + component.updatePosition();
  148 + });
  149 + };
  150 + const setupHelpMarkdownComponent = (helpMarkdownComponent: any) => {
  151 + if (helpMarkdownComponent.isMarkdownReady) {
  152 + showHelpMarkdownComponent();
  153 + } else {
  154 + helpMarkdownComponent.markdownReady.subscribe(() => {
  155 + showHelpMarkdownComponent();
  156 + });
  157 + }
  158 + };
  159 + if (component.tbComponentRef) {
  160 + setupHelpMarkdownComponent(component.tbComponentRef.instance);
  161 + } else {
  162 + component.tbComponentChange.subscribe((helpMarkdownComponentRef) => {
  163 + setupHelpMarkdownComponent(helpMarkdownComponentRef.instance);
  164 + });
  165 + }
  166 + component.show();
  167 + visibleFn(true);
  168 + }
  169 + }
  170 +
  171 + private findPopoverByTrigger(trigger: Element): TbPopoverComponent | null {
  172 + const res = this.popoverWithTriggers.find(val => this.elementsAreEqualOrDescendant(trigger, val.trigger));
  173 + if (res) {
  174 + return res.popoverComponent;
  175 + } else {
  176 + return null;
  177 + }
  178 + }
  179 +
  180 + private removePopoverByComponent(component: TbPopoverComponent): void {
  181 + const index = this.popoverWithTriggers.findIndex(val => val.popoverComponent === component);
  182 + if (index > -1) {
  183 + this.popoverWithTriggers.splice(index, 1);
  184 + }
  185 + }
  186 +
  187 + private elementsAreEqualOrDescendant(element1: Element, element2: Element): boolean {
  188 + return element1 === element2 || element1.contains(element2) || element2.contains(element1);
  189 + }
  190 +}
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { InjectionToken, Type } from '@angular/core';
  18 +import { ComponentType } from '@angular/cdk/portal';
  19 +
  20 +export const HELP_MARKDOWN_COMPONENT_TOKEN: InjectionToken<ComponentType<any>> =
  21 + new InjectionToken<ComponentType<any>>('HELP_MARKDOWN_COMPONENT_TOKEN');
  22 +
  23 +export const SHARED_MODULE_TOKEN: InjectionToken<Type<any>> =
  24 + new InjectionToken<Type<any>>('HELP_MARKDOWN_COMPONENT_TOKEN');
@@ -54,7 +54,7 @@ export const MediaBreakpoints = { @@ -54,7 +54,7 @@ export const MediaBreakpoints = {
54 'gt-xl': 'screen and (min-width: 5001px)' 54 'gt-xl': 'screen and (min-width: 5001px)'
55 }; 55 };
56 56
57 -const helpBaseUrl = 'https://thingsboard.io'; 57 +export const helpBaseUrl = 'https://thingsboard.io';
58 58
59 export const HelpLinks = { 59 export const HelpLinks = {
60 linksMap: { 60 linksMap: {
@@ -147,11 +147,14 @@ import { MAT_DATE_LOCALE } from '@angular/material/core'; @@ -147,11 +147,14 @@ import { MAT_DATE_LOCALE } from '@angular/material/core';
147 import { CopyButtonComponent } from '@shared/components/button/copy-button.component'; 147 import { CopyButtonComponent } from '@shared/components/button/copy-button.component';
148 import { TogglePasswordComponent } from '@shared/components/button/toggle-password.component'; 148 import { TogglePasswordComponent } from '@shared/components/button/toggle-password.component';
149 import { HelpPopupComponent } from '@shared/components/help-popup.component'; 149 import { HelpPopupComponent } from '@shared/components/help-popup.component';
150 -import { TbPopoverComponent, TbPopoverDirective, TbPopoverService } from '@shared/components/popover.component'; 150 +import { TbPopoverComponent, TbPopoverDirective } from '@shared/components/popover.component';
151 import { TbStringTemplateOutletDirective } from '@shared/components/directives/sring-template-outlet.directive'; 151 import { TbStringTemplateOutletDirective } from '@shared/components/directives/sring-template-outlet.directive';
152 import { TbComponentOutletDirective} from '@shared/components/directives/component-outlet.directive'; 152 import { TbComponentOutletDirective} from '@shared/components/directives/component-outlet.directive';
153 import { HelpMarkdownComponent } from '@shared/components/help-markdown.component'; 153 import { HelpMarkdownComponent } from '@shared/components/help-markdown.component';
154 import { MarkedOptionsService } from '@shared/components/marked-options.service'; 154 import { MarkedOptionsService } from '@shared/components/marked-options.service';
  155 +import { TbPopoverService } from '@shared/components/popover.service';
  156 +import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens';
  157 +import { TbMarkdownComponent } from '@shared/components/markdown.component';
155 158
156 export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { 159 export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
157 return markedOptionsService; 160 return markedOptionsService;
@@ -174,6 +177,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) @@ -174,6 +177,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
174 provide: MAT_DATE_LOCALE, 177 provide: MAT_DATE_LOCALE,
175 useValue: 'en-GB' 178 useValue: 'en-GB'
176 }, 179 },
  180 + { provide: HELP_MARKDOWN_COMPONENT_TOKEN, useValue: HelpMarkdownComponent },
  181 + { provide: SHARED_MODULE_TOKEN, useValue: SharedModule },
177 TbPopoverService 182 TbPopoverService
178 ], 183 ],
179 declarations: [ 184 declarations: [
@@ -190,6 +195,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) @@ -190,6 +195,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
190 TbStringTemplateOutletDirective, 195 TbStringTemplateOutletDirective,
191 TbComponentOutletDirective, 196 TbComponentOutletDirective,
192 TbPopoverDirective, 197 TbPopoverDirective,
  198 + TbMarkdownComponent,
193 HelpComponent, 199 HelpComponent,
194 HelpMarkdownComponent, 200 HelpMarkdownComponent,
195 HelpPopupComponent, 201 HelpPopupComponent,
@@ -336,6 +342,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) @@ -336,6 +342,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
336 TbStringTemplateOutletDirective, 342 TbStringTemplateOutletDirective,
337 TbComponentOutletDirective, 343 TbComponentOutletDirective,
338 TbPopoverDirective, 344 TbPopoverDirective,
  345 + TbMarkdownComponent,
339 HelpComponent, 346 HelpComponent,
340 HelpMarkdownComponent, 347 HelpMarkdownComponent,
341 HelpPopupComponent, 348 HelpPopupComponent,
  1 +#### Custom action function
  2 +
  3 +<div class="divider"></div>
  4 +<br/>
  5 +
  6 +*function ($event, widgetContext, entityId, entityName, additionalParams, entityLabel): void*
  7 +
  8 +A JavaScript function performing custom action.
  9 +
  10 +**Parameters:**
  11 +
  12 +<ul>
  13 + <li><b>$event:</b> <code><a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a></code> - The <a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a> object. Usually a result of a mouse click event.
  14 + </li>
  15 + <li><b>widgetContext:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a></code> - A reference to <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a> that has all necessary API
  16 + and data used by widget instance.
  17 + </li>
  18 + <li><b>entityId:</b> <code>string</code> - An optional string id of the target entity.
  19 + </li>
  20 + <li><b>entityName:</b> <code>string</code> - An optional string name of the target entity.
  21 + </li>
  22 + {% include widget/action/custom_additional_params %}
  23 + <li><b>entityLabel:</b> <code>string</code> - An optional string label of the target entity.
  24 + </li>
  25 +</ul>
  26 +
  27 +<div class="divider"></div>
  28 +
  29 +##### Examples
  30 +
  31 +* Display alert dialog with entity information:
  32 +
  33 +```javascript
  34 +var title;
  35 +var content;
  36 +if (entityName) {
  37 + title = entityName + ' details';
  38 + content = '<b>Entity name</b>: ' + entityName;
  39 + if (additionalParams && additionalParams.entity) {
  40 + var entity = additionalParams.entity;
  41 + if (entity.id) {
  42 + content += '<br><b>Entity type</b>: ' + entity.id.entityType;
  43 + }
  44 + if (!isNaN(entity.temperature) && entity.temperature !== '') {
  45 + content += '<br><b>Temperature</b>: ' + entity.temperature + ' °C';
  46 + }
  47 + }
  48 +} else {
  49 + title = 'No entity information available';
  50 + content = '<b>No entity information available</b>';
  51 +}
  52 +
  53 +showAlertDialog(title, content);
  54 +
  55 +function showAlertDialog(title, content) {
  56 + setTimeout(function() {
  57 + widgetContext.dialogs.alert(title, content).subscribe();
  58 + }, 100);
  59 +}
  60 +{:copy-code}
  61 +```
  62 +
  63 +* Delete device after confirmation:
  64 +
  65 +```javascript
  66 +var $injector = widgetContext.$scope.$injector;
  67 +var dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));
  68 +var deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
  69 +
  70 +openDeleteDeviceDialog();
  71 +
  72 +function openDeleteDeviceDialog() {
  73 + var title = 'Are you sure you want to delete the device ' + entityName + '?';
  74 + var content = 'Be careful, after the confirmation, the device and all related data will become unrecoverable!';
  75 + dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(
  76 + function(result) {
  77 + if (result) {
  78 + deleteDevice();
  79 + }
  80 + }
  81 + );
  82 +}
  83 +
  84 +function deleteDevice() {
  85 + deviceService.deleteDevice(entityId.id).subscribe(
  86 + function() {
  87 + widgetContext.updateAliases();
  88 + }
  89 + );
  90 +}
  91 +{:copy-code}
  92 +```
  1 + <li><b>additionalParams:</b> <code>{[key: string]: any}</code> - An optional key/value object holding additional entity parameters depending on widget type and action source:
  2 + <ul>
  3 + <li>Entities table widget (<i>On row click</i> or <i>Action cell button</i>) - <b>additionalParams:</b> <code>{ entity: <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts#L61" target="_blank">EntityData</a> }</code>:
  4 + <ul>
  5 + <li><b>entity:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts#L61" target="_blank">EntityData</a></code> - An
  6 + <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts#L61" target="_blank">EntityData</a> object
  7 + presenting basic entity properties (ex. <code>id</code>, <code>entityName</code>) and <br> provides access to other entity attributes/timeseries declared in widget datasource configuration.
  8 + </li>
  9 + </ul>
  10 + </li>
  11 + <li>Alarms table widget (<i>On row click</i> or <i>Action cell button</i>) - <b>additionalParams:</b> <code>{ alarm: <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/shared/models/alarm.models.ts#L108" target="_blank">AlarmDataInfo</a> }</code>:
  12 + <ul>
  13 + <li><b>alarm:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/shared/models/alarm.models.ts#L108" target="_blank">AlarmDataInfo</a></code> - An
  14 + <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/shared/models/alarm.models.ts#L108" target="_blank">AlarmDataInfo</a> object
  15 + presenting basic alarm properties (ex. <code>type</code>, <code>severity</code>, <code>originator</code>, etc.) and <br> provides access to other alarm or originator entity fields/attributes/timeseries declared in widget datasource configuration.
  16 + </li>
  17 + </ul>
  18 + </li>
  19 + <li>Timeseries table widget (<i>On row click</i> or <i>Action cell button</i>) - <b>additionalParams:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts#L80" target="_blank">TimeseriesRow</a></code>:
  20 + <ul>
  21 + <li><b>additionalParams:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts#L80" target="_blank">TimeseriesRow</a></code> - A
  22 + <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts#L80" target="_blank">TimeseriesRow</a> object
  23 + presenting <code>formattedTs</code> (a string value of formatted timestamp) and <br> timeseries values for each column declared in widget datasource configuration.
  24 + </li>
  25 + </ul>
  26 + </li>
  27 + <li>Entities hierarchy widget (<i>On node selected</i>) - <b>additionalParams:</b> <code>{ nodeCtx: <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts#L35" target="_blank">HierarchyNodeContext</a> }</code>:
  28 + <ul>
  29 + <li><b>nodeCtx:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts#L35" target="_blank">HierarchyNodeContext</a></code> - An
  30 + <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts#L35" target="_blank">HierarchyNodeContext</a> object
  31 + containing <code>entity</code> field holding basic entity properties <br> (ex. <code>id</code>, <code>name</code>, <code>label</code>) and <code>data</code> field holding other entity attributes/timeseries declared in widget datasource configuration.
  32 + </li>
  33 + </ul>
  34 + </li>
  35 + <li>Pie - Flot widget (<i>On slice click</i>) - <b>additionalParams:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts#L62" target="_blank">TbFlotPlotItem</a></code>:
  36 + <ul>
  37 + <li><b>additionalParams:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts#L62" target="_blank">TbFlotPlotItem</a></code> - A
  38 + <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts#L62" target="_blank">TbFlotPlotItem</a> object
  39 + containing <code>series</code> field with information about datasource and <br> data key of clicked pie slice.
  40 + </li>
  41 + </ul>
  42 + </li>
  43 + <li><i>All other widgets</i> - does not provide <b>additionalParams</b> value.
  44 + </li>
  45 + </ul>
  46 + </li>
  1 +#### Custom action (with HTML template) function
  2 +
  3 +<div class="divider"></div>
  4 +<br/>
  5 +
  6 +*function ($event, widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel): void*
  7 +
  8 +A JavaScript function performing custom action with defined HTML template to render dialog.
  9 +
  10 +**Parameters:**
  11 +
  12 +<ul>
  13 + <li><b>$event:</b> <code><a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a></code> - The <a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a> object. Usually a result of a mouse click event.
  14 + </li>
  15 + <li><b>widgetContext:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a></code> - A reference to <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a> that has all necessary API
  16 + and data used by widget instance.
  17 + </li>
  18 + <li><b>entityId:</b> <code>string</code> - An optional string id of the target entity.
  19 + </li>
  20 + <li><b>entityName:</b> <code>string</code> - An optional string name of the target entity.
  21 + </li>
  22 + <li><b>htmlTemplate:</b> <code>string</code> - An optional HTML template string defined in <b>HTML</b> tab.<br/> Used to render custom dialog (see <b>Examples</b> for more details).
  23 + </li>
  24 + {% include widget/action/custom_additional_params %}
  25 + <li><b>entityLabel:</b> <code>string</code> - An optional string label of the target entity.
  26 + </li>
  27 +</ul>
  28 +
  29 +<div class="divider"></div>
  30 +
  31 +##### Examples
  32 +
  33 +###### Display dialog to create a device or an asset
  34 +
  35 +<br>
  36 +
  37 +<div style="padding-left: 64px;"
  38 + tb-help-popup="widget/action/examples/custom_pretty_create_dialog_js"
  39 + tb-help-popup-placement="top"
  40 + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
  41 + trigger-text="JavaScript function">
  42 +</div>
  43 +
  44 +<br>
  45 +
  46 +<div style="padding-left: 64px;"
  47 + tb-help-popup="widget/action/examples/custom_pretty_create_dialog_html"
  48 + tb-help-popup-placement="top"
  49 + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
  50 + trigger-text="HTML code">
  51 +</div>
  52 +
  53 +###### Display dialog to edit a device or an asset
  54 +
  55 +<br>
  56 +
  57 +<div style="padding-left: 64px;"
  58 + tb-help-popup="widget/action/examples/custom_pretty_edit_dialog_js"
  59 + tb-help-popup-placement="top"
  60 + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
  61 + trigger-text="JavaScript function">
  62 +</div>
  63 +
  64 +<br>
  65 +
  66 +<div style="padding-left: 64px;"
  67 + tb-help-popup="widget/action/examples/custom_pretty_edit_dialog_html"
  68 + tb-help-popup-placement="top"
  69 + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
  70 + trigger-text="HTML code">
  71 +</div>
  72 +
  73 +<br>
  74 +<br>
  1 +#### HTML template of dialog to create a device or an asset
  2 +
  3 +```html
  4 +<form #addEntityForm="ngForm" [formGroup]="addEntityFormGroup"
  5 + (ngSubmit)="save()" class="add-entity-form">
  6 + <mat-toolbar fxLayout="row" color="primary">
  7 + <h2>Add entity</h2>
  8 + <span fxFlex></span>
  9 + <button mat-icon-button (click)="cancel()" type="button">
  10 + <mat-icon class="material-icons">close</mat-icon>
  11 + </button>
  12 + </mat-toolbar>
  13 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  14 + </mat-progress-bar>
  15 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  16 + <div mat-dialog-content fxLayout="column">
  17 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  18 + <mat-form-field fxFlex class="mat-block">
  19 + <mat-label>Entity Name</mat-label>
  20 + <input matInput formControlName="entityName" required>
  21 + <mat-error *ngIf="addEntityFormGroup.get('entityName').hasError('required')">
  22 + Entity name is required.
  23 + </mat-error>
  24 + </mat-form-field>
  25 + <mat-form-field fxFlex class="mat-block">
  26 + <mat-label>Entity Label</mat-label>
  27 + <input matInput formControlName="entityLabel" >
  28 + </mat-form-field>
  29 + </div>
  30 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  31 + <tb-entity-type-select
  32 + class="mat-block"
  33 + formControlName="entityType"
  34 + [showLabel]="true"
  35 + [allowedEntityTypes]="allowedEntityTypes"
  36 + ></tb-entity-type-select>
  37 + <tb-entity-subtype-autocomplete
  38 + fxFlex *ngIf="addEntityFormGroup.get('entityType').value == 'ASSET'"
  39 + class="mat-block"
  40 + formControlName="type"
  41 + [required]="true"
  42 + [entityType]="'ASSET'"
  43 + ></tb-entity-subtype-autocomplete>
  44 + <tb-entity-subtype-autocomplete
  45 + fxFlex *ngIf="addEntityFormGroup.get('entityType').value != 'ASSET'"
  46 + class="mat-block"
  47 + formControlName="type"
  48 + [required]="true"
  49 + [entityType]="'DEVICE'"
  50 + ></tb-entity-subtype-autocomplete>
  51 + </div>
  52 + <div formGroupName="attributes" fxLayout="column">
  53 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  54 + <mat-form-field fxFlex class="mat-block">
  55 + <mat-label>Latitude</mat-label>
  56 + <input type="number" step="any" matInput formControlName="latitude">
  57 + </mat-form-field>
  58 + <mat-form-field fxFlex class="mat-block">
  59 + <mat-label>Longitude</mat-label>
  60 + <input type="number" step="any" matInput formControlName="longitude">
  61 + </mat-form-field>
  62 + </div>
  63 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  64 + <mat-form-field fxFlex class="mat-block">
  65 + <mat-label>Address</mat-label>
  66 + <input matInput formControlName="address">
  67 + </mat-form-field>
  68 + <mat-form-field fxFlex class="mat-block">
  69 + <mat-label>Owner</mat-label>
  70 + <input matInput formControlName="owner">
  71 + </mat-form-field>
  72 + </div>
  73 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  74 + <mat-form-field fxFlex class="mat-block">
  75 + <mat-label>Integer Value</mat-label>
  76 + <input type="number" step="1" matInput formControlName="number">
  77 + <mat-error *ngIf="addEntityFormGroup.get('attributes.number').hasError('pattern')">
  78 + Invalid integer value.
  79 + </mat-error>
  80 + </mat-form-field>
  81 + <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>
  82 + <label class="checkbox-label">Boolean Value</label>
  83 + <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">
  84 +
  85 + </mat-checkbox>
  86 + </div>
  87 + </div>
  88 + </div>
  89 + <div class="relations-list">
  90 + <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>
  91 + <div class="body" [fxShow]="relations().length">
  92 + <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="relations" *ngFor="let relation of relations().controls; let i = index;">
  93 + <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;">
  94 + <div fxFlex fxLayout="column">
  95 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  96 + <mat-form-field class="mat-block" style="min-width: 100px;">
  97 + <mat-label>Direction</mat-label>
  98 + <mat-select formControlName="direction" name="direction">
  99 + <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
  100 +
  101 + </mat-option>
  102 + </mat-select>
  103 + <mat-error *ngIf="relation.get('direction').hasError('required')">
  104 + Relation direction is required.
  105 + </mat-error>
  106 + </mat-form-field>
  107 + <tb-relation-type-autocomplete
  108 + fxFlex class="mat-block"
  109 + formControlName="relationType"
  110 + [required]="true">
  111 + </tb-relation-type-autocomplete>
  112 + </div>
  113 + <div fxLayout="row" fxLayout.xs="column">
  114 + <tb-entity-select
  115 + fxFlex class="mat-block"
  116 + [required]="true"
  117 + formControlName="relatedEntity">
  118 + </tb-entity-select>
  119 + </div>
  120 + </div>
  121 + <div fxLayout="column" fxLayoutAlign="center center">
  122 + <button mat-icon-button color="primary"
  123 + aria-label="Remove"
  124 + type="button"
  125 + (click)="removeRelation(i)"
  126 + matTooltip="Remove relation"
  127 + matTooltipPosition="above">
  128 + <mat-icon>close</mat-icon>
  129 + </button>
  130 + </div>
  131 + </div>
  132 + </div>
  133 + </div>
  134 + <div>
  135 + <button mat-raised-button color="primary"
  136 + type="button"
  137 + (click)="addRelation()"
  138 + matTooltip="Add Relation"
  139 + matTooltipPosition="above">
  140 + Add
  141 + </button>
  142 + </div>
  143 + </div>
  144 + </div>
  145 + <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
  146 + <button mat-button color="primary"
  147 + type="button"
  148 + [disabled]="(isLoading$ | async)"
  149 + (click)="cancel()" cdkFocusInitial>
  150 + Cancel
  151 + </button>
  152 + <button mat-button mat-raised-button color="primary"
  153 + type="submit"
  154 + [disabled]="(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty">
  155 + Create
  156 + </button>
  157 + </div>
  158 +</form>
  159 +{:copy-code}
  160 +```
  1 +#### Function displaying dialog to create a device or an asset
  2 +
  3 +```javascript
  4 +let $injector = widgetContext.$scope.$injector;
  5 +let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));
  6 +let assetService = $injector.get(widgetContext.servicesMap.get('assetService'));
  7 +let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
  8 +let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));
  9 +let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));
  10 +
  11 +openAddEntityDialog();
  12 +
  13 +function openAddEntityDialog() {
  14 + customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();
  15 +}
  16 +
  17 +function AddEntityDialogController(instance) {
  18 + let vm = instance;
  19 +
  20 + vm.allowedEntityTypes = ['ASSET', 'DEVICE'];
  21 + vm.entitySearchDirection = {
  22 + from: "FROM",
  23 + to: "TO"
  24 + }
  25 +
  26 + vm.addEntityFormGroup = vm.fb.group({
  27 + entityName: ['', [vm.validators.required]],
  28 + entityType: ['DEVICE'],
  29 + entityLabel: [null],
  30 + type: ['', [vm.validators.required]],
  31 + attributes: vm.fb.group({
  32 + latitude: [null],
  33 + longitude: [null],
  34 + address: [null],
  35 + owner: [null],
  36 + number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]],
  37 + booleanValue: [null]
  38 + }),
  39 + relations: vm.fb.array([])
  40 + });
  41 +
  42 + vm.cancel = function () {
  43 + vm.dialogRef.close(null);
  44 + };
  45 +
  46 + vm.relations = function () {
  47 + return vm.addEntityFormGroup.get('relations');
  48 + };
  49 +
  50 + vm.addRelation = function () {
  51 + vm.relations().push(vm.fb.group({
  52 + relatedEntity: [null, [vm.validators.required]],
  53 + relationType: [null, [vm.validators.required]],
  54 + direction: [null, [vm.validators.required]]
  55 + }));
  56 + };
  57 +
  58 + vm.removeRelation = function (index) {
  59 + vm.relations().removeAt(index);
  60 + vm.relations().markAsDirty();
  61 + };
  62 +
  63 + vm.save = function () {
  64 + vm.addEntityFormGroup.markAsPristine();
  65 + saveEntityObservable().subscribe(
  66 + function (entity) {
  67 + widgetContext.rxjs.forkJoin([
  68 + saveAttributes(entity.id),
  69 + saveRelations(entity.id)
  70 + ]).subscribe(
  71 + function () {
  72 + widgetContext.updateAliases();
  73 + vm.dialogRef.close(null);
  74 + }
  75 + );
  76 + }
  77 + );
  78 + };
  79 +
  80 + function saveEntityObservable() {
  81 + const formValues = vm.addEntityFormGroup.value;
  82 + let entity = {
  83 + name: formValues.entityName,
  84 + type: formValues.type,
  85 + label: formValues.entityLabel
  86 + };
  87 + if (formValues.entityType == 'ASSET') {
  88 + return assetService.saveAsset(entity);
  89 + } else if (formValues.entityType == 'DEVICE') {
  90 + return deviceService.saveDevice(entity);
  91 + }
  92 + }
  93 +
  94 + function saveAttributes(entityId) {
  95 + let attributes = vm.addEntityFormGroup.get('attributes').value;
  96 + let attributesArray = [];
  97 + for (let key in attributes) {
  98 + if (attributes[key] !== null) {
  99 + attributesArray.push({key: key, value: attributes[key]});
  100 + }
  101 + }
  102 + if (attributesArray.length > 0) {
  103 + return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray);
  104 + }
  105 + return widgetContext.rxjs.of([]);
  106 + }
  107 +
  108 + function saveRelations(entityId) {
  109 + let relations = vm.addEntityFormGroup.get('relations').value;
  110 + let tasks = [];
  111 + for (let i = 0; i < relations.length; i++) {
  112 + let relation = {
  113 + type: relations[i].relationType,
  114 + typeGroup: 'COMMON'
  115 + };
  116 + if (relations[i].direction == 'FROM') {
  117 + relation.to = relations[i].relatedEntity;
  118 + relation.from = entityId;
  119 + } else {
  120 + relation.to = entityId;
  121 + relation.from = relations[i].relatedEntity;
  122 + }
  123 + tasks.push(entityRelationService.saveRelation(relation));
  124 + }
  125 + if (tasks.length > 0) {
  126 + return widgetContext.rxjs.forkJoin(tasks);
  127 + }
  128 + return widgetContext.rxjs.of([]);
  129 + }
  130 +}
  131 +{:copy-code}
  132 +```
  1 +#### HTML template of dialog to edit a device or an asset
  2 +
  3 +```html
  4 +<form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup"
  5 + (ngSubmit)="save()" class="edit-entity-form">
  6 + <mat-toolbar fxLayout="row" color="primary">
  7 + <h2>Edit </h2>
  8 + <span fxFlex></span>
  9 + <button mat-icon-button (click)="cancel()" type="button">
  10 + <mat-icon class="material-icons">close</mat-icon>
  11 + </button>
  12 + </mat-toolbar>
  13 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  14 + </mat-progress-bar>
  15 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  16 + <div mat-dialog-content fxLayout="column">
  17 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  18 + <mat-form-field fxFlex class="mat-block">
  19 + <mat-label>Entity Name</mat-label>
  20 + <input matInput formControlName="entityName" required readonly="">
  21 + </mat-form-field>
  22 + <mat-form-field fxFlex class="mat-block">
  23 + <mat-label>Entity Label</mat-label>
  24 + <input matInput formControlName="entityLabel">
  25 + </mat-form-field>
  26 + </div>
  27 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  28 + <mat-form-field fxFlex class="mat-block">
  29 + <mat-label>Entity Type</mat-label>
  30 + <input matInput formControlName="entityType" readonly>
  31 + </mat-form-field>
  32 + <mat-form-field fxFlex class="mat-block">
  33 + <mat-label>Type</mat-label>
  34 + <input matInput formControlName="type" readonly>
  35 + </mat-form-field>
  36 + </div>
  37 + <div formGroupName="attributes" fxLayout="column">
  38 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  39 + <mat-form-field fxFlex class="mat-block">
  40 + <mat-label>Latitude</mat-label>
  41 + <input type="number" step="any" matInput formControlName="latitude">
  42 + </mat-form-field>
  43 + <mat-form-field fxFlex class="mat-block">
  44 + <mat-label>Longitude</mat-label>
  45 + <input type="number" step="any" matInput formControlName="longitude">
  46 + </mat-form-field>
  47 + </div>
  48 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  49 + <mat-form-field fxFlex class="mat-block">
  50 + <mat-label>Address</mat-label>
  51 + <input matInput formControlName="address">
  52 + </mat-form-field>
  53 + <mat-form-field fxFlex class="mat-block">
  54 + <mat-label>Owner</mat-label>
  55 + <input matInput formControlName="owner">
  56 + </mat-form-field>
  57 + </div>
  58 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  59 + <mat-form-field fxFlex class="mat-block">
  60 + <mat-label>Integer Value</mat-label>
  61 + <input type="number" step="1" matInput formControlName="number">
  62 + <mat-error *ngIf="editEntityFormGroup.get('attributes.number').hasError('pattern')">
  63 + Invalid integer value.
  64 + </mat-error>
  65 + </mat-form-field>
  66 + <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>
  67 + <label class="checkbox-label">Boolean Value</label>
  68 + <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">
  69 +
  70 + </mat-checkbox>
  71 + </div>
  72 + </div>
  73 + </div>
  74 + <div class="relations-list old-relations">
  75 + <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>
  76 + <div class="body" [fxShow]="oldRelations().length">
  77 + <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="oldRelations"
  78 + *ngFor="let relation of oldRelations().controls; let i = index;">
  79 + <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;">
  80 + <div fxFlex fxLayout="column">
  81 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  82 + <mat-form-field class="mat-block" style="min-width: 100px;">
  83 + <mat-label>Direction</mat-label>
  84 + <mat-select formControlName="direction" name="direction">
  85 + <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
  86 +
  87 + </mat-option>
  88 + </mat-select>
  89 + <mat-error *ngIf="relation.get('direction').hasError('required')">
  90 + Relation direction is required.
  91 + </mat-error>
  92 + </mat-form-field>
  93 + <tb-relation-type-autocomplete
  94 + fxFlex class="mat-block"
  95 + formControlName="relationType"
  96 + required="true">
  97 + </tb-relation-type-autocomplete>
  98 + </div>
  99 + <div fxLayout="row" fxLayout.xs="column">
  100 + <tb-entity-select
  101 + fxFlex class="mat-block"
  102 + required="true"
  103 + formControlName="relatedEntity">
  104 + </tb-entity-select>
  105 + </div>
  106 + </div>
  107 + <div fxLayout="column" fxLayoutAlign="center center">
  108 + <button mat-icon-button color="primary"
  109 + aria-label="Remove"
  110 + type="button"
  111 + (click)="removeOldRelation(i)"
  112 + matTooltip="Remove relation"
  113 + matTooltipPosition="above">
  114 + <mat-icon>close</mat-icon>
  115 + </button>
  116 + </div>
  117 + </div>
  118 + </div>
  119 + </div>
  120 + </div>
  121 + <div class="relations-list">
  122 + <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">New Relations</div>
  123 + <div class="body" [fxShow]="relations().length">
  124 + <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="relations" *ngFor="let relation of relations().controls; let i = index;">
  125 + <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;">
  126 + <div fxFlex fxLayout="column">
  127 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  128 + <mat-form-field class="mat-block" style="min-width: 100px;">
  129 + <mat-label>Direction</mat-label>
  130 + <mat-select formControlName="direction" name="direction">
  131 + <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
  132 +
  133 + </mat-option>
  134 + </mat-select>
  135 + <mat-error *ngIf="relation.get('direction').hasError('required')">
  136 + Relation direction is required.
  137 + </mat-error>
  138 + </mat-form-field>
  139 + <tb-relation-type-autocomplete
  140 + fxFlex class="mat-block"
  141 + formControlName="relationType"
  142 + [required]="true">
  143 + </tb-relation-type-autocomplete>
  144 + </div>
  145 + <div fxLayout="row" fxLayout.xs="column">
  146 + <tb-entity-select
  147 + fxFlex class="mat-block"
  148 + [required]="true"
  149 + formControlName="relatedEntity">
  150 + </tb-entity-select>
  151 + </div>
  152 + </div>
  153 + <div fxLayout="column" fxLayoutAlign="center center">
  154 + <button mat-icon-button color="primary"
  155 + aria-label="Remove"
  156 + type="button"
  157 + (click)="removeRelation(i)"
  158 + matTooltip="Remove relation"
  159 + matTooltipPosition="above">
  160 + <mat-icon>close</mat-icon>
  161 + </button>
  162 + </div>
  163 + </div>
  164 + </div>
  165 + </div>
  166 + <div>
  167 + <button mat-raised-button color="primary"
  168 + type="button"
  169 + (click)="addRelation()"
  170 + matTooltip="Add Relation"
  171 + matTooltipPosition="above">
  172 + Add
  173 + </button>
  174 + </div>
  175 + </div>
  176 + </div>
  177 + <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
  178 + <button mat-button color="primary"
  179 + type="button"
  180 + [disabled]="(isLoading$ | async)"
  181 + (click)="cancel()" cdkFocusInitial>
  182 + Cancel
  183 + </button>
  184 + <button mat-button mat-raised-button color="primary"
  185 + type="submit"
  186 + [disabled]="(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty">
  187 + Save
  188 + </button>
  189 + </div>
  190 +</form>
  191 +{:copy-code}
  192 +```
  1 +#### Function displaying dialog to edit a device or an asset
  2 +
  3 +```javascript
  4 +let $injector = widgetContext.$scope.$injector;
  5 +let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));
  6 +let entityService = $injector.get(widgetContext.servicesMap.get('entityService'));
  7 +let assetService = $injector.get(widgetContext.servicesMap.get('assetService'));
  8 +let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
  9 +let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));
  10 +let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));
  11 +
  12 +openEditEntityDialog();
  13 +
  14 +function openEditEntityDialog() {
  15 + customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();
  16 +}
  17 +
  18 +function EditEntityDialogController(instance) {
  19 + let vm = instance;
  20 +
  21 + vm.entityName = entityName;
  22 + vm.entityType = entityId.entityType;
  23 + vm.entitySearchDirection = {
  24 + from: "FROM",
  25 + to: "TO"
  26 + };
  27 + vm.attributes = {};
  28 + vm.oldRelationsData = [];
  29 + vm.relationsToDelete = [];
  30 + vm.entity = {};
  31 +
  32 + vm.editEntityFormGroup = vm.fb.group({
  33 + entityName: ['', [vm.validators.required]],
  34 + entityType: [null],
  35 + entityLabel: [null],
  36 + type: ['', [vm.validators.required]],
  37 + attributes: vm.fb.group({
  38 + latitude: [null],
  39 + longitude: [null],
  40 + address: [null],
  41 + owner: [null],
  42 + number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]],
  43 + booleanValue: [false]
  44 + }),
  45 + oldRelations: vm.fb.array([]),
  46 + relations: vm.fb.array([])
  47 + });
  48 +
  49 + getEntityInfo();
  50 +
  51 + vm.cancel = function() {
  52 + vm.dialogRef.close(null);
  53 + };
  54 +
  55 + vm.relations = function() {
  56 + return vm.editEntityFormGroup.get('relations');
  57 + };
  58 +
  59 + vm.oldRelations = function() {
  60 + return vm.editEntityFormGroup.get('oldRelations');
  61 + };
  62 +
  63 + vm.addRelation = function() {
  64 + vm.relations().push(vm.fb.group({
  65 + relatedEntity: [null, [vm.validators.required]],
  66 + relationType: [null, [vm.validators.required]],
  67 + direction: [null, [vm.validators.required]]
  68 + }));
  69 + };
  70 +
  71 + function addOldRelation() {
  72 + vm.oldRelations().push(vm.fb.group({
  73 + relatedEntity: [{value: null, disabled: true}, [vm.validators.required]],
  74 + relationType: [{value: null, disabled: true}, [vm.validators.required]],
  75 + direction: [{value: null, disabled: true}, [vm.validators.required]]
  76 + }));
  77 + }
  78 +
  79 + vm.removeRelation = function(index) {
  80 + vm.relations().removeAt(index);
  81 + vm.relations().markAsDirty();
  82 + };
  83 +
  84 + vm.removeOldRelation = function(index) {
  85 + vm.oldRelations().removeAt(index);
  86 + vm.relationsToDelete.push(vm.oldRelationsData[index]);
  87 + vm.oldRelations().markAsDirty();
  88 + };
  89 +
  90 + vm.save = function() {
  91 + vm.editEntityFormGroup.markAsPristine();
  92 + widgetContext.rxjs.forkJoin([
  93 + saveAttributes(entityId),
  94 + saveRelations(entityId),
  95 + saveEntity()
  96 + ]).subscribe(
  97 + function () {
  98 + widgetContext.updateAliases();
  99 + vm.dialogRef.close(null);
  100 + }
  101 + );
  102 + };
  103 +
  104 + function getEntityAttributes(attributes) {
  105 + for (var i = 0; i < attributes.length; i++) {
  106 + vm.attributes[attributes[i].key] = attributes[i].value;
  107 + }
  108 + }
  109 +
  110 + function getEntityRelations(relations) {
  111 + let relationsFrom = relations[0];
  112 + let relationsTo = relations[1];
  113 + for (let i=0; i < relationsFrom.length; i++) {
  114 + let relation = {
  115 + direction: 'FROM',
  116 + relationType: relationsFrom[i].type,
  117 + relatedEntity: relationsFrom[i].to
  118 + };
  119 + vm.oldRelationsData.push(relation);
  120 + addOldRelation();
  121 + }
  122 + for (let i=0; i < relationsTo.length; i++) {
  123 + let relation = {
  124 + direction: 'TO',
  125 + relationType: relationsTo[i].type,
  126 + relatedEntity: relationsTo[i].from
  127 + };
  128 + vm.oldRelationsData.push(relation);
  129 + addOldRelation();
  130 + }
  131 + }
  132 +
  133 + function getEntityInfo() {
  134 + widgetContext.rxjs.forkJoin([
  135 + entityRelationService.findInfoByFrom(entityId),
  136 + entityRelationService.findInfoByTo(entityId),
  137 + attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE'),
  138 + entityService.getEntity(entityId.entityType, entityId.id)
  139 + ]).subscribe(
  140 + function (data) {
  141 + getEntityRelations(data.slice(0,2));
  142 + getEntityAttributes(data[2]);
  143 + vm.entity = data[3];
  144 + vm.editEntityFormGroup.patchValue({
  145 + entityName: vm.entity.name,
  146 + entityType: vm.entityType,
  147 + entityLabel: vm.entity.label,
  148 + type: vm.entity.type,
  149 + attributes: vm.attributes,
  150 + oldRelations: vm.oldRelationsData
  151 + }, {emitEvent: false});
  152 + }
  153 + );
  154 + }
  155 +
  156 + function saveEntity() {
  157 + const formValues = vm.editEntityFormGroup.value;
  158 + if (vm.entity.label !== formValues.entityLabel){
  159 + vm.entity.label = formValues.entityLabel;
  160 + if (formValues.entityType == 'ASSET') {
  161 + return assetService.saveAsset(vm.entity);
  162 + } else if (formValues.entityType == 'DEVICE') {
  163 + return deviceService.saveDevice(vm.entity);
  164 + }
  165 + }
  166 + return widgetContext.rxjs.of([]);
  167 + }
  168 +
  169 + function saveAttributes(entityId) {
  170 + let attributes = vm.editEntityFormGroup.get('attributes').value;
  171 + let attributesArray = [];
  172 + for (let key in attributes) {
  173 + if (attributes[key] !== vm.attributes[key]) {
  174 + attributesArray.push({key: key, value: attributes[key]});
  175 + }
  176 + }
  177 + if (attributesArray.length > 0) {
  178 + return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray);
  179 + }
  180 + return widgetContext.rxjs.of([]);
  181 + }
  182 +
  183 + function saveRelations(entityId) {
  184 + let relations = vm.editEntityFormGroup.get('relations').value;
  185 + let tasks = [];
  186 + for(let i=0; i < relations.length; i++) {
  187 + let relation = {
  188 + type: relations[i].relationType,
  189 + typeGroup: 'COMMON'
  190 + };
  191 + if (relations[i].direction == 'FROM') {
  192 + relation.to = relations[i].relatedEntity;
  193 + relation.from = entityId;
  194 + } else {
  195 + relation.to = entityId;
  196 + relation.from = relations[i].relatedEntity;
  197 + }
  198 + tasks.push(entityRelationService.saveRelation(relation));
  199 + }
  200 + for (let i=0; i < vm.relationsToDelete.length; i++) {
  201 + let relation = {
  202 + type: vm.relationsToDelete[i].relationType
  203 + };
  204 + if (vm.relationsToDelete[i].direction == 'FROM') {
  205 + relation.to = vm.relationsToDelete[i].relatedEntity;
  206 + relation.from = entityId;
  207 + } else {
  208 + relation.to = entityId;
  209 + relation.from = vm.relationsToDelete[i].relatedEntity;
  210 + }
  211 + tasks.push(entityRelationService.deleteRelation(relation.from, relation.type, relation.to));
  212 + }
  213 + if (tasks.length > 0) {
  214 + return widgetContext.rxjs.forkJoin(tasks);
  215 + }
  216 + return widgetContext.rxjs.of([]);
  217 + }
  218 +}
  219 +{:copy-code}
  220 +```
1 -#### Show cell button action JavaScript Function 1 +#### Show cell button action function
  2 +
  3 +<div class="divider"></div>
  4 +<br/>
  5 +
  6 +*function (widgetContext, data): boolean*
  7 +
  8 +A JavaScript function evaluating whether to display particular table cell action.
  9 +
  10 +**Parameters:**
  11 +
  12 +<ul>
  13 + <li><b>widgetContext:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a></code> - A reference to <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a> that has all necessary API
  14 + and data used by widget instance.
  15 + </li>
  16 + <li><b>data:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a></code> - A <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a> object of specific table row.<br/>
  17 + Represents basic entity properties (ex. <code>entityId</code>, <code>entityName</code>)<br/>and provides access to other entity attributes/timeseries declared in widget datasource configuration.
  18 + </li>
  19 +</ul>
  20 +
  21 +**Returns:**
  22 +
  23 +`true` if cell action should be displayed, `false` otherwise.
  24 +
  25 +<div class="divider"></div>
  26 +
  27 +##### Examples
  28 +
  29 +* Display action only for customer users:
  30 +
  31 +```javascript
  32 +return widgetContext.currentUser.authority === 'CUSTOMER_USER';
  33 +{:copy-code}
  34 +```
  35 +
  36 +* Display action only if the entity in the row is device and has type `thermostat`:
  37 +
  38 +```javascript
  39 +return data && data.entityType === 'DEVICE' && data.Type === 'thermostat';
  40 +{:copy-code}
  41 +```
  42 +
  43 +* Display action only if the entity in the row has `temperature` latest timeseries or attribute value greater than 25:
2 44
3 ```javascript 45 ```javascript
4 -return data.entityName === 'Test device'; {:copy-code} 46 +return data && data.temperature > 25;
  47 +{:copy-code}
5 ``` 48 ```
@@ -5,15 +5,15 @@ @@ -5,15 +5,15 @@
5 5
6 *function (widgetContext, data): boolean* 6 *function (widgetContext, data): boolean*
7 7
8 -JavaScript function evaluating whether to display particular widget header action. 8 +A JavaScript function evaluating whether to display particular widget header action.
9 9
10 **Parameters:** 10 **Parameters:**
11 11
12 <ul> 12 <ul>
13 - <li><b>widgetContext</b> - A reference to <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a> that has all necessary API<br/> 13 + <li><b>widgetContext:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a></code> - A reference to <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a> that has all necessary API
14 and data used by widget instance. 14 and data used by widget instance.
15 </li> 15 </li>
16 - <li><b>data</b> - An array of <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a> objects.<br/> 16 + <li><b>data:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData[]</a></code> - An array of <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a> objects.<br/>
17 Each object represents basic entity properties (ex. <code>entityId</code>, <code>entityName</code>)<br/>and provides access to other entity attributes/timeseries declared in widget datasource configuration. 17 Each object represents basic entity properties (ex. <code>entityId</code>, <code>entityName</code>)<br/>and provides access to other entity attributes/timeseries declared in widget datasource configuration.
18 </li> 18 </li>
19 </ul> 19 </ul>
  1 +#### Data generation function
  2 +
  3 +<div class="divider"></div>
  4 +<br/>
  5 +
  6 +*function (time, prevValue): any*
  7 +
  8 +A JavaScript function generating datapoint values.
  9 +
  10 +**Parameters:**
  11 +
  12 +<ul>
  13 + <li><b>time:</b> <code>number</code> - timestamp in milliseconds of the current datapoint.
  14 + </li>
  15 + <li><b>prevValue:</b> <code>primitive (number/string/boolean)</code> - A previous datapoint value.
  16 + </li>
  17 +</ul>
  18 +
  19 +**Returns:**
  20 +
  21 +A primitive type (number, string or boolean) presenting newly generated datapoint value.
  22 +
  23 +<div class="divider"></div>
  24 +
  25 +##### Examples
  26 +
  27 +* Generate data with sine function:
  28 +
  29 +```javascript
  30 +return Math.sin(time/5000);
  31 +{:copy-code}
  32 +```
  33 +
  34 +* Generate true/false sequence:
  35 +
  36 +```javascript
  37 +if (!prevValue) {
  38 + return true;
  39 +} else {
  40 + return false;
  41 +}
  42 +{:copy-code}
  43 +```
  44 +
  45 +* Generate repeating sequence of predefined values (for ex. latitude):
  46 +
  47 +```javascript
  48 +var lats = [37.7696499,
  49 + 37.7699074,
  50 + 37.7699536,
  51 + 37.7697242,
  52 + 37.7695189,
  53 + 37.7696889];
  54 +
  55 +var index = Math.floor((time/3 % 14000) / 1000);
  56 +
  57 +return lats[index];
  58 +{:copy-code}
  59 +```
  1 +#### Data post-processing function
  2 +
  3 +<div class="divider"></div>
  4 +<br/>
  5 +
  6 +*function (time, value, prevValue, timePrev, prevOrigValue): any*
  7 +
  8 +A JavaScript function doing post-processing on telemetry data.
  9 +
  10 +**Parameters:**
  11 +
  12 +<ul>
  13 + <li><b>time:</b> <code>number</code> - timestamp in milliseconds of the current datapoint.
  14 + </li>
  15 + <li><b>value:</b> <code>primitive (number/string/boolean)</code> - A value of the current datapoint.
  16 + </li>
  17 + <li><b>prevValue:</b> <code>primitive (number/string/boolean)</code> - A value of the previous datapoint after applied post-processing.
  18 + </li>
  19 + <li><b>timePrev:</b> <code>number</code> - timestamp in milliseconds of the previous datapoint value.
  20 + </li>
  21 + <li><b>prevOrigValue:</b> <code>primitive (number/string/boolean)</code> - An original value of the previous datapoint.
  22 + </li>
  23 +</ul>
  24 +
  25 +**Returns:**
  26 +
  27 +A primitive type (number, string or boolean) presenting the new datapoint value.
  28 +
  29 +<div class="divider"></div>
  30 +
  31 +##### Examples
  32 +
  33 +* Multiply all datapoint values by 10:
  34 +
  35 +```javascript
  36 +return value * 10;
  37 +{:copy-code}
  38 +```
  39 +
  40 +* Round all datapoint values to whole numbers:
  41 +
  42 +```javascript
  43 +return Math.round(value);
  44 +{:copy-code}
  45 +```
  46 +
  47 +* Get relative difference between data points:
  48 +
  49 +```javascript
  50 +if (prevOrigValue) {
  51 + return (value - prevOrigValue) / prevOrigValue;
  52 +} else {
  53 + return 0;
  54 +}
  55 +{:copy-code}
  56 +```
1 -#### Tooltip value format JavaScript Function 1 +#### Tooltip value format function
2 2
3 -##### Input arguments 3 +<div class="divider"></div>
  4 +<br/>
4 5
5 -- value - value to format 6 +*function (value): string*
  7 +
  8 +A JavaScript function used to format datapoint value to be shown on the chart tooltip.
  9 +
  10 +**Parameters:**
  11 +
  12 +<ul>
  13 + <li><b>value:</b> <code>primitive (number/string/boolean)</code> - A value of the datapoint that should be formatted.
  14 + </li>
  15 +</ul>
  16 +
  17 +**Returns:**
  18 +
  19 +A string representing the formatted value.
  20 +
  21 +<div class="divider"></div>
  22 +
  23 +##### Examples
  24 +
  25 +* Present the datapoint value in tooltip in Celsius (°C) units:
6 26
7 ```javascript 27 ```javascript
8 -return value; 28 +return value + ' °C';
9 {:copy-code} 29 {:copy-code}
10 ``` 30 ```
11 31
12 -##### Examples 32 +* Present the datapoint value in tooltip in Amperage (A) units and two decimal places:
  33 +
  34 +```javascript
  35 +return value.toFixed(2) + ' A';
  36 +{:copy-code}
  37 +```
@@ -516,12 +516,15 @@ mat-label { @@ -516,12 +516,15 @@ mat-label {
516 } 516 }
517 517
518 p, div { 518 p, div {
519 - padding-left: 32px;  
520 padding-right: 32px; 519 padding-right: 32px;
521 color: rgba(15, 22, 29, 0.8); 520 color: rgba(15, 22, 29, 0.8);
522 line-height: 1.5em; 521 line-height: 1.5em;
523 } 522 }
524 523
  524 + p, div {
  525 + padding-left: 32px;
  526 + }
  527 +
525 ul { 528 ul {
526 padding-left: 62px; 529 padding-left: 62px;
527 padding-right: 32px; 530 padding-right: 32px;
@@ -541,6 +544,12 @@ mat-label { @@ -541,6 +544,12 @@ mat-label {
541 li { 544 li {
542 padding-bottom: .75em; 545 padding-bottom: .75em;
543 line-height: 1.5em; 546 line-height: 1.5em;
  547 + ul {
  548 + margin-bottom: 0;
  549 + }
  550 + p {
  551 + padding-left: 0;
  552 + }
544 } 553 }
545 554
546 a { 555 a {
@@ -651,7 +660,7 @@ mat-label { @@ -651,7 +660,7 @@ mat-label {
651 line-height: 24px; 660 line-height: 24px;
652 color: #fff; 661 color: #fff;
653 background-color: #305680; 662 background-color: #305680;
654 - box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.12), 0px 2px 2px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.2); 663 + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 2px 2px rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
655 text-decoration: none; 664 text-decoration: none;
656 font-size: 16px; 665 font-size: 16px;
657 font-weight: 500; 666 font-weight: 500;
@@ -777,7 +786,7 @@ mat-label { @@ -777,7 +786,7 @@ mat-label {
777 786
778 button.clipboard-btn { 787 button.clipboard-btn {
779 top: -10px; 788 top: -10px;
780 - right: 0px; 789 + right: 0;
781 padding: 0 3px; 790 padding: 0 3px;
782 } 791 }
783 } 792 }
@@ -1824,10 +1824,10 @@ @@ -1824,10 +1824,10 @@
1824 resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6" 1824 resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6"
1825 integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q== 1825 integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==
1826 1826
1827 -"@types/marked@^1.1.0":  
1828 - version "1.2.2"  
1829 - resolved "https://registry.yarnpkg.com/@types/marked/-/marked-1.2.2.tgz#1f858a0e690247ecf3b2eef576f98f86e8d960d4"  
1830 - integrity sha512-wLfw1hnuuDYrFz97IzJja0pdVsC0oedtS4QsKH1/inyW9qkLQbXgMUqEQT0MVtUBx3twjWeInUfjQbhBVLECXw== 1827 +"@types/marked@^2.0.0":
  1828 + version "2.0.5"
  1829 + resolved "https://registry.yarnpkg.com/@types/marked/-/marked-2.0.5.tgz#453e27f1e97199d45bb25297b0dd2b9bbc1e05ea"
  1830 + integrity sha512-shRZ7XnYFD/8n8zSjKvFdto1QNSf4tONZIlNEZGrJe8GsOE8DL/hG1Hbl8gZlfLnjS7+f5tZGIaTgfpyW38h4w==
1831 1831
1832 "@types/minimatch@*": 1832 "@types/minimatch@*":
1833 version "3.0.3" 1833 version "3.0.3"
@@ -3268,6 +3268,11 @@ commander@^2.11.0, commander@^2.12.1, commander@^2.19.0, commander@^2.20.0: @@ -3268,6 +3268,11 @@ commander@^2.11.0, commander@^2.12.1, commander@^2.19.0, commander@^2.20.0:
3268 resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" 3268 resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
3269 integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== 3269 integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
3270 3270
  3271 +commander@^6.0.0:
  3272 + version "6.2.1"
  3273 + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
  3274 + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
  3275 +
3271 commander@^7.1.0: 3276 commander@^7.1.0:
3272 version "7.2.0" 3277 version "7.2.0"
3273 resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" 3278 resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
@@ -6171,12 +6176,12 @@ karma@~6.3.2: @@ -6171,12 +6176,12 @@ karma@~6.3.2:
6171 ua-parser-js "^0.7.23" 6176 ua-parser-js "^0.7.23"
6172 yargs "^16.1.1" 6177 yargs "^16.1.1"
6173 6178
6174 -katex@^0.12.0:  
6175 - version "0.12.0"  
6176 - resolved "https://registry.yarnpkg.com/katex/-/katex-0.12.0.tgz#2fb1c665dbd2b043edcf8a1f5c555f46beaa0cb9"  
6177 - integrity sha512-y+8btoc/CK70XqcHqjxiGWBOeIL8upbS0peTPXTvgrh21n1RiWWcIpSWM+4uXq+IAgNh9YYQWdc7LVDPDAEEAg== 6179 +katex@^0.13.0:
  6180 + version "0.13.18"
  6181 + resolved "https://registry.yarnpkg.com/katex/-/katex-0.13.18.tgz#ba89e8e4b70cc2325e25e019a62b9fe71e5c2931"
  6182 + integrity sha512-a3dC4NSVSDU3O1WZbTnOiA8rVNJ2lSiomOl0kmckCIGObccIHXof7gAseIY0o1gjEspe+34ZeSEX2D1ChFKIvA==
6178 dependencies: 6183 dependencies:
6179 - commander "^2.19.0" 6184 + commander "^6.0.0"
6180 6185
6181 killable@^1.0.1: 6186 killable@^1.0.1:
6182 version "1.0.1" 6187 version "1.0.1"
@@ -6484,10 +6489,10 @@ map-visit@^1.0.0: @@ -6484,10 +6489,10 @@ map-visit@^1.0.0:
6484 dependencies: 6489 dependencies:
6485 object-visit "^1.0.0" 6490 object-visit "^1.0.0"
6486 6491
6487 -marked@^1.1.0:  
6488 - version "1.2.9"  
6489 - resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.9.tgz#53786f8b05d4c01a2a5a76b7d1ec9943d29d72dc"  
6490 - integrity sha512-H8lIX2SvyitGX+TRdtS06m1jHMijKN/XjfH6Ooii9fvxMlh8QdqBfBDkGUpMWH2kQNrtixjzYUa3SH8ROTgRRw== 6492 +marked@^2.0.0:
  6493 + version "2.1.3"
  6494 + resolved "https://registry.yarnpkg.com/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753"
  6495 + integrity sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==
6491 6496
6492 material-design-icons@^3.0.1: 6497 material-design-icons@^3.0.1:
6493 version "3.0.1" 6498 version "3.0.1"
@@ -6941,16 +6946,16 @@ ngx-hm-carousel@^2.0.0-rc.1: @@ -6941,16 +6946,16 @@ ngx-hm-carousel@^2.0.0-rc.1:
6941 hammerjs "^2.0.8" 6946 hammerjs "^2.0.8"
6942 resize-observer-polyfill "^1.5.1" 6947 resize-observer-polyfill "^1.5.1"
6943 6948
6944 -ngx-markdown@^10.1.1:  
6945 - version "10.1.1"  
6946 - resolved "https://registry.yarnpkg.com/ngx-markdown/-/ngx-markdown-10.1.1.tgz#17840c773db7ced4b18ccbf2e8cb06182e422de3"  
6947 - integrity sha512-bUVgN6asb35d5U4xM5CNfo7pSpuwqJSdTgK0PhNZzLiaiyPIK2owtLF6sWGhxTThJu+LngJPjj4MQ+AFe/s8XQ== 6949 +ngx-markdown@^11.1.3:
  6950 + version "11.1.3"
  6951 + resolved "https://registry.yarnpkg.com/ngx-markdown/-/ngx-markdown-11.1.3.tgz#4d01c2dc425e5a46d1b6aa8517d9a1c1feaa1efd"
  6952 + integrity sha512-z32q8l76ubrcP62L03mdvrizwueLBHV10LkT8MEDnFcjmY+8J1PytxFJ9EBTJpvc+CaPolgAoi7felN2XJZTSg==
6948 dependencies: 6953 dependencies:
6949 - "@types/marked" "^1.1.0" 6954 + "@types/marked" "^2.0.0"
6950 emoji-toolkit "^6.0.1" 6955 emoji-toolkit "^6.0.1"
6951 - katex "^0.12.0"  
6952 - marked "^1.1.0"  
6953 - prismjs "^1.20.0" 6956 + katex "^0.13.0"
  6957 + marked "^2.0.0"
  6958 + prismjs "^1.23.0"
6954 tslib "^2.0.0" 6959 tslib "^2.0.0"
6955 6960
6956 ngx-sharebuttons@^8.0.5: 6961 ngx-sharebuttons@^8.0.5:
@@ -7985,11 +7990,6 @@ pretty-bytes@^5.3.0: @@ -7985,11 +7990,6 @@ pretty-bytes@^5.3.0:
7985 resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" 7990 resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
7986 integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== 7991 integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
7987 7992
7988 -prismjs@^1.20.0:  
7989 - version "1.24.1"  
7990 - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.24.1.tgz#c4d7895c4d6500289482fa8936d9cdd192684036"  
7991 - integrity sha512-mNPsedLuk90RVJioIky8ANZEwYm5w9LcvCXrxHlwf4fNVSn8jEipMybMkWUyyF0JhnC+C4VcOVSBuHRKs1L5Ow==  
7992 -  
7993 prismjs@^1.23.0: 7993 prismjs@^1.23.0:
7994 version "1.23.0" 7994 version "1.23.0"
7995 resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33" 7995 resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33"