Commit 516ae3d8142c4c554cd20499bc6a2774300b48eb

Authored by Vladyslav
Committed by GitHub
1 parent 25ba8139

Create ui to support attribute type JSON (#2471)

... ... @@ -168,7 +168,7 @@
168 168 class="tb-value-cell"
169 169 (click)="editAttribute($event, attribute)">
170 170 <div fxLayout="row">
171   - <span fxFlex>{{attribute.value}}</span>
  171 + <span fxFlex>{{attribute.value | tbJson}}</span>
172 172 <span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)">
173 173 <mat-icon>edit</mat-icon>
174 174 </span>
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2020 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 +<form [formGroup]="jsonFormGroup" (ngSubmit)="add()" style="min-width: 400px;">
  19 + <mat-toolbar fxLayout="row" color="primary">
  20 + <h2>{{ (this.data.title ? this.data.title : 'details.edit-json') | translate }}</h2>
  21 + <span fxFlex></span>
  22 + <button mat-button mat-icon-button
  23 + (click)="cancel()"
  24 + type="button">
  25 + <mat-icon class="material-icons">close</mat-icon>
  26 + </button>
  27 + </mat-toolbar>
  28 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  29 + </mat-progress-bar>
  30 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  31 + <div mat-dialog-content>
  32 + <fieldset [disabled]="isLoading$ | async">
  33 + <tb-json-object-edit
  34 + formControlName="json"
  35 + label="{{ 'value.json-value' | translate }}"
  36 + validateContent="true"
  37 + [required]="true"
  38 + [fillHeight]="false">
  39 + </tb-json-object-edit>
  40 + </fieldset>
  41 + </div>
  42 + <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
  43 + <span fxFlex></span>
  44 + <button mat-button mat-raised-button color="primary"
  45 + type="submit"
  46 + [disabled]="(isLoading$ | async) || jsonFormGroup.invalid || !jsonFormGroup.dirty">
  47 + {{ 'action.save' | translate }}
  48 + </button>
  49 + <button mat-button color="primary"
  50 + style="margin-right: 20px;"
  51 + type="button"
  52 + [disabled]="(isLoading$ | async)"
  53 + (click)="cancel()" cdkFocusInitial>
  54 + {{ 'action.cancel' | translate }}
  55 + </button>
  56 + </div>
  57 +</form>
