Commit 25ba81394dee5afe6d63e4c9db18a6a6d5036bae

Authored by Igor Kulikov
2 parents 95466c39 0ed07256

Merge branch 'master' of github.com:thingsboard/thingsboard into develop/3.0

... ... @@ -29,6 +29,8 @@ import thingsboardWebCameraInputWidget from '../widget/lib/web-camera-input-widg
29 29
30 30 import thingsboardRpcWidgets from '../widget/lib/rpc';
31 31
  32 +import thingsboardJsonToString from '../components/tb-json-to-string.directive';
  33 +
32 34 import TbFlot from '../widget/lib/flot-widget';
33 35 import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge';
34 36 import TbAnalogueRadialGauge from '../widget/lib/analogue-radial-gauge';
... ... @@ -52,7 +54,7 @@ export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsbo
52 54 thingsboardTimeseriesTableWidget, thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget,
53 55 thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardDateRangeNavigatorWidget,
54 56 thingsboardMultipleInputWidget, thingsboardWebCameraInputWidget, thingsboardRpcWidgets, thingsboardTypes,
55   - thingsboardUtils, TripAnimationWidget])
  57 + thingsboardUtils, thingsboardJsonToString, TripAnimationWidget])
56 58 .factory('widgetService', WidgetService)
57 59 .name;
58 60
... ...
... ... @@ -881,6 +881,11 @@ export default angular.module('thingsboard.types', [])
881 881 value: "boolean",
882 882 name: "value.boolean",
883 883 icon: "mdi:checkbox-marked-outline"
  884 + },
  885 + json: {
  886 + value: "json",
  887 + name: "value.json",
  888 + icon: "mdi:json"
884 889 }
885 890 },
886 891 widgetType: {
... ...
... ... @@ -57,9 +57,12 @@ function JsonContent($compile, $templateCache, toast, types, utils) {
57 57 updateEditorSize();
58 58 };
59 59
60   - scope.beautifyJson = function () {
61   - var res = js_beautify(scope.contentBody, {indent_size: 4, wrap_line_length: 60});
62   - scope.contentBody = res;
  60 + scope.beautifyJSON = function () {
  61 + scope.contentBody = js_beautify(scope.contentBody, {indent_size: 4, wrap_line_length: 60});
  62 + };
  63 +
  64 + scope.minifyJSON = function () {
  65 + scope.contentBody = angular.toJson(angular.fromJson(scope.contentBody));
63 66 };
64 67
65 68 function updateEditorSize() {
... ... @@ -116,7 +119,7 @@ function JsonContent($compile, $templateCache, toast, types, utils) {
116 119 scope.$watch('contentBody', function (newContent, oldContent) {
117 120 ngModelCtrl.$setViewValue(scope.contentBody);
118 121 if (!angular.equals(newContent, oldContent)) {
119   - scope.contentValid = true;
  122 + scope.contentValid = scope.validate();
120 123 }
121 124 scope.updateValidity();
122 125 });
... ... @@ -139,15 +142,17 @@ function JsonContent($compile, $templateCache, toast, types, utils) {
139 142 }
140 143 return true;
141 144 } catch (e) {
142   - var details = utils.parseException(e);
143   - var errorInfo = 'Error:';
144   - if (details.name) {
145   - errorInfo += ' ' + details.name + ':';
146   - }
147   - if (details.message) {
148   - errorInfo += ' ' + details.message;
  145 + if (!scope.hideErrorToast) {
  146 + var details = utils.parseException(e);
  147 + var errorInfo = 'Error:';
  148 + if (details.name) {
  149 + errorInfo += ' ' + details.name + ':';
  150 + }
  151 + if (details.message) {
  152 + errorInfo += ' ' + details.message;
  153 + }
  154 + scope.showError(errorInfo);
149 155 }
150   - scope.showError(errorInfo);
151 156 return false;
152 157 }
153 158 };
... ... @@ -169,7 +174,7 @@ function JsonContent($compile, $templateCache, toast, types, utils) {
169 174 });
170 175
171 176 $compile(element.contents())(scope);
172   - }
  177 + };
