Showing
7 changed files
with
362 additions
and
2 deletions
... | ... | @@ -455,6 +455,24 @@ |
455 | 455 | "dataKeySettingsSchema": "{}\n", |
456 | 456 | "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" |
457 | 457 | } |
458 | + }, | |
459 | + { | |
460 | + "alias": "update_json_attribute", | |
461 | + "name": "Update JSON attribute", | |
462 | + "image": "data:image/png;base64,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", | |
463 | + "description": "Simple form to input new JSON value for pre-defined attribute/timeseries key.", | |
464 | + "descriptor": { | |
465 | + "type": "latest", | |
466 | + "sizeX": 7.5, | |
467 | + "sizeY": 3, | |
468 | + "resources": [], | |
469 | + "templateHtml": "<tb-json-input-widget \n [ctx]=\"ctx\">\n</tb-json-input-widget>", | |
470 | + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", | |
471 | + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.jsonInputWidget.onDataUpdated();\n}\n\nself.onResize = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}", | |
472 | + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AdvancedSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"widgetMode\": {\n \"title\": \"Widget mode\",\n \"type\": \"string\",\n \"default\": \"ATTRIBUTE\"\n },\n \"attributeScope\": {\n \"title\": \"Attribute scope\",\n \"type\": \"string\",\n \"default\": \"SERVER_SCOPE\"\n },\n \"showLabel\":{\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"attributeRequired\": {\n \"title\": \"Value required\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"widgetMode\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"ATTRIBUTE\",\n \"label\": \"Update attribute\"\n },\n {\n \"value\": \"TIME_SERIES\",\n \"label\": \"Update timeseries\"\n }\n ]\n },\n {\n \"key\": \"attributeScope\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"condition\": \"model.widgetMode === 'ATTRIBUTE'\",\n \"items\": [\n {\n \"value\": \"SERVER_SCOPE\",\n \"label\": \"Server attribute\"\n },\n {\n \"value\": \"SHARED_SCOPE\",\n \"label\": \"Shared attribute\"\n }\n ]\n },\n \"showLabel\",\n {\n \"key\": \"labelValue\",\n \"condition\": \"model.showLabel\"\n },\n \"attributeRequired\",\n \"showResultMessage\"\n ]\n}", | |
473 | + "dataKeySettingsSchema": "{}", | |
474 | + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" | |
475 | + } | |
458 | 476 | } |
459 | 477 | ] |
460 | 478 | } | ... | ... |
... | ... | @@ -127,6 +127,10 @@ export function isEmpty(obj: any): boolean { |
127 | 127 | return true; |
128 | 128 | } |
129 | 129 | |
130 | +export function isLiteralObject(value: any) { | |
131 | + return (!!value) && (value.constructor === Object); | |
132 | +} | |
133 | + | |
130 | 134 | export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined { |
131 | 135 | if (isDefinedAndNotNull(value) && isNumeric(value) && |
132 | 136 | (isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) { | ... | ... |
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 | + | |
19 | +<div class="tb-json-input" tb-toast toastTarget="{{ toastTargetId }}"> | |
20 | + <form *ngIf="attributeUpdateFormGroup" | |
21 | + fxLayout="column" | |
22 | + class="tb-json-input__form" | |
23 | + [formGroup]="attributeUpdateFormGroup" | |
24 | + (ngSubmit)="save()"> | |
25 | + <div fxLayout="column" fxLayoutGap="10px" fxFlex *ngIf="entityDetected && isValidParameter && dataKeyDetected"> | |
26 | + <fieldset fxFlex> | |
27 | + <tb-json-object-edit | |
28 | + [editorStyle]="{minHeight: '100px'}" | |
29 | + fillHeight="true" | |
30 | + [required]="settings.attributeRequired" | |
31 | + label="{{ settings.showLabel ? labelValue : '' }}" | |
32 | + formControlName="currentValue" | |
33 | + (focusin)="isFocused = true;" | |
34 | + (focusout)="isFocused = false;" | |
35 | + ></tb-json-object-edit> | |
36 | + </fieldset> | |
37 | + <div class="tb-json-input-form__actions" fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="20px"> | |
38 | + <button mat-button color="primary" | |
39 | + type="button" | |
40 | + [disabled]="!attributeUpdateFormGroup.dirty" | |
41 | + (click)="discard()" | |
42 | + matTooltip="{{ 'widgets.input-widgets.discard-changes' | translate }}" | |
43 | + matTooltipPosition="above"> | |
44 | + {{ "action.undo" | translate }} | |
45 | + </button> | |
46 | + <button mat-button mat-raised-button color="primary" | |
47 | + type="submit" | |
48 | + [disabled]="attributeUpdateFormGroup.invalid || !attributeUpdateFormGroup.dirty"> | |
49 | + {{ "action.save" | translate }} | |
50 | + </button> | |
51 | + </div> | |
52 | + </div> | |
53 | + | |
54 | + <div fxLayout="column" fxLayoutAlign="center center" fxFlex *ngIf="!entityDetected || !dataKeyDetected || !isValidParameter"> | |
55 | + <div class="tb-json-input__error" | |
56 | + *ngIf="!entityDetected"> | |
57 | + {{ 'widgets.input-widgets.no-entity-selected' | translate }} | |
58 | + </div> | |
59 | + <div class="tb-json-input__error" | |
60 | + *ngIf="entityDetected && !dataKeyDetected"> | |
61 | + {{ 'widgets.input-widgets.no-datakey-selected' | translate }} | |
62 | + </div> | |
63 | + <div class="tb-json-input__error" | |
64 | + *ngIf="dataKeyDetected && !isValidParameter"> | |
65 | + {{ errorMessage | translate }} | |
66 | + </div> | |
67 | + </div> | |
68 | + </form> | |
69 | +</div> | ... | ... |
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 | +.tb-json-input { | |
18 | + width: 100%; | |
19 | + height: 100%; | |
20 | + padding: 5px; | |
21 | + | |
22 | + &__form { | |
23 | + overflow: auto; | |
24 | + height: 100%; | |
25 | + } | |
26 | + | |
27 | + &__error { | |
28 | + text-align: center; | |
29 | + font-size: 18px; | |
30 | + color: #a0a0a0; | |
31 | + } | |
32 | +} | |
33 | + | |
34 | +.tb-toast { | |
35 | + font-size: 14px!important; | |
36 | +} | ... | ... |
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 { Component, Input, OnInit } from '@angular/core'; | |
18 | +import { PageComponent } from '@shared/components/page.component'; | |
19 | +import { WidgetContext } from '@home/models/widget-component.models'; | |
20 | +import { Store } from '@ngrx/store'; | |
21 | +import { AppState } from '@core/core.state'; | |
22 | +import { UtilsService } from '@core/services/utils.service'; | |
23 | +import { TranslateService } from '@ngx-translate/core'; | |
24 | +import { Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models'; | |
25 | +import { IWidgetSubscription } from '@core/api/widget-api.models'; | |
26 | +import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; | |
27 | +import { AttributeService } from '@core/http/attribute.service'; | |
28 | +import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; | |
29 | +import { EntityId } from '@shared/models/id/entity-id'; | |
30 | +import { EntityType } from '@shared/models/entity-type.models'; | |
31 | +import { createLabelFromDatasource } from '@core/utils'; | |
32 | +import { Observable } from 'rxjs'; | |
33 | + | |
34 | +enum JsonInputWidgetMode { | |
35 | + ATTRIBUTE = 'ATTRIBUTE', | |
36 | + TIME_SERIES = 'TIME_SERIES', | |
37 | +} | |
38 | + | |
39 | +interface JsonInputWidgetSettings { | |
40 | + widgetTitle: string; | |
41 | + widgetMode: JsonInputWidgetMode; | |
42 | + attributeScope?: AttributeScope; | |
43 | + showLabel: boolean; | |
44 | + labelValue?: string; | |
45 | + attributeRequired: boolean; | |
46 | + showResultMessage: boolean; | |
47 | +} | |
48 | + | |
49 | +@Component({ | |
50 | + selector: 'tb-json-input-widget ', | |
51 | + templateUrl: './json-input-widget.component.html', | |
52 | + styleUrls: ['./json-input-widget.component.scss'] | |
53 | +}) | |
54 | +export class JsonInputWidgetComponent extends PageComponent implements OnInit { | |
55 | + | |
56 | + @Input() | |
57 | + ctx: WidgetContext; | |
58 | + | |
59 | + public settings: JsonInputWidgetSettings; | |
60 | + private widgetConfig: WidgetConfig; | |
61 | + private subscription: IWidgetSubscription; | |
62 | + private datasource: Datasource; | |
63 | + | |
64 | + labelValue: string; | |
65 | + | |
66 | + entityDetected = false; | |
67 | + dataKeyDetected = false; | |
68 | + isValidParameter = false; | |
69 | + errorMessage: string; | |
70 | + | |
71 | + isFocused: boolean; | |
72 | + originalValue: any; | |
73 | + attributeUpdateFormGroup: FormGroup; | |
74 | + | |
75 | + toastTargetId = 'json-input-widget' + this.utils.guid(); | |
76 | + | |
77 | + constructor(protected store: Store<AppState>, | |
78 | + private utils: UtilsService, | |
79 | + private fb: FormBuilder, | |
80 | + private attributeService: AttributeService, | |
81 | + private translate: TranslateService) { | |
82 | + super(store); | |
83 | + } | |
84 | + | |
85 | + ngOnInit(): void { | |
86 | + this.ctx.$scope.jsonInputWidget = this; | |
87 | + this.settings = this.ctx.settings; | |
88 | + this.widgetConfig = this.ctx.widgetConfig; | |
89 | + this.subscription = this.ctx.defaultSubscription; | |
90 | + this.datasource = this.subscription.datasources[0]; | |
91 | + this.initializeConfig(); | |
92 | + this.validateDatasources(); | |
93 | + this.buildForm(); | |
94 | + this.ctx.updateWidgetParams(); | |
95 | + } | |
96 | + | |
97 | + private initializeConfig() { | |
98 | + if (this.settings.widgetTitle && this.settings.widgetTitle.length) { | |
99 | + const title = createLabelFromDatasource(this.datasource, this.settings.widgetTitle); | |
100 | + this.ctx.widgetTitle = this.utils.customTranslation(title, title); | |
101 | + } else { | |
102 | + this.ctx.widgetTitle = this.ctx.widgetConfig.title; | |
103 | + } | |
104 | + | |
105 | + if (this.settings.labelValue && this.settings.labelValue.length) { | |
106 | + const label = createLabelFromDatasource(this.datasource, this.settings.labelValue); | |
107 | + this.labelValue = this.utils.customTranslation(label, label); | |
108 | + } else { | |
109 | + this.labelValue = this.translate.instant('widgets.input-widgets.value'); | |
110 | + } | |
111 | + } | |
112 | + | |
113 | + private validateDatasources() { | |
114 | + if (this.datasource?.type === DatasourceType.entity) { | |
115 | + this.entityDetected = true; | |
116 | + if (this.datasource.dataKeys.length) { | |
117 | + this.dataKeyDetected = true; | |
118 | + | |
119 | + if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) { | |
120 | + if (this.datasource.dataKeys[0].type === DataKeyType.attribute) { | |
121 | + if (this.settings.attributeScope === AttributeScope.SERVER_SCOPE || this.datasource.entityType === EntityType.DEVICE) { | |
122 | + this.isValidParameter = true; | |
123 | + } else { | |
124 | + this.errorMessage = 'widgets.input-widgets.not-allowed-entity'; | |
125 | + } | |
126 | + } else { | |
127 | + this.errorMessage = 'widgets.input-widgets.no-attribute-selected'; | |
128 | + } | |
129 | + } else { | |
130 | + if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) { | |
131 | + this.isValidParameter = true; | |
132 | + } else { | |
133 | + this.errorMessage = 'widgets.input-widgets.no-timeseries-selected'; | |
134 | + } | |
135 | + } | |
136 | + | |
137 | + } | |
138 | + } | |
139 | + } | |
140 | + | |
141 | + private buildForm() { | |
142 | + const validators: ValidatorFn[] = []; | |
143 | + if (this.settings.attributeRequired) { | |
144 | + validators.push(Validators.required); | |
145 | + } | |
146 | + this.attributeUpdateFormGroup = this.fb.group({ | |
147 | + currentValue: [{}, validators] | |
148 | + }); | |
149 | + this.attributeUpdateFormGroup.valueChanges.subscribe( () => { | |
150 | + this.ctx.detectChanges(); | |
151 | + }); | |
152 | + } | |
153 | + | |
154 | + private updateWidgetData(data: Array<DatasourceData>) { | |
155 | + if (this.isValidParameter) { | |
156 | + let value = {}; | |
157 | + if (data[0].data[0][1] !== '') { | |
158 | + try { | |
159 | + value = JSON.parse(data[0].data[0][1]); | |
160 | + } catch (e) { | |
161 | + value = data[0].data[0][1]; | |
162 | + } | |
163 | + } | |
164 | + this.originalValue = value; | |
165 | + if (!this.isFocused) { | |
166 | + this.attributeUpdateFormGroup.get('currentValue').patchValue(this.originalValue); | |
167 | + this.ctx.detectChanges(); | |
168 | + } | |
169 | + } | |
170 | + } | |
171 | + | |
172 | + public onDataUpdated() { | |
173 | + this.updateWidgetData(this.subscription.data); | |
174 | + } | |
175 | + | |
176 | + public save() { | |
177 | + this.isFocused = false; | |
178 | + | |
179 | + const attributeToSave: AttributeData = { | |
180 | + key: this.datasource.dataKeys[0].name, | |
181 | + value: this.attributeUpdateFormGroup.get('currentValue').value | |
182 | + }; | |
183 | + | |
184 | + const entityId: EntityId = { | |
185 | + entityType: this.datasource.entityType, | |
186 | + id: this.datasource.entityId | |
187 | + }; | |
188 | + | |
189 | + let saveAttributeObservable: Observable<any>; | |
190 | + if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) { | |
191 | + saveAttributeObservable = this.attributeService.saveEntityAttributes( | |
192 | + entityId, | |
193 | + this.settings.attributeScope, | |
194 | + [ attributeToSave ], | |
195 | + {} | |
196 | + ); | |
197 | + } else { | |
198 | + saveAttributeObservable = this.attributeService.saveEntityTimeseries( | |
199 | + entityId, | |
200 | + LatestTelemetry.LATEST_TELEMETRY, | |
201 | + [ attributeToSave ], | |
202 | + {} | |
203 | + ); | |
204 | + } | |
205 | + saveAttributeObservable.subscribe( | |
206 | + () => { | |
207 | + this.attributeUpdateFormGroup.markAsPristine(); | |
208 | + this.ctx.detectChanges(); | |
209 | + if (this.settings.showResultMessage) { | |
210 | + this.ctx.showSuccessToast(this.translate.instant('widgets.input-widgets.update-successful'), | |
211 | + 1000, 'bottom', 'left', this.toastTargetId); | |
212 | + } | |
213 | + }, | |
214 | + () => { | |
215 | + if (this.settings.showResultMessage) { | |
216 | + this.ctx.showErrorToast(this.translate.instant('widgets.input-widgets.update-failed'), | |
217 | + 'bottom', 'left', this.toastTargetId); | |
218 | + } | |
219 | + }); | |
220 | + } | |
221 | + | |
222 | + public discard() { | |
223 | + this.attributeUpdateFormGroup.reset({currentValue: this.originalValue}, {emitEvent: false}); | |
224 | + this.attributeUpdateFormGroup.markAsPristine(); | |
225 | + this.isFocused = false; | |
226 | + } | |
227 | +} | ... | ... |
... | ... | @@ -37,6 +37,7 @@ import { GatewayFormComponent } from './lib/gateway/gateway-form.component'; |
37 | 37 | import { ImportExportService } from '@home/components/import-export/import-export.service'; |
38 | 38 | import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component'; |
39 | 39 | import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component'; |
40 | +import { JsonInputWidgetComponent } from '@home/components/widget/lib/json-input-widget.component'; | |
40 | 41 | |
41 | 42 | @NgModule({ |
42 | 43 | declarations: |
... | ... | @@ -49,6 +50,7 @@ import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navig |
49 | 50 | EntitiesHierarchyWidgetComponent, |
50 | 51 | DateRangeNavigatorWidgetComponent, |
51 | 52 | DateRangeNavigatorPanelComponent, |
53 | + JsonInputWidgetComponent, | |
52 | 54 | MultipleInputWidgetComponent, |
53 | 55 | TripAnimationComponent, |
54 | 56 | PhotoCameraInputWidgetComponent, |
... | ... | @@ -69,6 +71,7 @@ import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navig |
69 | 71 | EntitiesHierarchyWidgetComponent, |
70 | 72 | RpcWidgetsModule, |
71 | 73 | DateRangeNavigatorWidgetComponent, |
74 | + JsonInputWidgetComponent, | |
72 | 75 | MultipleInputWidgetComponent, |
73 | 76 | TripAnimationComponent, |
74 | 77 | PhotoCameraInputWidgetComponent, | ... | ... |
... | ... | @@ -22,7 +22,7 @@ import { ActionNotificationHide, ActionNotificationShow } from '@core/notificati |
22 | 22 | import { Store } from '@ngrx/store'; |
23 | 23 | import { AppState } from '@core/core.state'; |
24 | 24 | import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; |
25 | -import { guid } from '@core/utils'; | |
25 | +import { guid, isDefinedAndNotNull, isLiteralObject } from '@core/utils'; | |
26 | 26 | import { ResizeObserver } from '@juggle/resize-observer'; |
27 | 27 | import { getAce } from '@shared/models/ace/ace.models'; |
28 | 28 | |
... | ... | @@ -224,7 +224,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va |
224 | 224 | this.contentValue = ''; |
225 | 225 | this.objectValid = false; |
226 | 226 | try { |
227 | - if (this.modelValue) { | |
227 | + if (isDefinedAndNotNull(this.modelValue)) { | |
228 | 228 | this.contentValue = JSON.stringify(this.modelValue, undefined, 2); |
229 | 229 | this.objectValid = true; |
230 | 230 | } else { |
... | ... | @@ -250,6 +250,9 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va |
250 | 250 | if (this.contentValue && this.contentValue.length > 0) { |
251 | 251 | try { |
252 | 252 | data = JSON.parse(this.contentValue); |
253 | + if (!isLiteralObject(data)) { | |
254 | + throw new TypeError(`Value is not a valid JSON`); | |
255 | + } | |
253 | 256 | this.objectValid = true; |
254 | 257 | this.validationError = ''; |
255 | 258 | } catch (ex) { | ... | ... |