... ...
  1 +///
  2 +/// Copyright © 2016-2020 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, Inject, OnInit} from "@angular/core";
  18 +import {DialogComponent} from "@shared/components/dialog.component";
  19 +import {Store} from "@ngrx/store";
  20 +import {AppState} from "@core/core.state";
  21 +import {Router} from "@angular/router";
  22 +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
  23 +import {FormBuilder, FormGroup} from "@angular/forms";
  24 +
  25 +export interface JsonObjectEdittDialogData {
  26 + jsonValue: Object;
  27 + title?: string;
  28 +}
  29 +
  30 +@Component({
  31 + selector: 'tb-object-edit-dialog',
  32 + templateUrl: './json-object-edit-dialog.component.html',
  33 + styleUrls: []
  34 +})
  35 +export class JsonObjectEditDialogComponent extends DialogComponent<JsonObjectEditDialogComponent, Object>
  36 + implements OnInit {
  37 +
  38 + jsonFormGroup: FormGroup;
  39 +
  40 + submitted = false;
  41 +
  42 + constructor(protected store: Store<AppState>,
  43 + protected router: Router,
  44 + @Inject(MAT_DIALOG_DATA) public data: JsonObjectEdittDialogData,
  45 + public dialogRef: MatDialogRef<JsonObjectEditDialogComponent, Object>,
  46 + public fb: FormBuilder) {
  47 + super(store, router, dialogRef);
  48 + }
  49 +
  50 + ngOnInit(): void {
  51 + this.jsonFormGroup = this.fb.group({
  52 + json: [this.data.jsonValue, []]
  53 + });
  54 + }
  55 +
  56 + cancel(): void {
  57 + this.dialogRef.close(undefined);
  58 + }
  59 +
  60 + add(): void {
  61 + this.dialogRef.close(this.jsonFormGroup.get('json').value);
  62 + }
  63 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2020 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 {Directive, ElementRef, forwardRef, HostListener, Renderer2, SkipSelf} from "@angular/core";
  18 +import {
  19 + ControlValueAccessor,
  20 + FormControl, FormGroupDirective,
  21 + NG_VALIDATORS,
  22 + NG_VALUE_ACCESSOR, NgForm,
  23 + ValidationErrors,
  24 + Validator
  25 +} from "@angular/forms";
  26 +import {ErrorStateMatcher} from "@angular/material/core";
  27 +
  28 +@Directive({
  29 + selector: '[tb-json-to-string]',
  30 + providers: [{
  31 + provide: NG_VALUE_ACCESSOR,
  32 + useExisting: forwardRef(() => TbJsonToStringDirective),
  33 + multi: true
  34 + },
  35 + {
  36 + provide: NG_VALIDATORS,
  37 + useExisting: forwardRef(() => TbJsonToStringDirective),
  38 + multi: true,
  39 + },
  40 + {
  41 + provide: ErrorStateMatcher,
  42 + useExisting: TbJsonToStringDirective
  43 + }]
  44 +})
  45 +
  46 +export class TbJsonToStringDirective implements ControlValueAccessor, Validator, ErrorStateMatcher {
  47 + private propagateChange = null;
  48 + private parseError: boolean;
  49 + private data: any;
  50 +
  51 + @HostListener('input', ['$event.target.value']) input(newValue: any): void {
  52 + try {
  53 + this.data = JSON.parse(newValue);
  54 + this.parseError = false;
  55 + } catch (e) {
  56 + this.parseError = true;
  57 + }
  58 +
  59 + this.propagateChange(this.data);
  60 + }
  61 +
  62 + constructor(private render: Renderer2,
  63 + private element: ElementRef,
  64 + @SkipSelf() private errorStateMatcher: ErrorStateMatcher) {
  65 +
  66 + }
  67 +
  68 + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
  69 + const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
  70 + const customErrorState = !!(control && control.invalid && this.parseError);
  71 + return originalErrorState || customErrorState;
  72 + }
  73 +
  74 + validate(c: FormControl): ValidationErrors {
  75 + return (!this.parseError) ? null : {
  76 + invalidJSON: {
  77 + valid: false
  78 + }
  79 + };
  80 + }
  81 +
  82 + writeValue(obj: any): void {
  83 + if (obj) {
  84 + this.data = obj;
  85 + this.parseError = false;
  86 + this.render.setProperty(this.element.nativeElement, 'value', JSON.stringify(obj));
  87 + }
  88 + }
  89 +
  90 + registerOnChange(fn: any): void {
  91 + this.propagateChange = fn;
  92 + }
  93 +
  94 + registerOnTouched(fn: any): void {
  95 + }
  96 +}
... ...
... ... @@ -22,9 +22,13 @@
22 22 <label class="tb-title no-padding">{{ label }}</label>
23 23 <span fxFlex></span>
24 24 <button type="button"
25   - mat-button *ngIf="!readonly" class="tidy" (click)="beautifyJson()">
  25 + mat-button *ngIf="!readonly" class="tidy" (click)="beautifyJSON()">
26 26 {{'js-func.tidy' | translate }}
27 27 </button>
  28 + <button type="button"
  29 + mat-button *ngIf="!readonly" class="tidy" (click)="minifyJSON()">
  30 + {{'js-func.mini' | translate }}
  31 + </button>