173 178
174 179 return {
175 180 restrict: "E",
... ... @@ -177,6 +182,7 @@ function JsonContent($compile, $templateCache, toast, types, utils) {
177 182 scope: {
178 183 contentType: '=',
179 184 validateContent: '=?',
  185 + hideErrorToast: '=?',
180 186 readonly:'=ngReadonly',
181 187 fillHeight:'=?'
182 188 },
... ...
... ... @@ -27,7 +27,7 @@ tb-json-content {
27 27 min-height: 15px;
28 28 padding: 4px;
29 29 margin: 0 5px 0 0;
30   - font-size: .8rem;
  30 + font-size: 12px;
31 31 line-height: 15px;
32 32 color: #7b7b7b;
33 33 background: rgba(220, 220, 220, .35);
... ...
... ... @@ -17,11 +17,15 @@
17 17 -->
18 18 <div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
19 19 <div layout="row" layout-align="start center" style="height: 40px;" class="tb-json-content-toolbar">
20   - <label class="tb-title no-padding">{{ label }}</label>
  20 + <label class="tb-title no-padding"
  21 + ng-class="{'tb-error': !contentValid}">{{ label }}</label>
21 22 <span flex></span>
22   - <md-button ng-if="!readonly" class="tidy" aria-label="{{ 'js-func.tidy' | translate }}" ng-click="beautifyJson()">{{
  23 + <md-button ng-if="!readonly" class="tidy" aria-label="{{ 'js-func.tidy' | translate }}" ng-click="beautifyJSON()">{{
23 24 'js-func.tidy' | translate }}
24 25 </md-button>
  26 + <md-button ng-if="!readonly" class="tidy" aria-label="{{ 'js-func.mini' | translate }}" ng-click="minifyJSON()">{{
  27 + 'js-func.mini' | translate }}
  28 + </md-button>
25 29 <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
26 30 </div>
27 31 <div flex id="tb-json-panel" class="tb-json-content-panel" layout="column">
... ...
... ... @@ -50,6 +50,14 @@ function JsonObjectEdit($compile, $templateCache, $document, toast, utils) {
50 50 updateEditorSize();
51 51 };
52 52
  53 + scope.beautifyJSON = function () {
  54 + scope.contentBody = angular.toJson(scope.object, 4);
  55 + };
  56 +
  57 + scope.minifyJSON = function () {
  58 + scope.contentBody = angular.toJson(scope.object);
  59 + };
  60 +
53 61 function updateEditorSize() {
54 62 if (scope.json_editor) {
55 63 scope.json_editor.resize();
... ... @@ -169,7 +177,7 @@ function JsonObjectEdit($compile, $templateCache, $document, toast, utils) {
169 177 });
170 178
171 179 $compile(element.contents())(scope);
172   - }
  180 + };
173 181
174 182 return {
175 183 restrict: "E",
... ...
... ... @@ -21,6 +21,19 @@ tb-json-object-edit {
21 21 }
22 22 }
23 23
  24 +.tb-json-object-edit-toolbar {
  25 + .md-button.tidy {
  26 + min-width: 32px;
  27 + min-height: 15px;
  28 + padding: 4px;
  29 + margin: 0 5px 0 0;
  30 + font-size: 12px;
  31 + line-height: 15px;
  32 + color: #7b7b7b;
  33 + background: rgba(220, 220, 220, .35);
  34 + }
  35 +}
  36 +
24 37 .tb-json-object-panel {
25 38 height: 100%;
26 39 margin-left: 15px;
... ...
... ... @@ -16,12 +16,18 @@
16 16
17 17 -->
18 18 <div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
19   - <div layout="row" layout-align="start center">
  19 + <div layout="row" layout-align="start center" class="tb-json-object-edit-toolbar">
20 20 <label class="tb-title no-padding"
21 21 ng-class="{'tb-required': required,
22 22 'tb-readonly': readonly,
23 23 'tb-error': !objectValid}">{{ label }}</label>
24 24 <span flex></span>
  25 + <md-button ng-if="!readonly" class="tidy" aria-label="{{ 'js-func.tidy' | translate }}" ng-click="beautifyJSON()">
  26 + {{'js-func.tidy' | translate }}
  27 + </md-button>
  28 + <md-button ng-if="!readonly" class="tidy" aria-label="{{ 'js-func.mini' | translate }}" ng-click="minifyJSON()">
  29 + {{'js-func.mini' | translate }}
  30 + </md-button>
25 31 <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
26 32 </div>
27 33 <div flex id="tb-json-panel" class="tb-json-object-panel" layout="column">
... ...
  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 +export default angular.module('tbJsonToString', [])
  17 + .directive('tbJsonToString', InputJson)
  18 + .name;
  19 +
  20 +function InputJson() {
  21 + return {
  22 + restrict: 'A',
  23 + require: 'ngModel',
  24 + link: function(scope, element, attr, ngModelCtrl) {
  25 + function into(input) {
  26 + try {
  27 + ngModelCtrl.$setValidity('invalidJSON', true);
  28 + return angular.fromJson(input);
  29 + } catch (e) {
  30 + ngModelCtrl.$setValidity('invalidJSON', false);
  31 + }
  32 + }
  33 + function out(data) {
  34 + try {
  35 + ngModelCtrl.$setValidity('invalidJSON', true);
  36 + return angular.toJson(data);
  37 + } catch (e) {
  38 + ngModelCtrl.$setValidity('invalidJSON', false);
  39 + }
  40 + }
  41 + ngModelCtrl.$parsers.push(into);
  42 + ngModelCtrl.$formatters.push(out);
  43 + }
  44 + };
  45 +}
... ...
... ... @@ -13,10 +13,18 @@
13 13 * See the License for the specific language governing permissions and
14 14 * limitations under the License.
15 15 */
  16 +/* eslint-disable import/no-unresolved, import/default */
  17 +
  18 +import attributeDialogEditJsonTemplate from './attribute-dialog-edit-json.tpl.html';
  19 +
  20 +/* eslint-enable import/no-unresolved, import/default */
  21 +
  22 +import AttributeDialogEditJsonController from './attribute-dialog-edit-json.controller';
  23 +
16 24 /*@ngInject*/
17 25 export default function AddAttributeDialogController($scope, $mdDialog, types, attributeService, entityType, entityId, attributeScope) {
18 26
19   - var vm = this;
  27 + let vm = this;
20 28
21 29 vm.attribute = {};
22 30
... ... @@ -40,11 +48,37 @@ export default function AddAttributeDialogController($scope, $mdDialog, types, a
40 48 );
41 49 }
42 50
43   - $scope.$watch('vm.valueType', function() {
  51 + $scope.$watch('vm.valueType', function () {
44 52 if (vm.valueType === types.valueType.boolean) {
45 53 vm.attribute.value = false;
  54 + } else if (vm.valueType === types.valueType.json) {
  55 + vm.attribute.value = {};
46 56 } else {
47 57 vm.attribute.value = null;
48 58 }
49 59 });
  60 +
  61 + vm.addJSON = ($event) => {
  62 + showJsonDialog($event, vm.attribute.value, false).then((response) => {
  63 + vm.attribute.value = response;
  64 + })
  65 + };
  66 +
  67 + function showJsonDialog($event, jsonValue, readOnly) {
  68 + if ($event) {
  69 + $event.stopPropagation();
  70 + }
  71 + return $mdDialog.show({
  72 + controller: AttributeDialogEditJsonController,
  73 + controllerAs: 'vm',
  74 + templateUrl: attributeDialogEditJsonTemplate,
  75 + locals: {
  76 + jsonValue: jsonValue,
  77 + readOnly: readOnly
  78 + },
  79 + targetEvent: $event,
  80 + fullscreen: true,
  81 + multiple: true,
  82 + });
  83 + }
50 84 }
... ...
... ... @@ -26,7 +26,8 @@
26 26 </md-button>
27 27 </div>
28 28 </md-toolbar>
29   - <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
  29 + <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading"
  30 + ng-show="$root.loading"></md-progress-linear>
30 31 <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
31 32 <md-dialog-content>
32 33 <div class="md-dialog-content">
... ... @@ -58,7 +59,8 @@
58 59 </md-input-container>
59 60 <md-input-container ng-if="vm.valueType===vm.valueTypes.integer" flex="60" class="md-block">
60 61 <label translate>value.integer-value</label>
61   - <input required name="value" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attribute.value">
  62 + <input required name="value" type="number" step="1" ng-pattern="/^-?[0-9]+$/"
  63 + ng-model="vm.attribute.value">
62 64 <div ng-messages="theForm.value.$error">
63 65 <div translate ng-message="required">attribute.value-required</div>
64 66 <div translate ng-message="pattern">value.invalid-integer-value</div>
... ... @@ -71,11 +73,31 @@
71 73 <div translate ng-message="required">attribute.value-required</div>
72 74 </div>
73 75 </md-input-container>
74   - <div layout="column" layout-align="center" flex="60" ng-if="vm.valueType===vm.valueTypes.boolean">
75   - <md-checkbox ng-model="vm.attribute.value" style="margin-bottom: 0px;">
  76 + <div layout="column" layout-align="center" flex="60"
  77 + ng-if="vm.valueType===vm.valueTypes.boolean">
  78 + <md-checkbox ng-model="vm.attribute.value" style="margin-bottom: 0;">
76 79 {{ (vm.attribute.value ? 'value.true' : 'value.false') | translate }}
77 80 </md-checkbox>
78 81 </div>
  82 + <div layout="row" layout-align="center" flex="60"
  83 + ng-if="vm.valueType===vm.valueTypes.json" class="md-block">
  84 + <md-input-container flex class="md-block">
  85 + <label translate>value.json-value</label>
  86 + <input required tb-json-to-string name="value" ng-model="vm.attribute.value">
  87 + <div ng-messages="theForm.value.$error">
  88 + <div translate ng-message="required">attribute.value-required</div>
  89 + </div>
  90 + </md-input-container>
  91 + <md-button class="md-icon-button"
  92 + ng-click="vm.addJSON($event)"
  93 + style="margin: 18px 0;"
  94 + aria-label="{{ 'action.edit' | translate }}">
  95 + <md-tooltip md-direction="top">
  96 + {{ 'action.edit' | translate }}
  97 + </md-tooltip>
  98 + <ng-md-icon size="20" icon="open_in_new"></ng-md-icon>
  99 + </md-button>
  100 + </div>
79 101 </section>
80 102 </fieldset>
81 103 </md-content>
... ... @@ -87,8 +109,8 @@
87 109 class="md-raised md-primary">
88 110 {{ 'action.add' | translate }}
89 111 </md-button>
90   - <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
91   - translate }}
  112 + <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">
  113 + {{ 'action.cancel' | translate }}
92 114 </md-button>
93 115 </md-dialog-actions>
94 116 </form>
... ...
... ... @@ -151,5 +151,4 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog,
151 151 }
152 152 );
153 153 }
154   -
155 154 }
... ...
... ... @@ -26,7 +26,8 @@
26 26 </md-button>
27 27 </div>
28 28 </md-toolbar>
29   - <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
  29 + <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading"
  30 + ng-show="$root.loading"></md-progress-linear>
