Commit b4f230dbc8dca59d43a831ab22d20d5a562475aa
1 parent
e264f7b8
Improve markdown component. Add new helps.
Showing
39 changed files
with
1724 additions
and
247 deletions
... | ... | @@ -73,7 +73,7 @@ |
73 | 73 | "ngx-drag-drop": "^2.0.0", |
74 | 74 | "ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master", |
75 | 75 | "ngx-hm-carousel": "^2.0.0-rc.1", |
76 | - "ngx-markdown": "^10.1.1", | |
76 | + "ngx-markdown": "^11.1.3", | |
77 | 77 | "ngx-sharebuttons": "^8.0.5", |
78 | 78 | "ngx-translate-messageformat-compiler": "^4.9.0", |
79 | 79 | "objectpath": "^2.0.0", | ... | ... |
... | ... | @@ -55,11 +55,12 @@ export class DynamicComponentFactoryService { |
55 | 55 | public createDynamicComponentFactory<T>( |
56 | 56 | componentType: Type<T>, |
57 | 57 | template: string, |
58 | - modules?: Type<any>[]): Observable<ComponentFactory<T>> { | |
58 | + modules?: Type<any>[], | |
59 | + preserveWhitespaces?: boolean): Observable<ComponentFactory<T>> { | |
59 | 60 | const dymamicComponentFactorySubject = new ReplaySubject<ComponentFactory<T>>(); |
60 | 61 | import('@angular/compiler').then( |
61 | 62 | () => { |
62 | - const comp = this.createDynamicComponent(componentType, template); | |
63 | + const comp = this.createDynamicComponent(componentType, template, preserveWhitespaces); | |
63 | 64 | let moduleImports: Type<any>[] = [CommonModule]; |
64 | 65 | if (modules) { |
65 | 66 | moduleImports = [...moduleImports, ...modules]; |
... | ... | @@ -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 | 108 | // noinspection AngularMissingOrInvalidDeclarationInModule |
108 | 109 | return Component({ |
109 | - template | |
110 | + template, | |
111 | + preserveWhitespaces | |
110 | 112 | })(componentType); |
111 | 113 | } |
112 | 114 | ... | ... |
... | ... | @@ -18,7 +18,8 @@ import { Injectable } from '@angular/core'; |
18 | 18 | import { HttpClient } from '@angular/common/http'; |
19 | 19 | import { TranslateService } from '@ngx-translate/core'; |
20 | 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 | 24 | const NOT_FOUND_CONTENT = '## Not found'; |
24 | 25 | |
... | ... | @@ -27,6 +28,8 @@ const NOT_FOUND_CONTENT = '## Not found'; |
27 | 28 | }) |
28 | 29 | export class HelpService { |
29 | 30 | |
31 | + private helpBaseUrl = helpBaseUrl; | |
32 | + | |
30 | 33 | private helpCache: {[lang: string]: {[key: string]: string}} = {}; |
31 | 34 | |
32 | 35 | constructor( |
... | ... | @@ -52,6 +55,9 @@ export class HelpService { |
52 | 55 | return of(NOT_FOUND_CONTENT); |
53 | 56 | } |
54 | 57 | }), |
58 | + mergeMap((content) => { | |
59 | + return this.processIncludes(this.processVariables(content)); | |
60 | + }), | |
55 | 61 | tap((content) => { |
56 | 62 | let langContent = this.helpCache[lang]; |
57 | 63 | if (!langContent) { |
... | ... | @@ -68,4 +74,25 @@ export class HelpService { |
68 | 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 | 46 | [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']" |
47 | 47 | [disableUndefinedCheck]="true" |
48 | 48 | [validationArgs]="[]" |
49 | - [editorCompleter]="customPrettyActionEditorCompleter"> | |
49 | + [editorCompleter]="customPrettyActionEditorCompleter" | |
50 | + helpId="widget/action/custom_pretty_action_fn"> | |
50 | 51 | </tb-js-func> |
51 | 52 | </div> |
52 | 53 | </div> | ... | ... |
... | ... | @@ -96,7 +96,8 @@ |
96 | 96 | [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']" |
97 | 97 | [disableUndefinedCheck]="true" |
98 | 98 | [validationArgs]="[]" |
99 | - [editorCompleter]="customPrettyActionEditorCompleter"> | |
99 | + [editorCompleter]="customPrettyActionEditorCompleter" | |
100 | + helpId="widget/action/custom_pretty_action_fn"> | |
100 | 101 | </tb-js-func> |
101 | 102 | </mat-tab> |
102 | 103 | </mat-tab-group> | ... | ... |
... | ... | @@ -224,6 +224,7 @@ |
224 | 224 | [globalVariables]="functionScopeVariables" |
225 | 225 | [validationArgs]="[]" |
226 | 226 | [editorCompleter]="customActionEditorCompleter" |
227 | + helpId="widget/action/custom_action_fn" | |
227 | 228 | ></tb-js-func> |
228 | 229 | </ng-template> |
229 | 230 | <ng-template [ngSwitchCase]="widgetActionType.customPretty"> | ... | ... |
... | ... | @@ -70,6 +70,7 @@ |
70 | 70 | [globalVariables]="functionScopeVariables" |
71 | 71 | [validationArgs]="[[1, 1],[1, '1']]" |
72 | 72 | resultType="any" |
73 | + helpId="widget/config/datakey_generation_fn" | |
73 | 74 | formControlName="funcBody"> |
74 | 75 | </tb-js-func> |
75 | 76 | </section> |
... | ... | @@ -82,6 +83,7 @@ |
82 | 83 | [globalVariables]="functionScopeVariables" |
83 | 84 | [validationArgs]="[[1, 1, 1, 1, 1],[1, '1', '1', 1, '1']]" |
84 | 85 | resultType="any" |
86 | + helpId="widget/config/datakey_postprocess_fn" | |
85 | 87 | formControlName="postFuncBody"> |
86 | 88 | </tb-js-func> |
87 | 89 | <label *ngIf="dataKeyFormGroup.get('usePostProcessing').value" class="tb-title" style="margin-left: 15px;"> | ... | ... |
... | ... | @@ -15,4 +15,4 @@ |
15 | 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 | 107 | import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token'; |
108 | 108 | import { MobileService } from '@core/services/mobile.service'; |
109 | 109 | import { DialogService } from '@core/services/dialog.service'; |
110 | -import { TbPopoverService } from '@shared/components/popover.component'; | |
111 | 110 | import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component'; |
112 | 111 | import { PopoverPlacement } from '@shared/components/popover.models'; |
112 | +import { TbPopoverService } from '@shared/components/popover.service'; | |
113 | 113 | |
114 | 114 | @Component({ |
115 | 115 | selector: 'tb-widget', | ... | ... |
... | ... | @@ -16,5 +16,5 @@ |
16 | 16 | |
17 | 17 | --> |
18 | 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 | 20 | </ng-container> | ... | ... |
... | ... | @@ -13,16 +13,14 @@ |
13 | 13 | * See the License for the specific language governing permissions and |
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | -:host { | |
16 | + | |
17 | +:host ::ng-deep { | |
17 | 18 | .tb-help-markdown { |
18 | 19 | overflow: auto; |
19 | 20 | max-width: 80vw; |
20 | 21 | max-height: 80vh; |
21 | 22 | margin-top: 30px; |
22 | 23 | } |
23 | -} | |
24 | - | |
25 | -:host ::ng-deep { | |
26 | 24 | .tb-help-markdown.tb-markdown-view { |
27 | 25 | h1, h2, h3, h4, h5, h6 { |
28 | 26 | &:first-child { | ... | ... |
... | ... | @@ -22,7 +22,7 @@ import { |
22 | 22 | Output, SimpleChanges |
23 | 23 | } from '@angular/core'; |
24 | 24 | import { BehaviorSubject } from 'rxjs'; |
25 | -import { delay, share } from 'rxjs/operators'; | |
25 | +import { share } from 'rxjs/operators'; | |
26 | 26 | import { HelpService } from '@core/services/help.service'; |
27 | 27 | |
28 | 28 | @Component({ |
... | ... | @@ -34,8 +34,12 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { |
34 | 34 | |
35 | 35 | @Input() helpId: string; |
36 | 36 | |
37 | + @Input() helpContent: string; | |
38 | + | |
37 | 39 | @Input() visible: boolean; |
38 | 40 | |
41 | + @Input() style: { [klass: string]: any } = {}; | |
42 | + | |
39 | 43 | @Output() markdownReady = new EventEmitter<void>(); |
40 | 44 | |
41 | 45 | markdownText = new BehaviorSubject<string>(null); |
... | ... | @@ -44,8 +48,6 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { |
44 | 48 | share() |
45 | 49 | ); |
46 | 50 | |
47 | - isMarkdownReady = false; | |
48 | - | |
49 | 51 | private loadHelpPending = false; |
50 | 52 | |
51 | 53 | constructor(private help: HelpService) {} |
... | ... | @@ -68,7 +70,7 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { |
68 | 70 | this.loadHelp(); |
69 | 71 | } |
70 | 72 | } |
71 | - if (propName === 'helpId') { | |
73 | + if (propName === 'helpId' || propName === 'helpContent') { | |
72 | 74 | this.markdownText.next(null); |
73 | 75 | this.loadHelpWhenVisible(); |
74 | 76 | } |
... | ... | @@ -89,16 +91,16 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { |
89 | 91 | this.help.getHelpContent(this.helpId).subscribe((content) => { |
90 | 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 | 99 | onMarkdownReady() { |
96 | - this.isMarkdownReady = true; | |
97 | 100 | this.markdownReady.next(); |
98 | 101 | } |
99 | 102 | |
100 | 103 | markdownClick($event: MouseEvent) { |
101 | - | |
102 | 104 | } |
103 | 105 | |
104 | 106 | } | ... | ... |
... | ... | @@ -15,16 +15,22 @@ |
15 | 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 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | 16 | .tb-help-popup-button { |
17 | + position: relative; | |
17 | 18 | } |
18 | 19 | .tb-help-popup-button-loading { |
19 | 20 | background: #fff; |
20 | 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 | 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 | 30 | @Component({ |
21 | 31 | // tslint:disable-next-line:component-selector |
... | ... | @@ -26,25 +36,41 @@ import { TbPopoverService } from '@shared/components/popover.component'; |
26 | 36 | }) |
27 | 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 | 42 | // tslint:disable-next-line:no-input-rename |
30 | 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 | 54 | popoverVisible = false; |
33 | 55 | popoverReady = true; |
34 | 56 | |
35 | - constructor(private elementRef: ElementRef, | |
36 | - private viewContainerRef: ViewContainerRef, | |
57 | + constructor(private viewContainerRef: ViewContainerRef, | |
37 | 58 | private renderer: Renderer2, |
38 | 59 | private popoverService: TbPopoverService) {} |
39 | 60 | |
40 | 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 | 64 | this.helpId, |
65 | + '', | |
43 | 66 | (visible) => { |
44 | 67 | this.popoverVisible = visible; |
45 | 68 | }, (ready => { |
46 | 69 | this.popoverReady = ready; |
47 | - })); | |
70 | + }), | |
71 | + this.helpPopupPlacement, | |
72 | + {}, | |
73 | + this.helpPopupStyle); | |
48 | 74 | } |
49 | 75 | |
50 | 76 | ngOnDestroy(): void { | ... | ... |
... | ... | @@ -46,8 +46,7 @@ import { GroupInfo } from '@shared/models/widget.models'; |
46 | 46 | import { Observable } from 'rxjs/internal/Observable'; |
47 | 47 | import { forkJoin, from } from 'rxjs'; |
48 | 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 | 51 | const tinycolor = tinycolor_; |
53 | 52 | |
... | ... | @@ -252,7 +251,7 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato |
252 | 251 | |
253 | 252 | private onHelpClick(event: MouseEvent, helpId: string, helpVisibleFn: (visible: boolean) => void, helpReadyFn: (ready: boolean) => void) { |
254 | 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 | 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, '{').replace(/}/g, '}'); | |
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 | 85 | |
86 | 86 | private wrapCopyCode(id: number, content: string, code: string): string { |
87 | 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 | 89 | `<button class="clipboard-btn">\n` + |
90 | 90 | ` <p>${this.translate.instant('markdown.copy-code')}</p>\n` + |
91 | 91 | ` <div>\n` + |
... | ... | @@ -119,7 +119,7 @@ export class MarkedOptionsService extends MarkedOptions { |
119 | 119 | private markdownCopyCode(id: number) { |
120 | 120 | const copyWrapper = $('#codeWrapper' + id); |
121 | 121 | if (copyWrapper.hasClass('noChars')) { |
122 | - const text = $('#copyCodeId' + id).text(); | |
122 | + const text = decodeURIComponent($('#copyCodeId' + id).text()); | |
123 | 123 | this.window.navigator.clipboard.writeText(text).then(() => { |
124 | 124 | import('tooltipster').then( |
125 | 125 | () => { | ... | ... |
... | ... | @@ -25,7 +25,6 @@ import { |
25 | 25 | Directive, |
26 | 26 | ElementRef, |
27 | 27 | EventEmitter, |
28 | - Injectable, | |
29 | 28 | Injector, |
30 | 29 | Input, |
31 | 30 | OnChanges, |
... | ... | @@ -36,7 +35,6 @@ import { |
36 | 35 | Renderer2, |
37 | 36 | SimpleChanges, |
38 | 37 | TemplateRef, |
39 | - Type, | |
40 | 38 | ViewChild, |
41 | 39 | ViewContainerRef, |
42 | 40 | ViewEncapsulation |
... | ... | @@ -54,13 +52,11 @@ import { |
54 | 52 | getPlacementName, |
55 | 53 | popoverMotion, |
56 | 54 | PopoverPlacement, |
57 | - PopoverWithTrigger, | |
58 | 55 | POSITION_MAP, |
59 | 56 | PropertyMapping |
60 | 57 | } from '@shared/components/popover.models'; |
61 | 58 | import { distinctUntilChanged, takeUntil } from 'rxjs/operators'; |
62 | 59 | import { isNotEmptyStr, onParentScrollOrWindowResize } from '@core/utils'; |
63 | -import { HelpMarkdownComponent } from '@shared/components/help-markdown.component'; | |
64 | 60 | |
65 | 61 | export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null; |
66 | 62 | |
... | ... | @@ -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 | 284 | @Component({ |
445 | 285 | selector: 'tb-popover', |
446 | 286 | exportAs: 'tbPopoverComponent', |
... | ... | @@ -703,10 +543,12 @@ export class TbPopoverComponent implements OnDestroy, OnInit { |
703 | 543 | |
704 | 544 | updateStyles(): void { |
705 | 545 | this.classMap = { |
706 | - [this.tbOverlayClassName]: true, | |
707 | 546 | [`tb-popover-placement-${this.preferredPlacement}`]: true, |
708 | 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 | 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 | +} | ... | ... |
ui-ngx/src/app/shared/components/tokens.ts
0 → 100644
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'); | ... | ... |
... | ... | @@ -147,11 +147,14 @@ import { MAT_DATE_LOCALE } from '@angular/material/core'; |
147 | 147 | import { CopyButtonComponent } from '@shared/components/button/copy-button.component'; |
148 | 148 | import { TogglePasswordComponent } from '@shared/components/button/toggle-password.component'; |
149 | 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 | 151 | import { TbStringTemplateOutletDirective } from '@shared/components/directives/sring-template-outlet.directive'; |
152 | 152 | import { TbComponentOutletDirective} from '@shared/components/directives/component-outlet.directive'; |
153 | 153 | import { HelpMarkdownComponent } from '@shared/components/help-markdown.component'; |
154 | 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 | 159 | export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { |
157 | 160 | return markedOptionsService; |
... | ... | @@ -174,6 +177,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) |
174 | 177 | provide: MAT_DATE_LOCALE, |
175 | 178 | useValue: 'en-GB' |
176 | 179 | }, |
180 | + { provide: HELP_MARKDOWN_COMPONENT_TOKEN, useValue: HelpMarkdownComponent }, | |
181 | + { provide: SHARED_MODULE_TOKEN, useValue: SharedModule }, | |
177 | 182 | TbPopoverService |
178 | 183 | ], |
179 | 184 | declarations: [ |
... | ... | @@ -190,6 +195,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) |
190 | 195 | TbStringTemplateOutletDirective, |
191 | 196 | TbComponentOutletDirective, |
192 | 197 | TbPopoverDirective, |
198 | + TbMarkdownComponent, | |
193 | 199 | HelpComponent, |
194 | 200 | HelpMarkdownComponent, |
195 | 201 | HelpPopupComponent, |
... | ... | @@ -336,6 +342,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) |
336 | 342 | TbStringTemplateOutletDirective, |
337 | 343 | TbComponentOutletDirective, |
338 | 344 | TbPopoverDirective, |
345 | + TbMarkdownComponent, | |
339 | 346 | HelpComponent, |
340 | 347 | HelpMarkdownComponent, |
341 | 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 | 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 | 5 | |
6 | 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 | 10 | **Parameters:** |
11 | 11 | |
12 | 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 | 14 | and data used by widget instance. |
15 | 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 | 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 | 18 | </li> |
19 | 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 | 27 | ```javascript |
8 | -return value; | |
28 | +return value + ' °C'; | |
9 | 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 | 516 | } |
517 | 517 | |
518 | 518 | p, div { |
519 | - padding-left: 32px; | |
520 | 519 | padding-right: 32px; |
521 | 520 | color: rgba(15, 22, 29, 0.8); |
522 | 521 | line-height: 1.5em; |
523 | 522 | } |
524 | 523 | |
524 | + p, div { | |
525 | + padding-left: 32px; | |
526 | + } | |
527 | + | |
525 | 528 | ul { |
526 | 529 | padding-left: 62px; |
527 | 530 | padding-right: 32px; |
... | ... | @@ -541,6 +544,12 @@ mat-label { |
541 | 544 | li { |
542 | 545 | padding-bottom: .75em; |
543 | 546 | line-height: 1.5em; |
547 | + ul { | |
548 | + margin-bottom: 0; | |
549 | + } | |
550 | + p { | |
551 | + padding-left: 0; | |
552 | + } | |
544 | 553 | } |
545 | 554 | |
546 | 555 | a { |
... | ... | @@ -651,7 +660,7 @@ mat-label { |
651 | 660 | line-height: 24px; |
652 | 661 | color: #fff; |
653 | 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 | 664 | text-decoration: none; |
656 | 665 | font-size: 16px; |
657 | 666 | font-weight: 500; |
... | ... | @@ -777,7 +786,7 @@ mat-label { |
777 | 786 | |
778 | 787 | button.clipboard-btn { |
779 | 788 | top: -10px; |
780 | - right: 0px; | |
789 | + right: 0; | |
781 | 790 | padding: 0 3px; |
782 | 791 | } |
783 | 792 | } | ... | ... |
... | ... | @@ -1824,10 +1824,10 @@ |
1824 | 1824 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6" |
1825 | 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 | 1832 | "@types/minimatch@*": |
1833 | 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 | 3268 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" |
3269 | 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 | 3276 | commander@^7.1.0: |
3272 | 3277 | version "7.2.0" |
3273 | 3278 | resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" |
... | ... | @@ -6171,12 +6176,12 @@ karma@~6.3.2: |
6171 | 6176 | ua-parser-js "^0.7.23" |
6172 | 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 | 6183 | dependencies: |
6179 | - commander "^2.19.0" | |
6184 | + commander "^6.0.0" | |
6180 | 6185 | |
6181 | 6186 | killable@^1.0.1: |
6182 | 6187 | version "1.0.1" |
... | ... | @@ -6484,10 +6489,10 @@ map-visit@^1.0.0: |
6484 | 6489 | dependencies: |
6485 | 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 | 6497 | material-design-icons@^3.0.1: |
6493 | 6498 | version "3.0.1" |
... | ... | @@ -6941,16 +6946,16 @@ ngx-hm-carousel@^2.0.0-rc.1: |
6941 | 6946 | hammerjs "^2.0.8" |
6942 | 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 | 6953 | dependencies: |
6949 | - "@types/marked" "^1.1.0" | |
6954 | + "@types/marked" "^2.0.0" | |
6950 | 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 | 6959 | tslib "^2.0.0" |
6955 | 6960 | |
6956 | 6961 | ngx-sharebuttons@^8.0.5: |
... | ... | @@ -7985,11 +7990,6 @@ pretty-bytes@^5.3.0: |
7985 | 7990 | resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" |
7986 | 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 | 7993 | prismjs@^1.23.0: |
7994 | 7994 | version "1.23.0" |
7995 | 7995 | resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33" | ... | ... |