Showing
18 changed files
with
1112 additions
and
43 deletions
@@ -31,7 +31,7 @@ import { ComponentDescriptorService } from './component-descriptor.service'; | @@ -31,7 +31,7 @@ import { ComponentDescriptorService } from './component-descriptor.service'; | ||
31 | import { | 31 | import { |
32 | IRuleNodeConfigurationComponent, | 32 | IRuleNodeConfigurationComponent, |
33 | LinkLabel, | 33 | LinkLabel, |
34 | - RuleNodeComponentDescriptor | 34 | + RuleNodeComponentDescriptor, TestScriptInputParams, TestScriptResult |
35 | } from '@app/shared/models/rule-node.models'; | 35 | } from '@app/shared/models/rule-node.models'; |
36 | import { ResourcesService } from '../services/resources.service'; | 36 | import { ResourcesService } from '../services/resources.service'; |
37 | import { catchError, map, mergeMap } from 'rxjs/operators'; | 37 | import { catchError, map, mergeMap } from 'rxjs/operators'; |
@@ -175,6 +175,10 @@ export class RuleChainService { | @@ -175,6 +175,10 @@ export class RuleChainService { | ||
175 | return this.http.get<DebugRuleNodeEventBody>(`/api/ruleNode/${ruleNodeId}/debugIn`, defaultHttpOptionsFromConfig(config)); | 175 | return this.http.get<DebugRuleNodeEventBody>(`/api/ruleNode/${ruleNodeId}/debugIn`, defaultHttpOptionsFromConfig(config)); |
176 | } | 176 | } |
177 | 177 | ||
178 | + public testScript(inputParams: TestScriptInputParams, config?: RequestConfig): Observable<TestScriptResult> { | ||
179 | + return this.http.post<TestScriptResult>('/api/ruleChain/testScript', inputParams, defaultHttpOptionsFromConfig(config)); | ||
180 | + } | ||
181 | + | ||
178 | private resolveTargetRuleChains(ruleChainConnections: Array<RuleChainConnectionInfo>): Observable<{[ruleChainId: string]: RuleChain}> { | 182 | private resolveTargetRuleChains(ruleChainConnections: Array<RuleChainConnectionInfo>): Observable<{[ruleChainId: string]: RuleChain}> { |
179 | if (ruleChainConnections && ruleChainConnections.length) { | 183 | if (ruleChainConnections && ruleChainConnections.length) { |
180 | const tasks: Observable<RuleChain>[] = []; | 184 | const tasks: Observable<RuleChain>[] = []; |
@@ -23,7 +23,7 @@ import { | @@ -23,7 +23,7 @@ import { | ||
23 | HttpResponseBase | 23 | HttpResponseBase |
24 | } from '@angular/common/http'; | 24 | } from '@angular/common/http'; |
25 | import { Observable } from 'rxjs/internal/Observable'; | 25 | import { Observable } from 'rxjs/internal/Observable'; |
26 | -import { Injectable } from '@angular/core'; | 26 | +import { Inject, Injectable } from '@angular/core'; |
27 | import { AuthService } from '../auth/auth.service'; | 27 | import { AuthService } from '../auth/auth.service'; |
28 | import { Constants } from '../../shared/models/constants'; | 28 | import { Constants } from '../../shared/models/constants'; |
29 | import { InterceptorHttpParams } from './interceptor-http-params'; | 29 | import { InterceptorHttpParams } from './interceptor-http-params'; |
@@ -52,10 +52,10 @@ export class GlobalHttpInterceptor implements HttpInterceptor { | @@ -52,10 +52,10 @@ export class GlobalHttpInterceptor implements HttpInterceptor { | ||
52 | 52 | ||
53 | private activeRequests = 0; | 53 | private activeRequests = 0; |
54 | 54 | ||
55 | - constructor(private store: Store<AppState>, | ||
56 | - private dialogService: DialogService, | ||
57 | - private translate: TranslateService, | ||
58 | - private authService: AuthService) { | 55 | + constructor(@Inject(Store) private store: Store<AppState>, |
56 | + @Inject(DialogService) private dialogService: DialogService, | ||
57 | + @Inject(TranslateService) private translate: TranslateService, | ||
58 | + @Inject(AuthService) private authService: AuthService) { | ||
59 | } | 59 | } |
60 | 60 | ||
61 | intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | 61 | intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { |
@@ -36,7 +36,7 @@ export class NodeScriptTestService { | @@ -36,7 +36,7 @@ export class NodeScriptTestService { | ||
36 | return this.ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).pipe( | 36 | return this.ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).pipe( |
37 | switchMap((debugIn) => { | 37 | switchMap((debugIn) => { |
38 | let msg: any; | 38 | let msg: any; |
39 | - let metadata: any; | 39 | + let metadata: {[key: string]: string}; |
40 | let msgType: string; | 40 | let msgType: string; |
41 | if (debugIn) { | 41 | if (debugIn) { |
42 | if (debugIn.data) { | 42 | if (debugIn.data) { |
@@ -59,7 +59,7 @@ export class NodeScriptTestService { | @@ -59,7 +59,7 @@ export class NodeScriptTestService { | ||
59 | 59 | ||
60 | private openTestScriptDialog(script: string, scriptType: string, | 60 | private openTestScriptDialog(script: string, scriptType: string, |
61 | functionTitle: string, functionName: string, argNames: string[], | 61 | functionTitle: string, functionName: string, argNames: string[], |
62 | - msg?: any, metadata?: any, msgType?: string): Observable<string> { | 62 | + msg?: any, metadata?: {[key: string]: string}, msgType?: string): Observable<string> { |
63 | if (!msg) { | 63 | if (!msg) { |
64 | msg = { | 64 | msg = { |
65 | temperature: 22.4, | 65 | temperature: 22.4, |
@@ -26,9 +26,6 @@ | @@ -26,9 +26,6 @@ | ||
26 | <mat-icon class="material-icons">close</mat-icon> | 26 | <mat-icon class="material-icons">close</mat-icon> |
27 | </button> | 27 | </button> |
28 | </mat-toolbar> | 28 | </mat-toolbar> |
29 | - <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | ||
30 | - </mat-progress-bar> | ||
31 | - <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | ||
32 | <div mat-dialog-content fxFlex style="position: relative;"> | 29 | <div mat-dialog-content fxFlex style="position: relative;"> |
33 | <div class="tb-absolute-fill"> | 30 | <div class="tb-absolute-fill"> |
34 | <div #topPanel class="tb-split tb-split-vertical"> | 31 | <div #topPanel class="tb-split tb-split-vertical"> |
@@ -37,7 +34,24 @@ | @@ -37,7 +34,24 @@ | ||
37 | <div class="tb-editor-area-title-panel"> | 34 | <div class="tb-editor-area-title-panel"> |
38 | <label translate>rulenode.message</label> | 35 | <label translate>rulenode.message</label> |
39 | </div> | 36 | </div> |
40 | - TODO: payloadForm | 37 | + <div formGroupName="payload" fxLayout="column" style="height: 100%;"> |
38 | + <div fxLayout="row"> | ||
39 | + <tb-message-type-autocomplete | ||
40 | + style="margin-bottom: 0px; min-width: 300px;" | ||
41 | + formControlName="msgType" | ||
42 | + required> | ||
43 | + </tb-message-type-autocomplete> | ||
44 | + </div> | ||
45 | + <tb-json-content | ||
46 | + #payloadContent | ||
47 | + fxFlex | ||
48 | + formControlName="msg" | ||
49 | + label="{{ 'rulenode.message' | translate }}" | ||
50 | + [contentType]="contentTypes.JSON" | ||
51 | + validateContent | ||
52 | + [fillHeight]="true"> | ||
53 | + </tb-json-content> | ||
54 | + </div> | ||
41 | </div> | 55 | </div> |
42 | </div> | 56 | </div> |
43 | <div #topRightPanel class="tb-split tb-content"> | 57 | <div #topRightPanel class="tb-split tb-content"> |
@@ -45,7 +59,10 @@ | @@ -45,7 +59,10 @@ | ||
45 | <div class="tb-editor-area-title-panel"> | 59 | <div class="tb-editor-area-title-panel"> |
46 | <label translate>rulenode.metadata</label> | 60 | <label translate>rulenode.metadata</label> |
47 | </div> | 61 | </div> |
48 | - TODO: metadataForm | 62 | + <tb-key-val-map |
63 | + formControlName="metadata" | ||
64 | + titleText="rulenode.metadata"> | ||
65 | + </tb-key-val-map> | ||
49 | </div> | 66 | </div> |
50 | </div> | 67 | </div> |
51 | </div> | 68 | </div> |
@@ -55,7 +72,14 @@ | @@ -55,7 +72,14 @@ | ||
55 | <div class="tb-editor-area-title-panel tb-js-function"> | 72 | <div class="tb-editor-area-title-panel tb-js-function"> |
56 | <label>{{ functionTitle }}</label> | 73 | <label>{{ functionTitle }}</label> |
57 | </div> | 74 | </div> |
58 | - TODO: funcBodyForm | 75 | + <tb-js-func |
76 | + formControlName="script" | ||
77 | + functionName="{{ data.functionName }}" | ||
78 | + [functionArgs]="data.argNames" | ||
79 | + [validationArgs]="[data.msg, data.metadata, data.msgType]" | ||
80 | + resultType="object" | ||
81 | + [fillHeight]="true"> | ||
82 | + </tb-js-func> | ||
59 | </div> | 83 | </div> |
60 | </div> | 84 | </div> |
61 | <div #bottomRightPanel class="tb-split tb-content"> | 85 | <div #bottomRightPanel class="tb-split tb-content"> |
@@ -63,7 +87,15 @@ | @@ -63,7 +87,15 @@ | ||
63 | <div class="tb-editor-area-title-panel"> | 87 | <div class="tb-editor-area-title-panel"> |
64 | <label translate>rulenode.output</label> | 88 | <label translate>rulenode.output</label> |
65 | </div> | 89 | </div> |
66 | - TODO: output | 90 | + <tb-json-content |
91 | + fxFlex | ||
92 | + formControlName="output" | ||
93 | + label="{{ 'rulenode.output' | translate }}" | ||
94 | + [contentType]="contentTypes.JSON" | ||
95 | + validateContent="false" | ||
96 | + readonly="true" | ||
97 | + [fillHeight]="true"> | ||
98 | + </tb-json-content> | ||
67 | </div> | 99 | </div> |
68 | </div> | 100 | </div> |
69 | </div> | 101 | </div> |
@@ -79,7 +111,7 @@ | @@ -79,7 +111,7 @@ | ||
79 | <span fxFlex></span> | 111 | <span fxFlex></span> |
80 | <button mat-button mat-raised-button color="primary" | 112 | <button mat-button mat-raised-button color="primary" |
81 | type="submit" | 113 | type="submit" |
82 | - [disabled]="(isLoading$ | async) || nodeScriptTestFormGroup.get('funcBody').invalid || !nodeScriptTestFormGroup.get('funcBody').dirty"> | 114 | + [disabled]="(isLoading$ | async) || nodeScriptTestFormGroup.get('script').invalid || !nodeScriptTestFormGroup.get('script').dirty"> |
83 | {{ 'action.save' | translate }} | 115 | {{ 'action.save' | translate }} |
84 | </button> | 116 | </button> |
85 | <button mat-button color="primary" | 117 | <button mat-button color="primary" |
@@ -21,7 +21,7 @@ import { | @@ -21,7 +21,7 @@ import { | ||
21 | Inject, | 21 | Inject, |
22 | OnInit, | 22 | OnInit, |
23 | QueryList, | 23 | QueryList, |
24 | - SkipSelf, | 24 | + SkipSelf, ViewChild, |
25 | ViewChildren, | 25 | ViewChildren, |
26 | ViewEncapsulation | 26 | ViewEncapsulation |
27 | } from '@angular/core'; | 27 | } from '@angular/core'; |
@@ -29,9 +29,16 @@ import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/mater | @@ -29,9 +29,16 @@ import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/mater | ||
29 | import { Store } from '@ngrx/store'; | 29 | import { Store } from '@ngrx/store'; |
30 | import { AppState } from '@core/core.state'; | 30 | import { AppState } from '@core/core.state'; |
31 | import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; | 31 | import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; |
32 | -import { combineLatest } from 'rxjs'; | 32 | +import { combineLatest, never, Observable, of, throwError, NEVER } from 'rxjs'; |
33 | import { Router } from '@angular/router'; | 33 | import { Router } from '@angular/router'; |
34 | import { DialogComponent } from '@app/shared/components/dialog.component'; | 34 | import { DialogComponent } from '@app/shared/components/dialog.component'; |
35 | +import { ContentType } from '@shared/models/constants'; | ||
36 | +import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; | ||
37 | +import { JsonContentComponent } from '@shared/components/json-content.component'; | ||
38 | +import { TestScriptInputParams } from '@shared/models/rule-node.models'; | ||
39 | +import { RuleChainService } from '@core/http/rule-chain.service'; | ||
40 | +import { map, mergeMap } from 'rxjs/operators'; | ||
41 | +import { ActionNotificationShow } from '@core/notification/notification.actions'; | ||
35 | 42 | ||
36 | export interface NodeScriptTestDialogData { | 43 | export interface NodeScriptTestDialogData { |
37 | script: string; | 44 | script: string; |
@@ -40,7 +47,7 @@ export interface NodeScriptTestDialogData { | @@ -40,7 +47,7 @@ export interface NodeScriptTestDialogData { | ||
40 | functionName: string; | 47 | functionName: string; |
41 | argNames: string[]; | 48 | argNames: string[]; |
42 | msg?: any; | 49 | msg?: any; |
43 | - metadata?: any; | 50 | + metadata?: {[key: string]: string}; |
44 | msgType?: string; | 51 | msgType?: string; |
45 | } | 52 | } |
46 | 53 | ||
@@ -75,46 +82,46 @@ export class NodeScriptTestDialogComponent extends DialogComponent<NodeScriptTes | @@ -75,46 +82,46 @@ export class NodeScriptTestDialogComponent extends DialogComponent<NodeScriptTes | ||
75 | @ViewChildren('bottomRightPanel') | 82 | @ViewChildren('bottomRightPanel') |
76 | bottomRightPanelElmRef: QueryList<ElementRef<HTMLElement>>; | 83 | bottomRightPanelElmRef: QueryList<ElementRef<HTMLElement>>; |
77 | 84 | ||
85 | + @ViewChild('payloadContent', {static: true}) payloadContent: JsonContentComponent; | ||
86 | + | ||
78 | nodeScriptTestFormGroup: FormGroup; | 87 | nodeScriptTestFormGroup: FormGroup; |
79 | 88 | ||
80 | functionTitle: string; | 89 | functionTitle: string; |
81 | 90 | ||
82 | submitted = false; | 91 | submitted = false; |
83 | 92 | ||
93 | + contentTypes = ContentType; | ||
94 | + | ||
84 | constructor(protected store: Store<AppState>, | 95 | constructor(protected store: Store<AppState>, |
85 | protected router: Router, | 96 | protected router: Router, |
86 | @Inject(MAT_DIALOG_DATA) public data: NodeScriptTestDialogData, | 97 | @Inject(MAT_DIALOG_DATA) public data: NodeScriptTestDialogData, |
87 | @SkipSelf() private errorStateMatcher: ErrorStateMatcher, | 98 | @SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
88 | public dialogRef: MatDialogRef<NodeScriptTestDialogComponent, string>, | 99 | public dialogRef: MatDialogRef<NodeScriptTestDialogComponent, string>, |
89 | - public fb: FormBuilder) { | 100 | + public fb: FormBuilder, |
101 | + private ruleChainService: RuleChainService) { | ||
90 | super(store, router, dialogRef); | 102 | super(store, router, dialogRef); |
91 | this.functionTitle = this.data.functionTitle; | 103 | this.functionTitle = this.data.functionTitle; |
92 | } | 104 | } |
93 | 105 | ||
94 | ngOnInit(): void { | 106 | ngOnInit(): void { |
95 | this.nodeScriptTestFormGroup = this.fb.group({ | 107 | this.nodeScriptTestFormGroup = this.fb.group({ |
96 | - funcBody: ['', [Validators.required]] | 108 | + payload: this.fb.group({ |
109 | + msgType: [this.data.msgType, [Validators.required]], | ||
110 | + msg: [js_beautify(JSON.stringify(this.data.msg), {indent_size: 4}), []], | ||
111 | + }), | ||
112 | + metadata: [this.data.metadata, [Validators.required]], | ||
113 | + script: [this.data.script, []], | ||
114 | + output: ['', []] | ||
97 | }); | 115 | }); |
98 | } | 116 | } |
99 | 117 | ||
100 | ngAfterViewInit(): void { | 118 | ngAfterViewInit(): void { |
101 | -/* combineLatest(this.topPanelElmRef.changes, | ||
102 | - this.topLeftPanelElmRef.changes, | ||
103 | - this.topRightPanelElmRef.changes, | ||
104 | - this.bottomPanelElmRef.changes, | ||
105 | - this.bottomLeftPanelElmRef.changes, | ||
106 | - this.bottomRightPanelElmRef.changes).subscribe(() => { | ||
107 | - if (this.topPanelElmRef.length && this.topLeftPanelElmRef.length && | ||
108 | - this.topRightPanelElmRef.length && this.bottomPanelElmRef.length && | ||
109 | - this.bottomLeftPanelElmRef.length && this.bottomRightPanelElmRef.length) {*/ | ||
110 | - this.initSplitLayout(this.topPanelElmRef.first.nativeElement, | ||
111 | - this.topLeftPanelElmRef.first.nativeElement, | ||
112 | - this.topRightPanelElmRef.first.nativeElement, | ||
113 | - this.bottomPanelElmRef.first.nativeElement, | ||
114 | - this.bottomLeftPanelElmRef.first.nativeElement, | ||
115 | - this.bottomRightPanelElmRef.first.nativeElement); | ||
116 | - // } | ||
117 | - //}); | 119 | + this.initSplitLayout(this.topPanelElmRef.first.nativeElement, |
120 | + this.topLeftPanelElmRef.first.nativeElement, | ||
121 | + this.topRightPanelElmRef.first.nativeElement, | ||
122 | + this.bottomPanelElmRef.first.nativeElement, | ||
123 | + this.bottomLeftPanelElmRef.first.nativeElement, | ||
124 | + this.bottomRightPanelElmRef.first.nativeElement); | ||
118 | } | 125 | } |
119 | 126 | ||
120 | private initSplitLayout(topPanel: any, | 127 | private initSplitLayout(topPanel: any, |
@@ -154,9 +161,54 @@ export class NodeScriptTestDialogComponent extends DialogComponent<NodeScriptTes | @@ -154,9 +161,54 @@ export class NodeScriptTestDialogComponent extends DialogComponent<NodeScriptTes | ||
154 | this.dialogRef.close(null); | 161 | this.dialogRef.close(null); |
155 | } | 162 | } |
156 | 163 | ||
164 | + test(): void { | ||
165 | + this.testNodeScript().subscribe((output) => { | ||
166 | + this.nodeScriptTestFormGroup.get('output').setValue(js_beautify(output, {indent_size: 4})); | ||
167 | + }); | ||
168 | + } | ||
169 | + | ||
170 | + private testNodeScript(): Observable<string> { | ||
171 | + if (this.checkInputParamErrors()) { | ||
172 | + const inputParams: TestScriptInputParams = { | ||
173 | + argNames: this.data.argNames, | ||
174 | + scriptType: this.data.scriptType, | ||
175 | + msgType: this.nodeScriptTestFormGroup.get('payload').get('msgType').value, | ||
176 | + msg: this.nodeScriptTestFormGroup.get('payload').get('msg').value, | ||
177 | + metadata: this.nodeScriptTestFormGroup.get('metadata').value, | ||
178 | + script: this.nodeScriptTestFormGroup.get('script').value | ||
179 | + }; | ||
180 | + return this.ruleChainService.testScript(inputParams).pipe( | ||
181 | + mergeMap((result) => { | ||
182 | + if (result.error) { | ||
183 | + this.store.dispatch(new ActionNotificationShow( | ||
184 | + { | ||
185 | + message: result.error, | ||
186 | + type: 'error' | ||
187 | + })); | ||
188 | + return NEVER; | ||
189 | + } else { | ||
190 | + return of(result.output); | ||
191 | + } | ||
192 | + }) | ||
193 | + ); | ||
194 | + } else { | ||
195 | + return NEVER; | ||
196 | + } | ||
197 | + } | ||
198 | + | ||
199 | + private checkInputParamErrors(): boolean { | ||
200 | + this.payloadContent.validateOnSubmit(); | ||
201 | + if (!this.nodeScriptTestFormGroup.get('payload').valid) { | ||
202 | + return false; | ||
203 | + } | ||
204 | + return true; | ||
205 | + } | ||
206 | + | ||
157 | save(): void { | 207 | save(): void { |
158 | this.submitted = true; | 208 | this.submitted = true; |
159 | - const script: string = this.nodeScriptTestFormGroup.get('funcBody').value; | ||
160 | - this.dialogRef.close(script); | 209 | + this.testNodeScript().subscribe(() => { |
210 | + this.nodeScriptTestFormGroup.get('script').markAsPristine(); | ||
211 | + this.dialogRef.close(this.nodeScriptTestFormGroup.get('script').value); | ||
212 | + }); | ||
161 | } | 213 | } |
162 | } | 214 | } |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 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 | +<div style="background: #fff;" [ngClass]="{'fill-height': fillHeight}" | ||
19 | + tb-fullscreen | ||
20 | + [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column"> | ||
21 | + <div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-json-content-toolbar"> | ||
22 | + <label class="tb-title no-padding">{{ label }}</label> | ||
23 | + <span fxFlex></span> | ||
24 | + <button type="button" | ||
25 | + mat-button *ngIf="!readonly" class="tidy" (click)="beautifyJson()"> | ||
26 | + {{'js-func.tidy' | translate }} | ||
27 | + </button> | ||
28 | + <button type='button' mat-button mat-icon-button (click)="fullscreen = !fullscreen" | ||
29 | + class="tb-mat-32" | ||
30 | + matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" | ||
31 | + matTooltipPosition="above"> | ||
32 | + <mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> | ||
33 | + </button> | ||
34 | + </div> | ||
35 | + <div id="tb-json-panel" tb-toast toastTarget="jsonContentEditor" | ||
36 | + class="tb-json-content-panel" fxLayout="column"> | ||
37 | + <div #jsonEditor id="tb-json-input" [ngStyle]="editorStyle" [ngClass]="{'fill-height': fillHeight}"></div> | ||
38 | + </div> | ||
39 | +</div> |
1 | +/** | ||
2 | + * Copyright © 2016-2019 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 | +:host { | ||
18 | + position: relative; | ||
19 | + | ||
20 | + .fill-height { | ||
21 | + height: 100%; | ||
22 | + } | ||
23 | +} | ||
24 | + | ||
25 | +.tb-json-content-toolbar { | ||
26 | + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { | ||
27 | + align-items: center; | ||
28 | + vertical-align: middle; | ||
29 | + min-width: 32px; | ||
30 | + min-height: 15px; | ||
31 | + padding: 4px; | ||
32 | + margin: 0; | ||
33 | + font-size: .8rem; | ||
34 | + line-height: 15px; | ||
35 | + color: #7b7b7b; | ||
36 | + background: rgba(220, 220, 220, .35); | ||
37 | + &:not(:last-child) { | ||
38 | + margin-right: 4px; | ||
39 | + } | ||
40 | + } | ||
41 | +} | ||
42 | + | ||
43 | +.tb-json-content-panel { | ||
44 | + height: 100%; | ||
45 | + margin-left: 15px; | ||
46 | + border: 1px solid #c0c0c0; | ||
47 | + | ||
48 | + #tb-json-input { | ||
49 | + width: 100%; | ||
50 | + min-width: 200px; | ||
51 | + height: 100%; | ||
52 | + | ||
53 | + &:not(.fill-height) { | ||
54 | + min-height: 200px; | ||
55 | + } | ||
56 | + } | ||
57 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 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 | + ElementRef, | ||
20 | + forwardRef, | ||
21 | + Input, | ||
22 | + OnChanges, | ||
23 | + OnInit, | ||
24 | + ViewChild, | ||
25 | + SimpleChanges, | ||
26 | + OnDestroy | ||
27 | +} from '@angular/core'; | ||
28 | +import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; | ||
29 | +import * as ace from 'ace-builds'; | ||
30 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | ||
31 | +import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; | ||
32 | +import { Store } from '@ngrx/store'; | ||
33 | +import { AppState } from '@core/core.state'; | ||
34 | +import { ContentType, contentTypesMap } from '@shared/models/constants'; | ||
35 | +import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; | ||
36 | + | ||
37 | +@Component({ | ||
38 | + selector: 'tb-json-content', | ||
39 | + templateUrl: './json-content.component.html', | ||
40 | + styleUrls: ['./json-content.component.scss'], | ||
41 | + providers: [ | ||
42 | + { | ||
43 | + provide: NG_VALUE_ACCESSOR, | ||
44 | + useExisting: forwardRef(() => JsonContentComponent), | ||
45 | + multi: true | ||
46 | + }, | ||
47 | + { | ||
48 | + provide: NG_VALIDATORS, | ||
49 | + useExisting: forwardRef(() => JsonContentComponent), | ||
50 | + multi: true, | ||
51 | + } | ||
52 | + ] | ||
53 | +}) | ||
54 | +export class JsonContentComponent implements OnInit, ControlValueAccessor, Validator, OnChanges, OnDestroy { | ||
55 | + | ||
56 | + @ViewChild('jsonEditor', {static: true}) | ||
57 | + jsonEditorElmRef: ElementRef; | ||
58 | + | ||
59 | + private jsonEditor: ace.Ace.Editor; | ||
60 | + private editorsResizeCaf: CancelAnimationFrame; | ||
61 | + private editorResizeListener: any; | ||
62 | + | ||
63 | + @Input() label: string; | ||
64 | + | ||
65 | + @Input() contentType: ContentType; | ||
66 | + | ||
67 | + @Input() disabled: boolean; | ||
68 | + | ||
69 | + @Input() fillHeight: boolean; | ||
70 | + | ||
71 | + @Input() editorStyle: {[klass: string]: any}; | ||
72 | + | ||
73 | + private readonlyValue: boolean; | ||
74 | + get readonly(): boolean { | ||
75 | + return this.readonlyValue; | ||
76 | + } | ||
77 | + @Input() | ||
78 | + set readonly(value: boolean) { | ||
79 | + this.readonlyValue = coerceBooleanProperty(value); | ||
80 | + } | ||
81 | + | ||
82 | + private validateContentValue: boolean; | ||
83 | + get validateContent(): boolean { | ||
84 | + return this.validateContentValue; | ||
85 | + } | ||
86 | + @Input() | ||
87 | + set validateContent(value: boolean) { | ||
88 | + this.validateContentValue = coerceBooleanProperty(value); | ||
89 | + } | ||
90 | + | ||
91 | + fullscreen = false; | ||
92 | + | ||
93 | + contentBody: string; | ||
94 | + | ||
95 | + contentValid: boolean; | ||
96 | + | ||
97 | + validationError: string; | ||
98 | + | ||
99 | + errorShowed = false; | ||
100 | + | ||
101 | + private propagateChange = null; | ||
102 | + | ||
103 | + constructor(public elementRef: ElementRef, | ||
104 | + protected store: Store<AppState>, | ||
105 | + private raf: RafService) { | ||
106 | + } | ||
107 | + | ||
108 | + ngOnInit(): void { | ||
109 | + const editorElement = this.jsonEditorElmRef.nativeElement; | ||
110 | + let mode = 'text'; | ||
111 | + if (this.contentType) { | ||
112 | + mode = contentTypesMap.get(this.contentType).code; | ||
113 | + } | ||
114 | + let editorOptions: Partial<ace.Ace.EditorOptions> = { | ||
115 | + mode: `ace/mode/${mode}`, | ||
116 | + theme: 'ace/theme/github', | ||
117 | + showGutter: true, | ||
118 | + showPrintMargin: false, | ||
119 | + readOnly: this.readonly | ||
120 | + }; | ||
121 | + | ||
122 | + const advancedOptions = { | ||
123 | + enableSnippets: true, | ||
124 | + enableBasicAutocompletion: true, | ||
125 | + enableLiveAutocompletion: true | ||
126 | + }; | ||
127 | + | ||
128 | + editorOptions = {...editorOptions, ...advancedOptions}; | ||
129 | + this.jsonEditor = ace.edit(editorElement, editorOptions); | ||
130 | + this.jsonEditor.session.setUseWrapMode(true); | ||
131 | + this.jsonEditor.setValue(this.contentBody ? this.contentBody : '', -1); | ||
132 | + this.jsonEditor.on('change', () => { | ||
133 | + this.cleanupJsonErrors(); | ||
134 | + this.updateView(); | ||
135 | + }); | ||
136 | + this.editorResizeListener = this.onAceEditorResize.bind(this); | ||
137 | + // @ts-ignore | ||
138 | + addResizeListener(editorElement, this.editorResizeListener); | ||
139 | + } | ||
140 | + | ||
141 | + ngOnDestroy(): void { | ||
142 | + if (this.editorResizeListener) { | ||
143 | + const editorElement = this.jsonEditorElmRef.nativeElement; | ||
144 | + // @ts-ignore | ||
145 | + removeResizeListener(editorElement, this.editorResizeListener); | ||
146 | + } | ||
147 | + } | ||
148 | + | ||
149 | + private onAceEditorResize() { | ||
150 | + if (this.editorsResizeCaf) { | ||
151 | + this.editorsResizeCaf(); | ||
152 | + this.editorsResizeCaf = null; | ||
153 | + } | ||
154 | + this.editorsResizeCaf = this.raf.raf(() => { | ||
155 | + this.jsonEditor.resize(); | ||
156 | + this.jsonEditor.renderer.updateFull(); | ||
157 | + }); | ||
158 | + } | ||
159 | + | ||
160 | + ngOnChanges(changes: SimpleChanges): void { | ||
161 | + for (const propName of Object.keys(changes)) { | ||
162 | + const change = changes[propName]; | ||
163 | + if (!change.firstChange && change.currentValue !== change.previousValue) { | ||
164 | + if (propName === 'contentType') { | ||
165 | + if (this.jsonEditor) { | ||
166 | + let mode = 'text'; | ||
167 | + if (this.contentType) { | ||
168 | + mode = contentTypesMap.get(this.contentType).code; | ||
169 | + } | ||
170 | + this.jsonEditor.session.setMode(`ace/mode/${mode}`); | ||
171 | + } | ||
172 | + } | ||
173 | + } | ||
174 | + } | ||
175 | + } | ||
176 | + | ||
177 | + registerOnChange(fn: any): void { | ||
178 | + this.propagateChange = fn; | ||
179 | + } | ||
180 | + | ||
181 | + registerOnTouched(fn: any): void { | ||
182 | + } | ||
183 | + | ||
184 | + setDisabledState(isDisabled: boolean): void { | ||
185 | + this.disabled = isDisabled; | ||
186 | + } | ||
187 | + | ||
188 | + public validate(c: FormControl) { | ||
189 | + return (this.contentValid) ? null : { | ||
190 | + contentBody: { | ||
191 | + valid: false, | ||
192 | + }, | ||
193 | + }; | ||
194 | + } | ||
195 | + | ||
196 | + validateOnSubmit(): void { | ||
197 | + if (!this.readonly) { | ||
198 | + this.cleanupJsonErrors(); | ||
199 | + this.contentValid = true; | ||
200 | + this.propagateChange(this.contentBody); | ||
201 | + this.contentValid = this.doValidate(); | ||
202 | + this.propagateChange(this.contentBody); | ||
203 | + } | ||
204 | + } | ||
205 | + | ||
206 | + private doValidate(): boolean { | ||
207 | + try { | ||
208 | + if (this.validateContent && this.contentType === ContentType.JSON) { | ||
209 | + JSON.parse(this.contentBody); | ||
210 | + } | ||
211 | + return true; | ||
212 | + } catch (ex) { | ||
213 | + let errorInfo = 'Error:'; | ||
214 | + if (ex.name) { | ||
215 | + errorInfo += ' ' + ex.name + ':'; | ||
216 | + } | ||
217 | + if (ex.message) { | ||
218 | + errorInfo += ' ' + ex.message; | ||
219 | + } | ||
220 | + this.store.dispatch(new ActionNotificationShow( | ||
221 | + { | ||
222 | + message: errorInfo, | ||
223 | + type: 'error', | ||
224 | + target: 'jsonContentEditor', | ||
225 | + verticalPosition: 'bottom', | ||
226 | + horizontalPosition: 'left' | ||
227 | + })); | ||
228 | + this.errorShowed = true; | ||
229 | + return false; | ||
230 | + } | ||
231 | + } | ||
232 | + | ||
233 | + cleanupJsonErrors(): void { | ||
234 | + if (this.errorShowed) { | ||
235 | + this.store.dispatch(new ActionNotificationHide( | ||
236 | + { | ||
237 | + target: 'jsonContentEditor' | ||
238 | + })); | ||
239 | + this.errorShowed = false; | ||
240 | + } | ||
241 | + } | ||
242 | + | ||
243 | + writeValue(value: string): void { | ||
244 | + this.contentBody = value; | ||
245 | + this.contentValid = true; | ||
246 | + if (this.jsonEditor) { | ||
247 | + this.jsonEditor.setValue(this.contentBody ? this.contentBody : '', -1); | ||
248 | + // this.jsonEditor. | ||
249 | + } | ||
250 | + } | ||
251 | + | ||
252 | + updateView() { | ||
253 | + const editorValue = this.jsonEditor.getValue(); | ||
254 | + if (this.contentBody !== editorValue) { | ||
255 | + this.contentBody = editorValue; | ||
256 | + this.contentValid = true; | ||
257 | + this.propagateChange(this.contentBody); | ||
258 | + } | ||
259 | + } | ||
260 | + | ||
261 | + beautifyJson() { | ||
262 | + const res = js_beautify(this.contentBody, {indent_size: 4, wrap_line_length: 60}); | ||
263 | + this.jsonEditor.setValue(res ? res : '', -1); | ||
264 | + this.updateView(); | ||
265 | + } | ||
266 | + | ||
267 | + onFullscreen() { | ||
268 | + if (this.jsonEditor) { | ||
269 | + setTimeout(() => { | ||
270 | + this.jsonEditor.resize(); | ||
271 | + }, 0); | ||
272 | + } | ||
273 | + } | ||
274 | + | ||
275 | +} |
@@ -19,7 +19,7 @@ import { | @@ -19,7 +19,7 @@ import { | ||
19 | Component, | 19 | Component, |
20 | ElementRef, | 20 | ElementRef, |
21 | forwardRef, | 21 | forwardRef, |
22 | - Input, | 22 | + Input, OnDestroy, |
23 | OnInit, | 23 | OnInit, |
24 | ViewChild | 24 | ViewChild |
25 | } from '@angular/core'; | 25 | } from '@angular/core'; |
@@ -29,6 +29,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; | @@ -29,6 +29,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; | ||
29 | import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; | 29 | import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; |
30 | import { Store } from '@ngrx/store'; | 30 | import { Store } from '@ngrx/store'; |
31 | import { AppState } from '@core/core.state'; | 31 | import { AppState } from '@core/core.state'; |
32 | +import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; | ||
32 | 33 | ||
33 | @Component({ | 34 | @Component({ |
34 | selector: 'tb-json-object-edit', | 35 | selector: 'tb-json-object-edit', |
@@ -47,12 +48,14 @@ import { AppState } from '@core/core.state'; | @@ -47,12 +48,14 @@ import { AppState } from '@core/core.state'; | ||
47 | } | 48 | } |
48 | ] | 49 | ] |
49 | }) | 50 | }) |
50 | -export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Validator { | 51 | +export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Validator, OnDestroy { |
51 | 52 | ||
52 | @ViewChild('jsonEditor', {static: true}) | 53 | @ViewChild('jsonEditor', {static: true}) |
53 | jsonEditorElmRef: ElementRef; | 54 | jsonEditorElmRef: ElementRef; |
54 | 55 | ||
55 | private jsonEditor: ace.Ace.Editor; | 56 | private jsonEditor: ace.Ace.Editor; |
57 | + private editorsResizeCaf: CancelAnimationFrame; | ||
58 | + private editorResizeListener: any; | ||
56 | 59 | ||
57 | @Input() label: string; | 60 | @Input() label: string; |
58 | 61 | ||
@@ -95,7 +98,8 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va | @@ -95,7 +98,8 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va | ||
95 | private propagateChange = null; | 98 | private propagateChange = null; |
96 | 99 | ||
97 | constructor(public elementRef: ElementRef, | 100 | constructor(public elementRef: ElementRef, |
98 | - protected store: Store<AppState>) { | 101 | + protected store: Store<AppState>, |
102 | + private raf: RafService) { | ||
99 | } | 103 | } |
100 | 104 | ||
101 | ngOnInit(): void { | 105 | ngOnInit(): void { |
@@ -122,6 +126,28 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va | @@ -122,6 +126,28 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va | ||
122 | this.cleanupJsonErrors(); | 126 | this.cleanupJsonErrors(); |
123 | this.updateView(); | 127 | this.updateView(); |
124 | }); | 128 | }); |
129 | + this.editorResizeListener = this.onAceEditorResize.bind(this); | ||
130 | + // @ts-ignore | ||
131 | + addResizeListener(editorElement, this.editorResizeListener); | ||
132 | + } | ||
133 | + | ||
134 | + ngOnDestroy(): void { | ||
135 | + if (this.editorResizeListener) { | ||
136 | + const editorElement = this.jsonEditorElmRef.nativeElement; | ||
137 | + // @ts-ignore | ||
138 | + removeResizeListener(editorElement, this.editorResizeListener); | ||
139 | + } | ||
140 | + } | ||
141 | + | ||
142 | + private onAceEditorResize() { | ||
143 | + if (this.editorsResizeCaf) { | ||
144 | + this.editorsResizeCaf(); | ||
145 | + this.editorsResizeCaf = null; | ||
146 | + } | ||
147 | + this.editorsResizeCaf = this.raf.raf(() => { | ||
148 | + this.jsonEditor.resize(); | ||
149 | + this.jsonEditor.renderer.updateFull(); | ||
150 | + }); | ||
125 | } | 151 | } |
126 | 152 | ||
127 | registerOnChange(fn: any): void { | 153 | registerOnChange(fn: any): void { |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 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 | + | ||
19 | +<section fxLayout="column" class="tb-kv-map" [formGroup]="kvListFormGroup"> | ||
20 | + <label translate class="tb-title no-padding">{{ titleText }}</label> | ||
21 | + <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" style="max-height: 40px;" | ||
22 | + formArrayName="keyVals" | ||
23 | + *ngFor="let keyValControl of keyValsFormArray().controls; let $index = index"> | ||
24 | + <mat-form-field fxFlex floatLabel="always" hideRequiredMarker class="mat-block" | ||
25 | + style="max-height: 40px;"> | ||
26 | + <mat-label></mat-label> | ||
27 | + <input [formControl]="keyValControl.get('key')" matInput required | ||
28 | + placeholder="{{ (keyPlaceholderText ? keyPlaceholderText : 'key-val.key') | translate }}"/> | ||
29 | + </mat-form-field> | ||
30 | + <mat-form-field fxFlex floatLabel="always" hideRequiredMarker class="mat-block" | ||
31 | + style="max-height: 40px;"> | ||
32 | + <mat-label></mat-label> | ||
33 | + <input [formControl]="keyValControl.get('value')" matInput required | ||
34 | + placeholder="{{ (valuePlaceholderText ? valuePlaceholderText : 'key-val.value') | translate }}"/> | ||
35 | + </mat-form-field> | ||
36 | + <button mat-button mat-icon-button color="primary" | ||
37 | + [fxShow]="!disabled" | ||
38 | + type="button" | ||
39 | + (click)="removeKeyVal($index)" | ||
40 | + [disabled]="isLoading$ | async" | ||
41 | + matTooltip="{{ 'key-val.remove-entry' | translate }}" | ||
42 | + matTooltipPosition="above"> | ||
43 | + <mat-icon>close</mat-icon> | ||
44 | + </button> | ||
45 | + </div> | ||
46 | + <span [fxShow]="!keyValsFormArray().length" | ||
47 | + fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}" | ||
48 | + class="no-data-found" translate>{{noDataText ? noDataText : 'key-val.no-data'}}</span> | ||
49 | + <div style="margin-top: 8px;"> | ||
50 | + <button mat-button mat-raised-button color="primary" | ||
51 | + [fxShow]="!disabled" | ||
52 | + [disabled]="isLoading$ | async" | ||
53 | + (click)="addKeyVal()" | ||
54 | + type="button" | ||
55 | + matTooltip="{{ 'key-val.add-entry' | translate }}" | ||
56 | + matTooltipPosition="above"> | ||
57 | + {{ 'action.add' | translate }} | ||
58 | + </button> | ||
59 | + </div> | ||
60 | +</section> |
1 | +/** | ||
2 | + * Copyright © 2016-2019 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 | + .tb-kv-map { | ||
18 | + span.no-data-found { | ||
19 | + position: relative; | ||
20 | + display: flex; | ||
21 | + height: 40px; | ||
22 | + text-transform: uppercase; | ||
23 | + | ||
24 | + &.disabled { | ||
25 | + color: rgba(0, 0, 0, .38); | ||
26 | + } | ||
27 | + } | ||
28 | + } | ||
29 | +} | ||
30 | + | ||
31 | +:host ::ng-deep { | ||
32 | + .mat-form-field-wrapper { | ||
33 | + padding-bottom: 0; | ||
34 | + } | ||
35 | + .mat-form-field-infix { | ||
36 | + border-top: 0; | ||
37 | + } | ||
38 | + .mat-form-field-underline { | ||
39 | + bottom: 0; | ||
40 | + } | ||
41 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 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 { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; | ||
18 | +import { | ||
19 | + AbstractControl, | ||
20 | + ControlValueAccessor, FormArray, | ||
21 | + FormBuilder, FormControl, | ||
22 | + FormGroup, NG_VALIDATORS, | ||
23 | + NG_VALUE_ACCESSOR, Validator, | ||
24 | + ValidatorFn, | ||
25 | + Validators | ||
26 | +} from '@angular/forms'; | ||
27 | +import { AliasFilterType, aliasFilterTypeTranslationMap, EntityAliasFilter } from '@shared/models/alias.models'; | ||
28 | +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; | ||
29 | +import { TranslateService } from '@ngx-translate/core'; | ||
30 | +import { EntityService } from '@core/http/entity.service'; | ||
31 | +import { EntitySearchDirection, entitySearchDirectionTranslations, EntityTypeFilter } from '@shared/models/relation.models'; | ||
32 | +import { PageComponent } from '@shared/components/page.component'; | ||
33 | +import { Store } from '@ngrx/store'; | ||
34 | +import { AppState } from '@core/core.state'; | ||
35 | +import { Subscription } from 'rxjs'; | ||
36 | + | ||
37 | +@Component({ | ||
38 | + selector: 'tb-key-val-map', | ||
39 | + templateUrl: './kv-map.component.html', | ||
40 | + styleUrls: ['./kv-map.component.scss'], | ||
41 | + providers: [ | ||
42 | + { | ||
43 | + provide: NG_VALUE_ACCESSOR, | ||
44 | + useExisting: forwardRef(() => KeyValMapComponent), | ||
45 | + multi: true | ||
46 | + }, | ||
47 | + { | ||
48 | + provide: NG_VALIDATORS, | ||
49 | + useExisting: forwardRef(() => KeyValMapComponent), | ||
50 | + multi: true, | ||
51 | + } | ||
52 | + ] | ||
53 | +}) | ||
54 | +export class KeyValMapComponent extends PageComponent implements ControlValueAccessor, OnInit, Validator { | ||
55 | + | ||
56 | + @Input() disabled: boolean; | ||
57 | + | ||
58 | + @Input() titleText: string; | ||
59 | + | ||
60 | + @Input() keyPlaceholderText: string; | ||
61 | + | ||
62 | + @Input() valuePlaceholderText: string; | ||
63 | + | ||
64 | + @Input() noDataText: string; | ||
65 | + | ||
66 | + kvListFormGroup: FormGroup; | ||
67 | + | ||
68 | + private propagateChange = null; | ||
69 | + | ||
70 | + private valueChangeSubscription: Subscription = null; | ||
71 | + | ||
72 | + constructor(protected store: Store<AppState>, | ||
73 | + private fb: FormBuilder) { | ||
74 | + super(store); | ||
75 | + } | ||
76 | + | ||
77 | + ngOnInit(): void { | ||
78 | + this.kvListFormGroup = this.fb.group({}); | ||
79 | + this.kvListFormGroup.addControl('keyVals', | ||
80 | + this.fb.array([])); | ||
81 | + } | ||
82 | + | ||
83 | + keyValsFormArray(): FormArray { | ||
84 | + return this.kvListFormGroup.get('keyVals') as FormArray; | ||
85 | + } | ||
86 | + | ||
87 | + registerOnChange(fn: any): void { | ||
88 | + this.propagateChange = fn; | ||
89 | + } | ||
90 | + | ||
91 | + registerOnTouched(fn: any): void { | ||
92 | + } | ||
93 | + | ||
94 | + setDisabledState?(isDisabled: boolean): void { | ||
95 | + this.disabled = isDisabled; | ||
96 | + if (this.disabled) { | ||
97 | + this.kvListFormGroup.disable({emitEvent: false}); | ||
98 | + } else { | ||
99 | + this.kvListFormGroup.enable({emitEvent: false}); | ||
100 | + } | ||
101 | + } | ||
102 | + | ||
103 | + writeValue(keyValMap: {[key: string]: string}): void { | ||
104 | + if (this.valueChangeSubscription) { | ||
105 | + this.valueChangeSubscription.unsubscribe(); | ||
106 | + } | ||
107 | + const keyValsControls: Array<AbstractControl> = []; | ||
108 | + if (keyValMap) { | ||
109 | + for (const property of Object.keys(keyValMap)) { | ||
110 | + if (Object.prototype.hasOwnProperty.call(keyValMap, property)) { | ||
111 | + keyValsControls.push(this.fb.group({ | ||
112 | + key: [property, [Validators.required]], | ||
113 | + value: [keyValMap[property], [Validators.required]] | ||
114 | + })); | ||
115 | + } | ||
116 | + } | ||
117 | + } | ||
118 | + this.kvListFormGroup.setControl('keyVals', this.fb.array(keyValsControls)); | ||
119 | + this.valueChangeSubscription = this.kvListFormGroup.valueChanges.subscribe(() => { | ||
120 | + this.updateModel(); | ||
121 | + }); | ||
122 | + } | ||
123 | + | ||
124 | + public removeKeyVal(index: number) { | ||
125 | + (this.kvListFormGroup.get('keyVals') as FormArray).removeAt(index); | ||
126 | + } | ||
127 | + | ||
128 | + public addKeyVal() { | ||
129 | + const keyValsFormArray = this.kvListFormGroup.get('keyVals') as FormArray; | ||
130 | + keyValsFormArray.push(this.fb.group({ | ||
131 | + key: ['', [Validators.required]], | ||
132 | + value: ['', [Validators.required]] | ||
133 | + })); | ||
134 | + } | ||
135 | + | ||
136 | + public validate(c: FormControl) { | ||
137 | + const kvList: {key: string; value: string}[] = this.kvListFormGroup.get('keyVals').value; | ||
138 | + let valid = true; | ||
139 | + for (const entry of kvList) { | ||
140 | + if (!entry.key || !entry.value) { | ||
141 | + valid = false; | ||
142 | + break; | ||
143 | + } | ||
144 | + } | ||
145 | + return (valid) ? null : { | ||
146 | + keyVals: { | ||
147 | + valid: false, | ||
148 | + }, | ||
149 | + }; | ||
150 | + } | ||
151 | + | ||
152 | + private updateModel() { | ||
153 | + const kvList: {key: string; value: string}[] = this.kvListFormGroup.get('keyVals').value; | ||
154 | + const keyValMap: {[key: string]: string} = {}; | ||
155 | + kvList.forEach((entry) => { | ||
156 | + keyValMap[entry.key] = entry.value; | ||
157 | + }); | ||
158 | + this.propagateChange(keyValMap); | ||
159 | + } | ||
160 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 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 | +<mat-form-field [formGroup]="messageTypeFormGroup" class="mat-block"> | ||
19 | + <mat-label>{{ 'rulenode.message-type' | translate }}</mat-label> | ||
20 | + <input matInput type="text" placeholder="{{ 'rulenode.select-message-type' | translate }}" | ||
21 | + #messageTypeInput | ||
22 | + formControlName="messageType" | ||
23 | + (focusin)="onFocus()" | ||
24 | + [required]="required" | ||
25 | + [matAutocomplete]="messageTypeAutocomplete"> | ||
26 | + <button *ngIf="messageTypeFormGroup.get('messageType').value && !disabled" | ||
27 | + type="button" | ||
28 | + matSuffix mat-button mat-icon-button aria-label="Clear" | ||
29 | + (click)="clear()"> | ||
30 | + <mat-icon class="material-icons">close</mat-icon> | ||
31 | + </button> | ||
32 | + <mat-autocomplete | ||
33 | + class="tb-autocomplete" | ||
34 | + #messageTypeAutocomplete="matAutocomplete" | ||
35 | + [displayWith]="displayMessageTypeFn"> | ||
36 | + <mat-option *ngFor="let messageType of filteredMessageTypes | async" [value]="messageType"> | ||
37 | + <span [innerHTML]="displayMessageTypeFn(messageType) | highlight:searchText"></span> | ||
38 | + </mat-option> | ||
39 | + </mat-autocomplete> | ||
40 | + <mat-error *ngIf="messageTypeFormGroup.get('messageType').hasError('required')"> | ||
41 | + {{ 'rulenode.message-type-required' | translate }} | ||
42 | + </mat-error> | ||
43 | +</mat-form-field> |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 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 { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; | ||
18 | +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; | ||
19 | +import { Observable, of } from 'rxjs'; | ||
20 | +import { map, mergeMap, startWith, tap } from 'rxjs/operators'; | ||
21 | +import { Store } from '@ngrx/store'; | ||
22 | +import { AppState } from '@app/core/core.state'; | ||
23 | +import { TranslateService } from '@ngx-translate/core'; | ||
24 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | ||
25 | +import { MessageType, messageTypeNames } from '@shared/models/rule-node.models'; | ||
26 | + | ||
27 | +@Component({ | ||
28 | + selector: 'tb-message-type-autocomplete', | ||
29 | + templateUrl: './message-type-autocomplete.component.html', | ||
30 | + styleUrls: [], | ||
31 | + providers: [{ | ||
32 | + provide: NG_VALUE_ACCESSOR, | ||
33 | + useExisting: forwardRef(() => MessageTypeAutocompleteComponent), | ||
34 | + multi: true | ||
35 | + }] | ||
36 | +}) | ||
37 | +export class MessageTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { | ||
38 | + | ||
39 | + messageTypeFormGroup: FormGroup; | ||
40 | + | ||
41 | + modelValue: string | null; | ||
42 | + | ||
43 | + private requiredValue: boolean; | ||
44 | + get required(): boolean { | ||
45 | + return this.requiredValue; | ||
46 | + } | ||
47 | + @Input() | ||
48 | + set required(value: boolean) { | ||
49 | + this.requiredValue = coerceBooleanProperty(value); | ||
50 | + } | ||
51 | + | ||
52 | + @Input() | ||
53 | + disabled: boolean; | ||
54 | + | ||
55 | + @ViewChild('messageTypeInput', {static: true}) messageTypeInput: ElementRef; | ||
56 | + | ||
57 | + filteredMessageTypes: Observable<Array<MessageType | string>>; | ||
58 | + | ||
59 | + searchText = ''; | ||
60 | + | ||
61 | + private dirty = false; | ||
62 | + | ||
63 | + private propagateChange = (v: any) => { }; | ||
64 | + | ||
65 | + constructor(private store: Store<AppState>, | ||
66 | + public translate: TranslateService, | ||
67 | + private fb: FormBuilder) { | ||
68 | + this.messageTypeFormGroup = this.fb.group({ | ||
69 | + messageType: [null] | ||
70 | + }); | ||
71 | + } | ||
72 | + | ||
73 | + registerOnChange(fn: any): void { | ||
74 | + this.propagateChange = fn; | ||
75 | + } | ||
76 | + | ||
77 | + registerOnTouched(fn: any): void { | ||
78 | + } | ||
79 | + | ||
80 | + ngOnInit() { | ||
81 | + this.filteredMessageTypes = this.messageTypeFormGroup.get('messageType').valueChanges | ||
82 | + .pipe( | ||
83 | + tap(value => { | ||
84 | + this.updateView(value); | ||
85 | + }), | ||
86 | + startWith<string | MessageType>(''), | ||
87 | + map(value => value ? value : ''), | ||
88 | + mergeMap(messageType => this.fetchMessageTypes(messageType) ) | ||
89 | + ); | ||
90 | + } | ||
91 | + | ||
92 | + ngAfterViewInit(): void { | ||
93 | + } | ||
94 | + | ||
95 | + ngOnDestroy(): void { | ||
96 | + } | ||
97 | + | ||
98 | + setDisabledState(isDisabled: boolean): void { | ||
99 | + this.disabled = isDisabled; | ||
100 | + if (this.disabled) { | ||
101 | + this.messageTypeFormGroup.disable({emitEvent: false}); | ||
102 | + } else { | ||
103 | + this.messageTypeFormGroup.enable({emitEvent: false}); | ||
104 | + } | ||
105 | + } | ||
106 | + | ||
107 | + writeValue(value: string | null): void { | ||
108 | + this.searchText = ''; | ||
109 | + this.modelValue = value; | ||
110 | + let res: MessageType | string = null; | ||
111 | + if (value) { | ||
112 | + if (Object.values(MessageType).includes(value)) { | ||
113 | + res = MessageType[value]; | ||
114 | + } else { | ||
115 | + res = value; | ||
116 | + } | ||
117 | + } | ||
118 | + this.messageTypeFormGroup.get('messageType').patchValue(res, {emitEvent: false}); | ||
119 | + this.dirty = true; | ||
120 | + } | ||
121 | + | ||
122 | + onFocus() { | ||
123 | + if (this.dirty) { | ||
124 | + this.messageTypeFormGroup.get('messageType').updateValueAndValidity({onlySelf: true, emitEvent: true}); | ||
125 | + this.dirty = false; | ||
126 | + } | ||
127 | + } | ||
128 | + | ||
129 | + updateView(value: MessageType | string | null) { | ||
130 | + let res: string = null; | ||
131 | + if (value) { | ||
132 | + if (Object.values(MessageType).includes(value)) { | ||
133 | + res = MessageType[value]; | ||
134 | + } else { | ||
135 | + res = value; | ||
136 | + } | ||
137 | + } | ||
138 | + if (this.modelValue !== res) { | ||
139 | + this.modelValue = res; | ||
140 | + this.propagateChange(this.modelValue); | ||
141 | + } | ||
142 | + } | ||
143 | + | ||
144 | + displayMessageTypeFn(messageType?: MessageType | string): string | undefined { | ||
145 | + if (messageType) { | ||
146 | + if (Object.values(MessageType).includes(messageType)) { | ||
147 | + return messageTypeNames.get(MessageType[messageType]); | ||
148 | + } else { | ||
149 | + return messageType; | ||
150 | + } | ||
151 | + } | ||
152 | + return undefined; | ||
153 | + } | ||
154 | + | ||
155 | + fetchMessageTypes(searchText?: string): Observable<Array<MessageType | string>> { | ||
156 | + this.searchText = searchText; | ||
157 | + const result: Array<MessageType | string> = []; | ||
158 | + messageTypeNames.forEach((value, key) => { | ||
159 | + if (value.toUpperCase().includes(searchText.toUpperCase())) { | ||
160 | + result.push(key); | ||
161 | + } | ||
162 | + }); | ||
163 | + if (result.length) { | ||
164 | + return of(result); | ||
165 | + } else { | ||
166 | + return of([searchText]); | ||
167 | + } | ||
168 | + } | ||
169 | + | ||
170 | + clear() { | ||
171 | + this.messageTypeFormGroup.get('messageType').patchValue(null, {emitEvent: true}); | ||
172 | + setTimeout(() => { | ||
173 | + this.messageTypeInput.nativeElement.blur(); | ||
174 | + this.messageTypeInput.nativeElement.focus(); | ||
175 | + }, 0); | ||
176 | + } | ||
177 | + | ||
178 | +} |
@@ -156,4 +156,41 @@ export const valueTypesMap = new Map<ValueType, ValueTypeData>( | @@ -156,4 +156,41 @@ export const valueTypesMap = new Map<ValueType, ValueTypeData>( | ||
156 | ] | 156 | ] |
157 | ); | 157 | ); |
158 | 158 | ||
159 | +export interface ContentTypeData { | ||
160 | + name: string; | ||
161 | + code: string; | ||
162 | +} | ||
163 | + | ||
164 | +export enum ContentType { | ||
165 | + JSON = 'JSON', | ||
166 | + TEXT = 'TEXT', | ||
167 | + BINARY = 'BINARY' | ||
168 | +} | ||
169 | + | ||
170 | +export const contentTypesMap = new Map<ContentType, ContentTypeData>( | ||
171 | + [ | ||
172 | + [ | ||
173 | + ContentType.JSON, | ||
174 | + { | ||
175 | + name: 'content-type.json', | ||
176 | + code: 'json' | ||
177 | + } | ||
178 | + ], | ||
179 | + [ | ||
180 | + ContentType.TEXT, | ||
181 | + { | ||
182 | + name: 'content-type.text', | ||
183 | + code: 'text' | ||
184 | + } | ||
185 | + ], | ||
186 | + [ | ||
187 | + ContentType.BINARY, | ||
188 | + { | ||
189 | + name: 'content-type.binary', | ||
190 | + code: 'text' | ||
191 | + } | ||
192 | + ] | ||
193 | + ] | ||
194 | +); | ||
195 | + | ||
159 | export const customTranslationsPrefix = 'custom.'; | 196 | export const customTranslationsPrefix = 'custom.'; |
@@ -232,6 +232,58 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor { | @@ -232,6 +232,58 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor { | ||
232 | configurationDescriptor?: RuleNodeConfigurationDescriptor; | 232 | configurationDescriptor?: RuleNodeConfigurationDescriptor; |
233 | } | 233 | } |
234 | 234 | ||
235 | +export interface TestScriptInputParams { | ||
236 | + script: string; | ||
237 | + scriptType: string; | ||
238 | + argNames: string[]; | ||
239 | + msg: string; | ||
240 | + metadata: {[key: string]: string}; | ||
241 | + msgType: string; | ||
242 | +} | ||
243 | + | ||
244 | +export interface TestScriptResult { | ||
245 | + output: string; | ||
246 | + error: string; | ||
247 | +} | ||
248 | + | ||
249 | +export enum MessageType { | ||
250 | + POST_ATTRIBUTES_REQUEST = 'POST_ATTRIBUTES_REQUEST', | ||
251 | + POST_TELEMETRY_REQUEST = 'POST_TELEMETRY_REQUEST', | ||
252 | + TO_SERVER_RPC_REQUEST = 'TO_SERVER_RPC_REQUEST', | ||
253 | + RPC_CALL_FROM_SERVER_TO_DEVICE = 'RPC_CALL_FROM_SERVER_TO_DEVICE', | ||
254 | + ACTIVITY_EVENT = 'ACTIVITY_EVENT', | ||
255 | + INACTIVITY_EVENT = 'INACTIVITY_EVENT', | ||
256 | + CONNECT_EVENT = 'CONNECT_EVENT', | ||
257 | + DISCONNECT_EVENT = 'DISCONNECT_EVENT', | ||
258 | + ENTITY_CREATED = 'ENTITY_CREATED', | ||
259 | + ENTITY_UPDATED = 'ENTITY_UPDATED', | ||
260 | + ENTITY_DELETED = 'ENTITY_DELETED', | ||
261 | + ENTITY_ASSIGNED = 'ENTITY_ASSIGNED', | ||
262 | + ENTITY_UNASSIGNED = 'ENTITY_UNASSIGNED', | ||
263 | + ATTRIBUTES_UPDATED = 'ATTRIBUTES_UPDATED', | ||
264 | + ATTRIBUTES_DELETED = 'ATTRIBUTES_DELETED' | ||
265 | +} | ||
266 | + | ||
267 | +export const messageTypeNames = new Map<MessageType, string>( | ||
268 | + [ | ||
269 | + [MessageType.POST_ATTRIBUTES_REQUEST, 'Post attributes'], | ||
270 | + [MessageType.POST_TELEMETRY_REQUEST, 'Post telemetry'], | ||
271 | + [MessageType.TO_SERVER_RPC_REQUEST, 'RPC Request from Device'], | ||
272 | + [MessageType.RPC_CALL_FROM_SERVER_TO_DEVICE, 'RPC Request to Device'], | ||
273 | + [MessageType.ACTIVITY_EVENT, 'Activity Event'], | ||
274 | + [MessageType.INACTIVITY_EVENT, 'Inactivity Event'], | ||
275 | + [MessageType.CONNECT_EVENT, 'Connect Event'], | ||
276 | + [MessageType.DISCONNECT_EVENT, 'Disconnect Event'], | ||
277 | + [MessageType.ENTITY_CREATED, 'Entity Created'], | ||
278 | + [MessageType.ENTITY_UPDATED, 'Entity Updated'], | ||
279 | + [MessageType.ENTITY_DELETED, 'Entity Deleted'], | ||
280 | + [MessageType.ENTITY_ASSIGNED, 'Entity Assigned'], | ||
281 | + [MessageType.ENTITY_UNASSIGNED, 'Entity Unassigned'], | ||
282 | + [MessageType.ATTRIBUTES_UPDATED, 'Attributes Updated'], | ||
283 | + [MessageType.ATTRIBUTES_DELETED, 'Attributes Deleted'] | ||
284 | + ] | ||
285 | +); | ||
286 | + | ||
235 | const ruleNodeClazzHelpLinkMap = { | 287 | const ruleNodeClazzHelpLinkMap = { |
236 | 'org.thingsboard.rule.engine.filter.TbCheckRelationNode': 'ruleNodeCheckRelation', | 288 | 'org.thingsboard.rule.engine.filter.TbCheckRelationNode': 'ruleNodeCheckRelation', |
237 | 'org.thingsboard.rule.engine.filter.TbCheckMessageNode': 'ruleNodeCheckExistenceFields', | 289 | 'org.thingsboard.rule.engine.filter.TbCheckMessageNode': 'ruleNodeCheckExistenceFields', |
@@ -115,6 +115,9 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se | @@ -115,6 +115,9 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se | ||
115 | import { ImageInputComponent } from './components/image-input.component'; | 115 | import { ImageInputComponent } from './components/image-input.component'; |
116 | import { FileInputComponent } from './components/file-input.component'; | 116 | import { FileInputComponent } from './components/file-input.component'; |
117 | import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-script-test-dialog.component'; | 117 | import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-script-test-dialog.component'; |
118 | +import { MessageTypeAutocompleteComponent } from './components/message-type-autocomplete.component'; | ||
119 | +import { JsonContentComponent } from './components/json-content.component'; | ||
120 | +import { KeyValMapComponent } from './components/kv-map.component'; | ||
118 | 121 | ||
119 | @NgModule({ | 122 | @NgModule({ |
120 | providers: [ | 123 | providers: [ |
@@ -175,6 +178,7 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc | @@ -175,6 +178,7 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc | ||
175 | RelationTypeAutocompleteComponent, | 178 | RelationTypeAutocompleteComponent, |
176 | SocialSharePanelComponent, | 179 | SocialSharePanelComponent, |
177 | JsonObjectEditComponent, | 180 | JsonObjectEditComponent, |
181 | + JsonContentComponent, | ||
178 | JsFuncComponent, | 182 | JsFuncComponent, |
179 | FabTriggerDirective, | 183 | FabTriggerDirective, |
180 | FabActionsDirective, | 184 | FabActionsDirective, |
@@ -188,6 +192,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc | @@ -188,6 +192,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc | ||
188 | JsonFormComponent, | 192 | JsonFormComponent, |
189 | ImageInputComponent, | 193 | ImageInputComponent, |
190 | FileInputComponent, | 194 | FileInputComponent, |
195 | + MessageTypeAutocompleteComponent, | ||
196 | + KeyValMapComponent, | ||
191 | NospacePipe, | 197 | NospacePipe, |
192 | MillisecondsToTimeStringPipe, | 198 | MillisecondsToTimeStringPipe, |
193 | EnumToArrayPipe, | 199 | EnumToArrayPipe, |
@@ -276,6 +282,7 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc | @@ -276,6 +282,7 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc | ||
276 | RelationTypeAutocompleteComponent, | 282 | RelationTypeAutocompleteComponent, |
277 | SocialSharePanelComponent, | 283 | SocialSharePanelComponent, |
278 | JsonObjectEditComponent, | 284 | JsonObjectEditComponent, |
285 | + JsonContentComponent, | ||
279 | JsFuncComponent, | 286 | JsFuncComponent, |
280 | FabTriggerDirective, | 287 | FabTriggerDirective, |
281 | FabActionsDirective, | 288 | FabActionsDirective, |
@@ -331,6 +338,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc | @@ -331,6 +338,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc | ||
331 | JsonFormComponent, | 338 | JsonFormComponent, |
332 | ImageInputComponent, | 339 | ImageInputComponent, |
333 | FileInputComponent, | 340 | FileInputComponent, |
341 | + MessageTypeAutocompleteComponent, | ||
342 | + KeyValMapComponent, | ||
334 | NospacePipe, | 343 | NospacePipe, |
335 | MillisecondsToTimeStringPipe, | 344 | MillisecondsToTimeStringPipe, |
336 | EnumToArrayPipe, | 345 | EnumToArrayPipe, |