30 31 <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
31 32 <md-dialog-content>
32 33 <div class="md-dialog-content">
... ... @@ -37,10 +38,10 @@
37 38 <section flex layout="column" style="width: 300px;">
38 39 <span translate style="padding-bottom: 10px;">dashboard.select-existing</span>
39 40 <tb-dashboard-autocomplete the-form="theForm"
40   - ng-disabled="$root.loading || vm.addToDashboardType != 0"
41   - tb-required="vm.addToDashboardType === 0"
42   - ng-model="vm.dashboardId"
43   - select-first-dashboard="false">
  41 + ng-disabled="$root.loading || vm.addToDashboardType != 0"
  42 + tb-required="vm.addToDashboardType === 0"
  43 + ng-model="vm.dashboardId"
  44 + select-first-dashboard="false">
44 45 </tb-dashboard-autocomplete>
45 46 </section>
46 47 </md-radio-button>
... ... @@ -49,7 +50,8 @@
49 50 <span translate>dashboard.create-new</span>
50 51 <md-input-container class="md-block">
51 52 <label translate>dashboard.new-dashboard-title</label>
52   - <input ng-required="vm.addToDashboardType === 1" name="title" ng-model="vm.newDashboard.title">
  53 + <input ng-required="vm.addToDashboardType === 1" name="title"
  54 + ng-model="vm.newDashboard.title">
