Commit 51125df8a24f830b606f7da53c21b0702892a12d

Authored by Chantsova Ekaterina
1 parent 593f95a7

Add JSON input widget

@@ -455,6 +455,24 @@ @@ -455,6 +455,24 @@
455 "dataKeySettingsSchema": "{}\n", 455 "dataKeySettingsSchema": "{}\n",
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\":{}}" 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,6 +127,10 @@ export function isEmpty(obj: any): boolean {
127 return true; 127 return true;
128 } 128 }
129 129
  130 +export function isLiteralObject(value: any) {
  131 + return (!!value) && (value.constructor === Object);
  132 +}
  133 +
130 export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined { 134 export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined {
131 if (isDefinedAndNotNull(value) && isNumeric(value) && 135 if (isDefinedAndNotNull(value) && isNumeric(value) &&
132 (isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) { 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,6 +37,7 @@ import { GatewayFormComponent } from './lib/gateway/gateway-form.component';
37 import { ImportExportService } from '@home/components/import-export/import-export.service'; 37 import { ImportExportService } from '@home/components/import-export/import-export.service';
38 import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component'; 38 import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component';
39 import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component'; 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 @NgModule({ 42 @NgModule({
42 declarations: 43 declarations:
@@ -49,6 +50,7 @@ import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navig @@ -49,6 +50,7 @@ import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navig
49 EntitiesHierarchyWidgetComponent, 50 EntitiesHierarchyWidgetComponent,
50 DateRangeNavigatorWidgetComponent, 51 DateRangeNavigatorWidgetComponent,
51 DateRangeNavigatorPanelComponent, 52 DateRangeNavigatorPanelComponent,
  53 + JsonInputWidgetComponent,
52 MultipleInputWidgetComponent, 54 MultipleInputWidgetComponent,
53 TripAnimationComponent, 55 TripAnimationComponent,
54 PhotoCameraInputWidgetComponent, 56 PhotoCameraInputWidgetComponent,
@@ -69,6 +71,7 @@ import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navig @@ -69,6 +71,7 @@ import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navig
69 EntitiesHierarchyWidgetComponent, 71 EntitiesHierarchyWidgetComponent,
70 RpcWidgetsModule, 72 RpcWidgetsModule,
71 DateRangeNavigatorWidgetComponent, 73 DateRangeNavigatorWidgetComponent,
  74 + JsonInputWidgetComponent,
72 MultipleInputWidgetComponent, 75 MultipleInputWidgetComponent,
73 TripAnimationComponent, 76 TripAnimationComponent,
74 PhotoCameraInputWidgetComponent, 77 PhotoCameraInputWidgetComponent,
@@ -22,7 +22,7 @@ import { ActionNotificationHide, ActionNotificationShow } from '@core/notificati @@ -22,7 +22,7 @@ import { ActionNotificationHide, ActionNotificationShow } from '@core/notificati
22 import { Store } from '@ngrx/store'; 22 import { Store } from '@ngrx/store';
23 import { AppState } from '@core/core.state'; 23 import { AppState } from '@core/core.state';
24 import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; 24 import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
25 -import { guid } from '@core/utils'; 25 +import { guid, isDefinedAndNotNull, isLiteralObject } from '@core/utils';
26 import { ResizeObserver } from '@juggle/resize-observer'; 26 import { ResizeObserver } from '@juggle/resize-observer';
27 import { getAce } from '@shared/models/ace/ace.models'; 27 import { getAce } from '@shared/models/ace/ace.models';
28 28
@@ -224,7 +224,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va @@ -224,7 +224,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
224 this.contentValue = ''; 224 this.contentValue = '';
225 this.objectValid = false; 225 this.objectValid = false;
226 try { 226 try {
227 - if (this.modelValue) { 227 + if (isDefinedAndNotNull(this.modelValue)) {
228 this.contentValue = JSON.stringify(this.modelValue, undefined, 2); 228 this.contentValue = JSON.stringify(this.modelValue, undefined, 2);
229 this.objectValid = true; 229 this.objectValid = true;
230 } else { 230 } else {
@@ -250,6 +250,9 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va @@ -250,6 +250,9 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
250 if (this.contentValue && this.contentValue.length > 0) { 250 if (this.contentValue && this.contentValue.length > 0) {
251 try { 251 try {
252 data = JSON.parse(this.contentValue); 252 data = JSON.parse(this.contentValue);
  253 + if (!isLiteralObject(data)) {
  254 + throw new TypeError(`Value is not a valid JSON`);
  255 + }
253 this.objectValid = true; 256 this.objectValid = true;
254 this.validationError = ''; 257 this.validationError = '';
255 } catch (ex) { 258 } catch (ex) {