28 32 <button type='button' mat-button mat-icon-button (click)="fullscreen = !fullscreen"
29 33 class="tb-mat-32"
30 34 matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
... ...
... ... @@ -257,12 +257,18 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
257 257 }
258 258 }
259 259
260   - beautifyJson() {
  260 + beautifyJSON() {
261 261 const res = js_beautify(this.contentBody, {indent_size: 4, wrap_line_length: 60});
262 262 this.jsonEditor.setValue(res ? res : '', -1);
263 263 this.updateView();
264 264 }
265 265
  266 + minifyJSON() {
  267 + const res = JSON.stringify(this.contentBody);
  268 + this.jsonEditor.setValue(res ? res : '', -1);
  269 + this.updateView();
  270 + }
  271 +
266 272 onFullscreen() {
267 273 if (this.jsonEditor) {
268 274 setTimeout(() => {
... ...
... ... @@ -18,12 +18,20 @@
18 18 <div style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
19 19 tb-fullscreen
20 20 [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
21   - <div fxLayout="row" fxLayoutAlign="start center">
  21 + <div fxLayout="row" fxLayoutAlign="start center" class="tb-json-object-toolbar">
22 22 <label class="tb-title no-padding"
23 23 ng-class="{'tb-required': required,
24 24 'tb-readonly': readonly,
25 25 'tb-error': !objectValid}">{{ label }}</label>
26 26 <span fxFlex></span>
  27 + <button type="button"
  28 + mat-button *ngIf="!readonly" class="tidy" (click)="beautifyJSON()">
  29 + {{'js-func.tidy' | translate }}
  30 + </button>
  31 + <button type="button"
  32 + mat-button *ngIf="!readonly" class="tidy" (click)="minifyJSON()">
  33 + {{'js-func.mini' | translate }}
  34 + </button>
27 35 <button mat-button mat-icon-button (click)="fullscreen = !fullscreen"
28 36 matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
29 37 matTooltipPosition="above">
... ...
... ... @@ -21,6 +21,25 @@
21 21 }
22 22 }
23 23
  24 +.tb-json-object-toolbar {
  25 + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
  26 + align-items: center;
  27 + vertical-align: middle;
  28 + min-width: 32px;
  29 + min-height: 15px;
  30 + padding: 4px;
  31 + margin: 0;
  32 + font-size: .8rem;
  33 + line-height: 15px;
  34 + color: #7b7b7b;
  35 + background: rgba(220, 220, 220, .35);
  36 +
  37 + &:not(:last-child) {
  38 + margin-right: 4px;
  39 + }
  40 + }
  41 +}
  42 +
24 43 .tb-json-object-panel {
25 44 height: 100%;
26 45 margin-left: 15px;
... ...
... ... @@ -195,6 +195,22 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
195 195 }
196 196 }
197 197
  198 + beautifyJSON() {
  199 + const res = JSON.stringify(this.modelValue, null, 2);
  200 + if (this.jsonEditor) {
  201 + this.jsonEditor.setValue(res ? res : '', -1);
  202 + }
  203 + this.updateView();
  204 + }
  205 +
  206 + minifyJSON() {
  207 + const res = JSON.stringify(this.modelValue);
  208 + if (this.jsonEditor) {
  209 + this.jsonEditor.setValue(res ? res : '', -1);
  210 + }
  211 + this.updateView();
  212 + }
  213 +
198 214 writeValue(value: any): void {
199 215 this.modelValue = value;
200 216 this.contentValue = '';
... ...
... ... @@ -59,5 +59,21 @@
59 59 {{ (modelValue ? 'value.true' : 'value.false') | translate }}
60 60 </mat-checkbox>
61 61 </div>
  62 + <div fxLayout="row" fxLayoutAlign="center" fxFlex="60" *ngIf="valueType === valueTypeEnum.JSON" class="mat-block">
  63 + <mat-form-field fxFlex class="mat-block">
  64 + <mat-label translate>value.json-value</mat-label>
  65 + <input [disabled]="disabled" matInput tb-json-to-string required name="value" #value="ngModel"
  66 + [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"/>
  67 + <button matSuffix mat-button mat-icon-button (click)="openEditJSONDialog($event)">
  68 + <mat-icon>open_in_new</mat-icon>
  69 + </button>
  70 + <mat-error *ngIf="value.hasError('required')">
  71 + {{ (requiredText ? requiredText : 'value.json-value-required') | translate }}
  72 + </mat-error>
  73 + <mat-error *ngIf="value.hasError('invalidJSON')">
  74 + {{ 'value.json-value-invalid' | translate }}
  75 + </mat-error>
  76 + </mat-form-field>
  77 + </div>
62 78 </section>
63 79 </form>
... ...
... ... @@ -17,6 +17,12 @@
17 17 import { Component, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
18 18 import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgForm } from '@angular/forms';
19 19 import { ValueType, valueTypesMap } from '@shared/models/constants';
  20 +import { isObject } from "@core/utils";
  21 +import { MatDialog } from "@angular/material/dialog";
  22 +import {
  23 + JsonObjectEditDialogComponent,
  24 + JsonObjectEdittDialogData
  25 +} from "@shared/components/dialog/json-object-edit-dialog.component";
20 26
21 27 @Component({
22 28 selector: 'tb-value-input',
... ... @@ -50,13 +56,35 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor {
50 56
51 57 private propagateChange = null;
52 58
53   - constructor() {
  59 + constructor(
  60 + public dialog: MatDialog,
  61 + ) {
54 62
55 63 }
56 64
57 65 ngOnInit(): void {
58 66 }
59 67
  68 + openEditJSONDialog($event: Event) {
  69 + if ($event) {
  70 + $event.stopPropagation();
  71 + }
  72 + this.dialog.open<JsonObjectEditDialogComponent, JsonObjectEdittDialogData, Object>(JsonObjectEditDialogComponent, {
  73 + disableClose: true,
  74 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  75 + data: {
  76 + jsonValue: this.modelValue
  77 + }
  78 + }).afterClosed().subscribe(
  79 + (res) => {
  80 + if (res) {
  81 + this.modelValue = res;
  82 + this.inputForm.control.patchValue({'value': this.modelValue});
  83 + }
  84 + }
  85 + );
  86 + }
  87 +
60 88 registerOnChange(fn: any): void {
61 89 this.propagateChange = fn;
62 90 }
... ... @@ -78,6 +106,8 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor {
78 106 } else {
79 107 this.valueType = ValueType.DOUBLE;
80 108 }
  109 + } else if (isObject(this.modelValue)) {
  110 + this.valueType = ValueType.JSON;
81 111 } else {
82 112 this.valueType = ValueType.STRING;
83 113 }
... ... @@ -94,6 +124,8 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor {
94 124 onValueTypeChanged() {
95 125 if (this.valueType === ValueType.BOOLEAN) {
96 126 this.modelValue = false;
  127 + } if (this.valueType === ValueType.JSON) {
  128 + this.modelValue = {};
97 129 } else {
98 130 this.modelValue = null;
99 131 }
... ...
... ... @@ -121,7 +121,8 @@ export enum ValueType {
121 121 STRING = 'STRING',
122 122 INTEGER = 'INTEGER',
123 123 DOUBLE = 'DOUBLE',
124   - BOOLEAN = 'BOOLEAN'
  124 + BOOLEAN = 'BOOLEAN',
  125 + JSON = 'JSON'
125 126 }
126 127
127 128 export const valueTypesMap = new Map<ValueType, ValueTypeData>(
... ... @@ -153,6 +154,13 @@ export const valueTypesMap = new Map<ValueType, ValueTypeData>(
153 154 name: 'value.boolean',
154 155 icon: 'mdi:checkbox-marked-outline'
155 156 }
  157 + ],
  158 + [
  159 + ValueType.JSON,
  160 + {
  161 + name: 'value.json',
  162 + icon: 'mdi:json'
  163 + }
156 164 ]
157 165 ]
158 166 );
... ...
  1 +///
  2 +/// Copyright © 2016-2020 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 {Pipe, PipeTransform} from '@angular/core';
  18 +import {isObject, isNumber} from "@core/utils";
  19 +
  20 +@Pipe({name: 'tbJson'})
  21 +export class TbJsonPipe implements PipeTransform {
  22 + transform(value: any): string {
  23 + if (isObject(value)) {
  24 + return JSON.stringify(value);
  25 + } else if (isNumber(value)) {
  26 + return value.toString();
  27 + }
  28 + return value;
  29 + }
  30 +}
... ...
... ... @@ -104,6 +104,7 @@ import { TbErrorComponent } from '@shared/components/tb-error.component';
104 104 import { EntityTypeListComponent } from '@shared/components/entity/entity-type-list.component';
105 105 import { EntitySubTypeListComponent } from '@shared/components/entity/entity-subtype-list.component';
106 106 import { TruncatePipe } from '@shared/pipe/truncate.pipe';
  107 +import { TbJsonPipe } from "@shared/pipe/tbJson.pipe";
107 108 import { ColorPickerDialogComponent } from '@shared/components/dialog/color-picker-dialog.component';
108 109 import { MatChipDraggableDirective } from '@shared/components/mat-chip-draggable.directive';
109 110 import { ColorInputComponent } from '@shared/components/color-input.component';
... ... @@ -124,6 +125,8 @@ import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component';
124 125 import { TbHotkeysDirective } from '@shared/components/hotkeys.directive';
125 126 import { NavTreeComponent } from '@shared/components/nav-tree.component';
126 127 import { LedLightComponent } from '@shared/components/led-light.component';
  128 +import { TbJsonToStringDirective } from "@shared/components/directives/tb-json-to-string.directive";
  129 +import { JsonObjectEditDialogComponent } from "@shared/components/dialog/json-object-edit-dialog.component";
127 130
128 131 @NgModule({
129 132 providers: [
... ... @@ -132,6 +135,7 @@ import { LedLightComponent } from '@shared/components/led-light.component';
132 135 EnumToArrayPipe,
133 136 HighlightPipe,
134 137 TruncatePipe,
  138 + TbJsonPipe,
135 139 {
136 140 provide: FlowInjectionToken,
137 141 useValue: Flow
... ... @@ -202,7 +206,10 @@ import { LedLightComponent } from '@shared/components/led-light.component';
202 206 EnumToArrayPipe,
203 207 HighlightPipe,
204 208 TruncatePipe,
205   - KeyboardShortcutPipe
  209 + TbJsonPipe,
  210 + KeyboardShortcutPipe,
  211 + TbJsonToStringDirective,
  212 + JsonObjectEditDialogComponent
206 213 ],
207 214 imports: [
208 215 CommonModule,
... ... @@ -357,8 +364,10 @@ import { LedLightComponent } from '@shared/components/led-light.component';
357 364 EnumToArrayPipe,
358 365 HighlightPipe,
359 366 TruncatePipe,
  367 + TbJsonPipe,
360 368 KeyboardShortcutPipe,
361   - TranslateModule
  369 + TranslateModule,
  370 + JsonObjectEditDialogComponent
362 371 ]
363 372 })
364 373 export class SharedModule { }
... ...
... ... @@ -626,6 +626,7 @@
626 626 "details": {
627 627 "details": "Details",
628 628 "edit-mode": "Edit mode",
  629 + "edit-json": "Edit JSON",
629 630 "toggle-edit-mode": "Toggle edit mode"
630 631 },
631 632 "device": {
... ... @@ -1298,7 +1299,8 @@
1298 1299 "js-func": {
1299 1300 "no-return-error": "Function must return value!",
1300 1301 "return-type-mismatch": "Function must return value of '{{type}}' type!",
1301   - "tidy": "Tidy"
  1302 + "tidy": "Tidy",
  1303 + "mini": "Mini"
1302 1304 },
1303 1305 "key-val": {
1304 1306 "key": "Key",
... ... @@ -1635,7 +1637,11 @@
1635 1637 "boolean-value": "Boolean value",
1636 1638 "false": "False",
1637 1639 "true": "True",
1638   - "long": "Long"
  1640 + "long": "Long",
  1641 + "json": "JSON",
  1642 + "json-value": "JSON value",
  1643 + "json-value-invalid": "JSON value has an invalid format",
  1644 + "json-value-required": "JSON value is required."
1639 1645 },
1640 1646 "widget": {
1641 1647 "widget-library": "Widgets Library",
... ...
... ... @@ -607,6 +607,7 @@
607 607 },
608 608 "details": {
609 609 "edit-mode": "Режим редактирования",
  610 + "edit-json": "Редактировать JSON",
610 611 "toggle-edit-mode": "Режим редактирования"
611 612 },
612 613 "device": {
... ... @@ -1191,8 +1192,7 @@
1191 1192 },
1192 1193 "js-func": {
1193 1194 "no-return-error": "Функция должна возвращать значение!",
1194   - "return-type-mismatch": "Функция должна возвращать значение типа '{{type}}'!",
1195   - "tidy": "Tidy"
  1195 + "return-type-mismatch": "Функция должна возвращать значение типа '{{type}}'!"
1196 1196 },
1197 1197 "key-val": {
1198 1198 "key": "Ключ",
... ...
... ... @@ -724,6 +724,7 @@
724 724 "details": {
725 725 "details": "Деталі",
726 726 "edit-mode": "Режим редагування",
  727 + "edit-json": "Редагувати JSON",
727 728 "toggle-edit-mode": "Перемкнути режим редагування"
728 729 },
729 730 "device": {
... ... @@ -1606,8 +1607,7 @@
1606 1607 },
1607 1608 "js-func": {
1608 1609 "no-return-error": "Функція повинна повертати значення!",
1609   - "return-type-mismatch": "Функція повинна повернути значення типу '{{type}}'!",
1610   - "tidy": "Tidy"
  1610 + "return-type-mismatch": "Функція повинна повернути значення типу '{{type}}'!"
1611 1611 },
1612 1612 "key-val": {
1613 1613 "key": "Ключ",
... ...