53 55 <div ng-messages="theForm.title.$error">
54 56 <div translate ng-message="required">dashboard.title-required</div>
55 57 </div>
... ... @@ -66,15 +68,15 @@
66 68 <md-checkbox
67 69 ng-model="vm.openDashboard"
68 70 aria-label="{{ 'dashboard.open-dashboard' | translate }}"
69   - style="margin-bottom: 0px; padding-right: 20px;">
  71 + style="margin-bottom: 0; padding-right: 20px;">
70 72 {{ 'dashboard.open-dashboard' | translate }}
71 73 </md-checkbox>
72 74 <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
73 75 class="md-raised md-primary">
74 76 {{ 'action.add' | translate }}
75 77 </md-button>
76   - <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
77   - translate }}
  78 + <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">
  79 + {{ 'action.cancel' | translate }}
78 80 </md-button>
79 81 </md-dialog-actions>
80 82 </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 +/* eslint-enable import/no-unresolved, import/default */
  17 +
  18 +import './attribute-dialog-edit-json.scss';
  19 +
  20 +/*@ngInject*/
  21 +export default function AttributeDialogEditJsonController($mdDialog, types, jsonValue, readOnly) {
  22 +
  23 + let vm = this;
  24 + vm.json = angular.toJson(jsonValue, 4);
  25 + vm.readOnly = readOnly;
  26 + vm.contentType = types.contentType.JSON.value;
  27 +
  28 + vm.save = () => {
  29 + $mdDialog.hide(angular.fromJson(vm.json));
  30 + };
  31 +
  32 + vm.cancel = () => {
  33 + $mdDialog.cancel();
  34 + };
  35 +}
... ...
  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 +.attribute-edit-json-dialog{
  17 + min-width: 400px;
  18 +}
  19 +
  20 +@media (max-width: 425px){
  21 + .attribute-edit-json-dialog {
  22 + min-width: 200px;
  23 + }
  24 +}
... ...
  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 +<md-dialog class="attribute-edit-json-dialog">
  19 + <form name="theForm" ng-submit="vm.save()">
  20 + <md-toolbar>
  21 + <div class="md-toolbar-tools">
  22 + <h2>{{ 'details.edit-json' | translate }}</h2>
  23 + <span flex></span>
  24 + <md-button class="md-icon-button"
  25 + ng-click="vm.cancel()"
  26 + aria-label="{{'action.close'|translate}}">
  27 + <ng-md-icon icon="close" aria-label="Close"></ng-md-icon>
  28 + </md-button>
  29 + </div>
  30 + </md-toolbar>
  31 + <md-dialog-content>
  32 + <div class="md-dialog-content">
  33 + <tb-json-content
  34 + ng-model="vm.json"
  35 + label="{{ 'value.json-value' | translate }}"
  36 + content-type="vm.contentType"
  37 + validate-content="true"
  38 + hide-error-toast="true"
  39 + fill-height="false"
  40 + ng-readonly="vm.readOnly">
  41 + </tb-json-content>
  42 + </div>
  43 + </md-dialog-content>
  44 + <md-dialog-actions layout="row" layout-align="end center" class="action-buttons">
  45 + <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
  46 + class="md-raised md-primary">
  47 + {{'action.save'|translate}}
  48 + </md-button>
  49 + <md-button id="cancel-btn" ng-disabled="$root.loading" ng-click="vm.cancel()">
  50 + {{'action.cancel'|translate }}
  51 + </md-button>
  52 + </md-dialog-actions>
  53 + </form>
  54 +</md-dialog>
... ...
... ... @@ -39,7 +39,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
39 39
40 40 element.html(template);
41 41
42   - var getAttributeScopeByValue = function(attributeScopeValue) {
  42 + var getAttributeScopeByValue = function (attributeScopeValue) {
43 43 if (scope.types.latestTelemetry.value === attributeScopeValue) {
44 44 return scope.types.latestTelemetry;
45 45 }
... ... @@ -48,7 +48,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
48 48 return scope.attributeScopes[attrScope];
49 49 }
50 50 }
51   - }
  51 + };
52 52
53 53 scope.types = types;
54 54
... ... @@ -87,14 +87,14 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
87 87 search: null
88 88 };
89 89
90   - scope.$watch("entityId", function(newVal) {
  90 + scope.$watch("entityId", function (newVal) {
91 91 if (newVal) {
92 92 scope.resetFilter();
93 93 scope.getEntityAttributes(false, true);
94 94 }
95 95 });
96 96
97   - scope.$watch("attributeScope", function(newVal, prevVal) {
  97 + scope.$watch("attributeScope", function (newVal, prevVal) {
98 98 if (newVal && !angular.equals(newVal, prevVal)) {
99 99 scope.mode = 'default';
100 100 scope.query.search = null;
... ... @@ -103,30 +103,30 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
103 103 }
104 104 });
105 105
106   - scope.resetFilter = function() {
  106 + scope.resetFilter = function () {
107 107 scope.mode = 'default';
108 108 scope.query.search = null;
109 109 scope.selectedAttributes = [];
110 110 scope.attributeScope = getAttributeScopeByValue(attrs.defaultAttributeScope);
111   - }
  111 + };
112 112
113   - scope.enterFilterMode = function(event) {
  113 + scope.enterFilterMode = function (event) {
114 114 let $button = angular.element(event.currentTarget);
115 115 let $toolbarsContainer = $button.closest('.toolbarsContainer');
116 116
117 117 scope.query.search = '';
118 118
119   - $timeout(()=>{
  119 + $timeout(() => {
120 120 $toolbarsContainer.find('.searchInput').focus();
121 121 })
122   - }
  122 + };
123 123
124   - scope.exitFilterMode = function() {
  124 + scope.exitFilterMode = function () {
125 125 scope.query.search = null;
126 126 scope.getEntityAttributes();
127   - }
  127 + };
128 128
129   - scope.$watch("query.search", function(newVal, prevVal) {
  129 + scope.$watch("query.search", function (newVal, prevVal) {
130 130 if (!angular.equals(newVal, prevVal) && scope.query.search != null) {
131 131 scope.getEntityAttributes();
132 132 }
... ... @@ -142,15 +142,15 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
142 142 }
143 143 }
144 144
145   - scope.onReorder = function() {
  145 + scope.onReorder = function () {
146 146 scope.getEntityAttributes(false, false);
147   - }
  147 + };
148 148
149   - scope.onPaginate = function() {
  149 + scope.onPaginate = function () {
150 150 scope.getEntityAttributes(false, false);
151   - }
  151 + };
152 152
153   - scope.getEntityAttributes = function(forceUpdate, reset) {
  153 + scope.getEntityAttributes = function (forceUpdate, reset) {
154 154 if (scope.attributesDeferred) {
155 155 scope.attributesDeferred.resolve();
156 156 }
... ... @@ -163,7 +163,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
163 163 }
164 164 scope.checkSubscription();
165 165 scope.attributesDeferred = attributeService.getEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value,
166   - scope.query, function(attributes, update, apply) {
  166 + scope.query, function (attributes, update, apply) {
167 167 success(attributes, update || forceUpdate, apply);
168 168 }
169 169 );
... ... @@ -176,9 +176,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
176 176 });
177 177 deferred.resolve();
178 178 }
179   - }
  179 + };
180 180
181   - scope.checkSubscription = function() {
  181 + scope.checkSubscription = function () {
182 182 var newSubscriptionId = null;
183 183 if (scope.entityId && scope.entityType && scope.attributeScope.clientSide && scope.mode != 'widget') {
184 184 newSubscriptionId = attributeService.subscribeForEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value);
... ... @@ -187,36 +187,38 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
187 187 attributeService.unsubscribeForEntityAttributes(scope.subscriptionId);
188 188 }
189 189 scope.subscriptionId = newSubscriptionId;
190   - }
  190 + };
191 191
192   - scope.$on('$destroy', function() {
  192 + scope.$on('$destroy', function () {
193 193 if (scope.subscriptionId) {
194 194 attributeService.unsubscribeForEntityAttributes(scope.subscriptionId);
195 195 }
196 196 });
197 197
198   - scope.editAttribute = function($event, attribute) {
  198 + scope.editAttribute = function ($event, attribute) {
199 199 if (!scope.attributeScope.clientSide) {
200 200 $event.stopPropagation();
201 201 $mdEditDialog.show({
202 202 controller: EditAttributeValueController,
203 203 templateUrl: editAttributeValueTemplate,
204   - locals: {attributeValue: attribute.value,
205   - save: function (model) {
206   - var updatedAttribute = angular.copy(attribute);
207   - updatedAttribute.value = model.value;
208   - attributeService.saveEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, [updatedAttribute]).then(
209   - function success() {
210   - scope.getEntityAttributes();
211   - }
212   - );
213   - }},
  204 + locals: {
  205 + attributeValue: attribute.value,
  206 + save: function (model) {
  207 + var updatedAttribute = angular.copy(attribute);
  208 + updatedAttribute.value = model.value;
  209 + attributeService.saveEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, [updatedAttribute]).then(
  210 + function success() {
  211 + scope.getEntityAttributes();
  212 + }
  213 + );
  214 + }
  215 + },
214 216 targetEvent: $event
215 217 });
216 218 }
217   - }
  219 + };
218 220
219   - scope.addAttribute = function($event) {
  221 + scope.addAttribute = function ($event) {
220 222 if (!scope.attributeScope.clientSide) {
221 223 $event.stopPropagation();
222 224 $mdDialog.show({
... ... @@ -224,16 +226,20 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
224 226 controllerAs: 'vm',
225 227 templateUrl: addAttributeDialogTemplate,
226 228 parent: angular.element($document[0].body),
227   - locals: {entityType: scope.entityType, entityId: scope.entityId, attributeScope: scope.attributeScope.value},
  229 + locals: {
  230 + entityType: scope.entityType,
  231 + entityId: scope.entityId,
  232 + attributeScope: scope.attributeScope.value
  233 + },
228 234 fullscreen: true,
229 235 targetEvent: $event
230 236 }).then(function () {
231 237 scope.getEntityAttributes();
232 238 });
233 239 }
234   - }
  240 + };
235 241
236   - scope.deleteAttributes = function($event) {
  242 + scope.deleteAttributes = function ($event) {
237 243 if (!scope.attributeScope.clientSide) {
238 244 $event.stopPropagation();
239 245 var confirm = $mdDialog.confirm()
... ... @@ -244,33 +250,33 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
244 250 .cancel($translate.instant('action.no'))
245 251 .ok($translate.instant('action.yes'));
246 252 $mdDialog.show(confirm).then(function () {
247   - attributeService.deleteEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, scope.selectedAttributes).then(
248   - function success() {
249   - scope.selectedAttributes = [];
250   - scope.getEntityAttributes();
251   - }
252   - )
  253 + attributeService.deleteEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, scope.selectedAttributes).then(
  254 + function success() {
  255 + scope.selectedAttributes = [];
  256 + scope.getEntityAttributes();
  257 + }
  258 + )
253 259 });
254 260 }
255   - }
  261 + };
256 262
257   - scope.nextWidget = function() {
  263 + scope.nextWidget = function () {
258 264 $mdUtil.nextTick(function () {
259 265 if (scope.widgetsCarousel.index < scope.widgetsList.length - 1) {
260 266 scope.widgetsCarousel.index++;
261 267 }
262 268 });
263   - }
  269 + };
264 270
265   - scope.prevWidget = function() {
  271 + scope.prevWidget = function () {
266 272 $mdUtil.nextTick(function () {
267 273 if (scope.widgetsCarousel.index > 0) {
268 274 scope.widgetsCarousel.index--;
269 275 }
270 276 });
271   - }
  277 + };
272 278
273   - scope.enterWidgetMode = function() {
  279 + scope.enterWidgetMode = function () {
274 280
275 281 if (scope.widgetsIndexWatch) {
276 282 scope.widgetsIndexWatch();
... ... @@ -303,7 +309,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
303 309 entitiAliases[entityAlias.id] = entityAlias;
304 310
305 311 var stateController = {
306   - getStateParams: function() {
  312 + getStateParams: function () {
307 313 return {};
308 314 }
309 315 };
... ... @@ -317,9 +323,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
317 323 type: types.datasourceType.entity,
318 324 entityAliasId: entityAlias.id,
319 325 dataKeys: []
320   - }
  326 + };
321 327 var i = 0;
322   - for (var attr =0; attr < scope.selectedAttributes.length;attr++) {
  328 + for (var attr = 0; attr < scope.selectedAttributes.length; attr++) {
323 329 var attribute = scope.selectedAttributes[attr];
324 330 var dataKey = {
325 331 name: attribute.key,
... ... @@ -328,12 +334,12 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
328 334 color: utils.getMaterialColor(i),
329 335 settings: {},
330 336 _hash: Math.random()
331   - }
  337 + };
332 338 datasource.dataKeys.push(dataKey);
333 339 i++;
334 340 }
335 341
336   - scope.widgetsIndexWatch = scope.$watch('widgetsCarousel.index', function(newVal, prevVal) {
  342 + scope.widgetsIndexWatch = scope.$watch('widgetsCarousel.index', function (newVal, prevVal) {
337 343 if (scope.mode === 'widget' && (newVal != prevVal)) {
338 344 var index = scope.widgetsCarousel.index;
339 345 for (var i = 0; i < scope.widgetsList.length; i++) {
... ... @@ -345,7 +351,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
345 351 }
346 352 });
347 353
348   - scope.widgetsBundleWatch = scope.$watch('widgetsBundle', function(newVal, prevVal) {
  354 + scope.widgetsBundleWatch = scope.$watch('widgetsBundle', function (newVal, prevVal) {
349 355 if (scope.mode === 'widget' && (scope.firstBundle === true || newVal != prevVal)) {
350 356 scope.widgetsList = [];
351 357 scope.widgetsListCache = [];
... ... @@ -358,7 +364,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
358 364 widgetService.getBundleWidgetTypes(scope.widgetsBundle.alias, isSystem).then(
359 365 function success(widgetTypes) {
360 366
361   - widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','-createdTime']);
  367 + widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type', '-createdTime']);
362 368
363 369 for (var i = 0; i < widgetTypes.length; i++) {
364 370 var widgetType = widgetTypes[i];
... ... @@ -398,9 +404,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
398 404 }
399 405 }
400 406 });
401   - }
  407 + };
402 408
403   - scope.exitWidgetMode = function() {
  409 + scope.exitWidgetMode = function () {
404 410 if (scope.widgetsBundleWatch) {
405 411 scope.widgetsBundleWatch();
406 412 scope.widgetsBundleWatch = null;
... ... @@ -412,9 +418,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
412 418 scope.selectedWidgetsBundleAlias = null;
413 419 scope.mode = 'default';
414 420 scope.getEntityAttributes(true);
415   - }
  421 + };
416 422
417   - scope.addWidgetToDashboard = function($event) {
  423 + scope.addWidgetToDashboard = function ($event) {
418 424 if (scope.mode === 'widget' && scope.widgetsListCache.length > 0) {
419 425 var widget = scope.widgetsListCache[scope.widgetsCarousel.index][0];
420 426 $event.stopPropagation();
... ... @@ -423,21 +429,26 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
423 429 controllerAs: 'vm',
424 430 templateUrl: addWidgetToDashboardDialogTemplate,
425 431 parent: angular.element($document[0].body),
426   - locals: {entityId: scope.entityId, entityType: scope.entityType, entityName: scope.entityName, widget: angular.copy(widget)},
  432 + locals: {
  433 + entityId: scope.entityId,
  434 + entityType: scope.entityType,
  435 + entityName: scope.entityName,
  436 + widget: angular.copy(widget)
  437 + },
427 438 fullscreen: true,
428 439 targetEvent: $event
429 440 }).then(function () {
430 441
431 442 });
432 443 }
433   - }
  444 + };
434 445
435   - scope.loading = function() {
  446 + scope.loading = function () {
436 447 return $rootScope.loading;
437   - }
  448 + };
438 449
439 450 $compile(element.contents())(scope);
440   - }
  451 + };
441 452
442 453 return {
443 454 restrict: "E",
... ...
... ... @@ -58,3 +58,11 @@ md-toolbar.md-table-toolbar.alternate {
58 58 }
59 59 }
60 60 }
  61 +
  62 +md-edit-dialog.tb-edit-dialog{
  63 + z-index: 78;
  64 +}
  65 +
  66 +md-backdrop.md-edit-dialog-backdrop{
  67 + z-index: 77;
  68 +}
... ...
... ... @@ -77,9 +77,7 @@
77 77 </md-toolbar>
78 78 <md-toolbar class="md-table-toolbar alternate" ng-show="mode==='default' && selectedAttributes.length">
79 79 <div class="md-toolbar-tools">
80   - <span translate="{{attributeScope === types.latestTelemetry
81   - ? 'attribute.selected-telemetry'
82   - : 'attribute.selected-attributes'}}"
  80 + <span translate="{{(attributeScope === types.latestTelemetry) ? 'attribute.selected-telemetry' : 'attribute.selected-attributes'}}"
83 81 translate-values="{count: selectedAttributes.length}"
84 82 translate-interpolation="messageformat"></span>
85 83 <span flex></span>
... ...
... ... @@ -13,14 +13,19 @@
13 13 * See the License for the specific language governing permissions and
14 14 * limitations under the License.
15 15 */
  16 +/* eslint-enable import/no-unresolved, import/default */
  17 +
  18 +import AttributeDialogEditJsonController from "./attribute-dialog-edit-json.controller";
  19 +import attributeDialogEditJsonTemplate from "./attribute-dialog-edit-json.tpl.html";
  20 +
16 21 /*@ngInject*/
17   -export default function EditAttributeValueController($scope, $q, $element, types, attributeValue, save) {
  22 +export default function EditAttributeValueController($scope, $mdDialog, $q, $element, $document, types, attributeValue, save) {
18 23
19 24 $scope.valueTypes = types.valueType;
20 25
21   - $scope.model = {};
22   -
23   - $scope.model.value = attributeValue;
  26 + $scope.model = {
  27 + value: attributeValue
  28 + };
24 29
25 30 if ($scope.model.value === true || $scope.model.value === false) {
26 31 $scope.valueType = types.valueType.boolean;
... ... @@ -30,26 +35,27 @@ export default function EditAttributeValueController($scope, $q, $element, types
30 35 } else {
31 36 $scope.valueType = types.valueType.double;
32 37 }
  38 + } else if (angular.isObject($scope.model.value)) {
  39 + $scope.valueType = types.valueType.json;
33 40 } else {
34 41 $scope.valueType = types.valueType.string;
35 42 }
36 43
37 44 $scope.submit = submit;
38 45 $scope.dismiss = dismiss;
  46 + $scope.editJSON = editJSON;
39 47
40 48 function dismiss() {
41 49 $element.remove();
42 50 }
43 51
44 52 function update() {
45   - if($scope.editDialog.$invalid) {
  53 + if ($scope.editDialog.$invalid) {
46 54 return $q.reject();
47 55 }
48   -
49   - if(angular.isFunction(save)) {
  56 + if (angular.isFunction(save)) {
50 57 return $q.when(save($scope.model));
51 58 }
52   -
53 59 return $q.resolve();
54 60 }
55 61
... ... @@ -59,13 +65,45 @@ export default function EditAttributeValueController($scope, $q, $element, types
59 65 });
60 66 }
61 67
62   - $scope.$watch('valueType', function(newVal, prevVal) {
63   - if (newVal != prevVal) {
  68 +
  69 + $scope.$watch('valueType', function (newVal, prevVal) {
  70 + if (newVal !== prevVal) {
64 71 if ($scope.valueType === types.valueType.boolean) {
65 72 $scope.model.value = false;
  73 + } else if ($scope.valueType === types.valueType.json) {
  74 + $scope.model.value = {};
66 75 } else {
67 76 $scope.model.value = null;
68 77 }
69 78 }
70 79 });
  80 +
  81 + function editJSON($event) {
  82 + $scope.hideDialog = true;
  83 + showJsonDialog($event, $scope.model.value, false).then((response) => {
  84 + $scope.hideDialog = false;
  85 + if (!angular.equals(response, $scope.model.value)) {
  86 + $scope.editDialog.$setDirty();
  87 + }
  88 + $scope.model.value = response;
  89 + })
  90 + }
  91 +
  92 + function showJsonDialog($event, jsonValue, readOnly) {
  93 + if ($event) {
  94 + $event.stopPropagation();
  95 + }
  96 + return $mdDialog.show({
  97 + controller: AttributeDialogEditJsonController,
  98 + controllerAs: 'vm',
  99 + templateUrl: attributeDialogEditJsonTemplate,
  100 + locals: {
  101 + jsonValue: jsonValue,
  102 + readOnly: readOnly
  103 + },
  104 + targetEvent: $event,
  105 + fullscreen: true,
  106 + multiple: true
  107 + });
  108 + }
71 109 }
... ...
... ... @@ -15,8 +15,8 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<md-edit-dialog>
19   - <form name="editDialog" ng-submit="submit()">
  18 +<md-edit-dialog class="tb-edit-dialog">
  19 + <form name="editDialog" ng-submit="submit()" >
20 20 <div layout="column" class="md-content" style="width: 400px;">
21 21 <fieldset>
22 22 <section layout="row">
... ... @@ -38,7 +38,8 @@
38 38 </md-input-container>
39 39 <md-input-container ng-if="valueType===valueTypes.integer" flex="60" class="md-block">
40 40 <label translate>value.integer-value</label>
41   - <input required name="value" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="model.value">
  41 + <input required name="value" type="number" step="1" ng-pattern="/^-?[0-9]+$/"
  42 + ng-model="model.value">
42 43 <div ng-messages="editDialog.value.$error">
43 44 <div translate ng-message="required">attribute.value-required</div>
44 45 <div translate ng-message="pattern">value.invalid-integer-value</div>
... ... @@ -52,16 +53,31 @@
52 53 </div>
53 54 </md-input-container>
54 55 <div layout="column" layout-align="center" flex="60" ng-if="valueType===valueTypes.boolean">
55   - <md-checkbox ng-model="model.value" style="margin-bottom: 0px;">
  56 + <md-checkbox ng-model="model.value" style="margin-bottom: 0;">
56 57 {{ (model.value ? 'value.true' : 'value.false') | translate }}
57 58 </md-checkbox>
58 59 </div>
  60 + <div layout="row" layout-align="center" ng-if="valueType===valueTypes.json" flex="60">
  61 + <md-input-container class="md-block">
  62 + <label translate>value.json-value</label>
  63 + <input required tb-json-to-string name="value" ng-model="model.value">
  64 + <div ng-messages="editDialog.value.$error">
  65 + <div translate ng-message="required">attribute.value-required</div>
  66 + </div>
  67 + </md-input-container>
  68 + <md-button class="md-icon-button" style="margin: 18px 0;"
  69 + ng-click="editJSON($event)" aria-label="{{ 'action.edit' | translate }}">
  70 + <md-tooltip md-direction="top">
  71 + {{ 'action.edit' | translate }}
  72 + </md-tooltip>
  73 + <ng-md-icon size="20" icon="open_in_new"></ng-md-icon>
  74 + </md-button>
  75 + </div>
59 76 </section>
60 77 </fieldset>
61 78 </div>
62 79 <div layout="row" layout-align="end" class="md-actions">
63   - <md-button ng-click="dismiss()">{{ 'action.cancel' |
64   - translate }}
  80 + <md-button ng-click="dismiss()">{{ 'action.cancel' | translate }}
65 81 </md-button>
66 82 <md-button ng-disabled="editDialog.$invalid || !editDialog.$dirty" type="submit"
67 83 class="md-raised md-primary">
... ... @@ -69,4 +85,4 @@
69 85 </md-button>
70 86 </div>
71 87 </form>
72   -</md-edit-dialog>
\ No newline at end of file
  88 +</md-edit-dialog>
... ...
... ... @@ -2232,7 +2232,7 @@
2232 2232 "last": "Τελευταίος",
2233 2233 "time-period": "Χρονική Περίοδος"
2234 2234 },
2235   - "user": {
  2235 + "user": {
2236 2236 "user": "Χρήστης",
2237 2237 "users": "Χρήστες",
2238 2238 "management": "Διαχείριση Χρηστών",
... ...
... ... @@ -607,6 +607,7 @@
607 607 },
608 608 "details": {
609 609 "edit-mode": "Edit mode",
  610 + "edit-json": "Edit JSON",
610 611 "toggle-edit-mode": "Toggle edit mode"
611 612 },
612 613 "device": {
... ... @@ -1270,7 +1271,8 @@
1270 1271 "js-func": {
1271 1272 "no-return-error": "Function must return value!",
1272 1273 "return-type-mismatch": "Function must return value of '{{type}}' type!",
1273   - "tidy": "Tidy"
  1274 + "tidy": "Tidy",
  1275 + "mini": "Mini"
1274 1276 },
1275 1277 "key-val": {
1276 1278 "key": "Key",
... ... @@ -1593,7 +1595,9 @@
1593 1595 "boolean-value": "Boolean value",
1594 1596 "false": "False",
1595 1597 "true": "True",
1596   - "long": "Long"
  1598 + "long": "Long",
  1599 + "json": "JSON",
  1600 + "json-value": "JSON value"
1597 1601 },
1598 1602 "widget": {
1599 1603 "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": "Ключ",
... ...