Commit 6091f1a8d8806916b7635035a243c7c799ae52be
1 parent
a568564e
Add new widget 'multiple-widget' to bundle 'input_widgets'
Showing
9 changed files
with
397 additions
and
7 deletions
... | ... | @@ -196,6 +196,22 @@ |
196 | 196 | "dataKeySettingsSchema": "{}\n", |
197 | 197 | "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\":\"Update integer timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" |
198 | 198 | } |
199 | + }, | |
200 | + { | |
201 | + "alias": "update_multiple_attributes", | |
202 | + "name": "Update Multiple Attributes", | |
203 | + "descriptor": { | |
204 | + "type": "latest", | |
205 | + "sizeX": 7.5, | |
206 | + "sizeY": 3.5, | |
207 | + "resources": [], | |
208 | + "templateHtml": "<tb-multiple-input-widget \n form-id=\"formId\"\n ctx=\"ctx\">\n</tb-multiple-input-widget>", | |
209 | + "templateCss": "", | |
210 | + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\n\r\nself.onInit = function() {\r\n var scope = self.ctx.$scope;\r\n var id = self.ctx.$scope.$injector.get('utils').guid();\r\n scope.formId = \"form-\"+id;\r\n scope.ctx = self.ctx;\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.$broadcast('multiple-input-data-updated', self.ctx.$scope.formId);\r\n}\r\n", | |
211 | + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"MultipleInput\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Multiple input title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"attributesShared\": {\n \"title\": \"Attributes are 'shared' (default value is 'server')\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"attributesShared\",\n \"showResultMessage\"\n ]\n}", | |
212 | + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"inputTypeNumber\": {\n \"title\": \"Datakey is a number\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"step\": {\n \"title\": \"Step interval between valid values (only for numbers)\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"inputTypeNumber\",\n \"step\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t},\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n }\n ]\n}\n", | |
213 | + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update Multiple Attributes\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" | |
214 | + } | |
199 | 215 | } |
200 | 216 | ] |
201 | -} | |
\ No newline at end of file | ||
217 | +} | ... | ... |
... | ... | @@ -24,6 +24,7 @@ import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget' |
24 | 24 | import thingsboardEntitiesHierarchyWidget from '../widget/lib/entities-hierarchy-widget'; |
25 | 25 | import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget'; |
26 | 26 | import thingsboardDateRangeNavigatorWidget from '../widget/lib/date-range-navigator/date-range-navigator'; |
27 | +import thingsboardMultipleInputWidget from '../widget/lib/multiple-input-widget'; | |
27 | 28 | |
28 | 29 | import thingsboardRpcWidgets from '../widget/lib/rpc'; |
29 | 30 | |
... | ... | @@ -49,7 +50,7 @@ import thingsboardUtils from '../common/utils.service'; |
49 | 50 | export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, |
50 | 51 | thingsboardTimeseriesTableWidget, thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, |
51 | 52 | thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardDateRangeNavigatorWidget, |
52 | - thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget]) | |
53 | + thingsboardMultipleInputWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget]) | |
53 | 54 | .factory('widgetService', WidgetService) |
54 | 55 | .name; |
55 | 56 | ... | ... |
... | ... | @@ -49,7 +49,8 @@ |
49 | 49 | "import": "Import", |
50 | 50 | "export": "Export", |
51 | 51 | "share-via": "Share via {{provider}}", |
52 | - "continue": "Continue" | |
52 | + "continue": "Continue", | |
53 | + "discard-changes": "Discard Changes" | |
53 | 54 | }, |
54 | 55 | "aggregation": { |
55 | 56 | "aggregation": "Aggregation", |
... | ... | @@ -1694,4 +1695,4 @@ |
1694 | 1695 | "cs_CZ": "Czech" |
1695 | 1696 | } |
1696 | 1697 | } |
1697 | -} | |
\ No newline at end of file | ||
1698 | +} | ... | ... |
... | ... | @@ -48,7 +48,8 @@ |
48 | 48 | "paste-reference": "Pegar referencia", |
49 | 49 | "import": "Importar", |
50 | 50 | "export": "Exportar", |
51 | - "share-via": "Compartir via {{provider}}" | |
51 | + "share-via": "Compartir via {{provider}}", | |
52 | + "discard-changes": "Cancelar los cambios" | |
52 | 53 | }, |
53 | 54 | "aggregation": { |
54 | 55 | "aggregation": "Agregación", | ... | ... |
... | ... | @@ -48,7 +48,8 @@ |
48 | 48 | "paste-reference": "Incolla riferimento", |
49 | 49 | "import": "Importa", |
50 | 50 | "export": "Esporta", |
51 | - "share-via": "Condividi con {{provider}}" | |
51 | + "share-via": "Condividi con {{provider}}", | |
52 | + "discard-changes": "Annulla le modifiche" | |
52 | 53 | }, |
53 | 54 | "aggregation": { |
54 | 55 | "aggregation": "Aggregazione", | ... | ... |
1 | +/* | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +import './multiple-input-widget.scss'; | |
17 | + | |
18 | +/* eslint-disable import/no-unresolved, import/default */ | |
19 | + | |
20 | +import multipleInputWidgetTemplate from './multiple-input-widget.tpl.html'; | |
21 | + | |
22 | +/* eslint-enable import/no-unresolved, import/default */ | |
23 | + | |
24 | +export default angular.module('thingsboard.widgets.multipleInputWidget', []) | |
25 | + .directive('tbMultipleInputWidget', MultipleInputWidget) | |
26 | + .name; | |
27 | + | |
28 | +/*@ngInject*/ | |
29 | +function MultipleInputWidget() { | |
30 | + return { | |
31 | + restrict: "E", | |
32 | + scope: true, | |
33 | + bindToController: { | |
34 | + formId: '=', | |
35 | + ctx: '=' | |
36 | + }, | |
37 | + controller: MultipleInputWidgetController, | |
38 | + controllerAs: 'vm', | |
39 | + templateUrl: multipleInputWidgetTemplate | |
40 | + }; | |
41 | +} | |
42 | + | |
43 | +/*@ngInject*/ | |
44 | +function MultipleInputWidgetController($q, $scope, attributeService, toast, types, utils) { | |
45 | + var vm = this; | |
46 | + | |
47 | + vm.dataKeyDetected = false; | |
48 | + vm.hasAnyChange = false; | |
49 | + vm.entityDetected = false; | |
50 | + vm.isValidParameter = true; | |
51 | + vm.message = 'No entity selected'; | |
52 | + | |
53 | + vm.rows = []; | |
54 | + vm.rowIndex = 0; | |
55 | + | |
56 | + vm.datasources = null; | |
57 | + | |
58 | + vm.cellStyle = cellStyle; | |
59 | + vm.discardAll = discardAll; | |
60 | + vm.inputChanged = inputChanged; | |
61 | + vm.postData = postData; | |
62 | + | |
63 | + $scope.$watch('vm.ctx', function() { | |
64 | + if (vm.ctx && vm.ctx.defaultSubscription) { | |
65 | + vm.settings = vm.ctx.settings; | |
66 | + vm.widgetConfig = vm.ctx.widgetConfig; | |
67 | + vm.subscription = vm.ctx.defaultSubscription; | |
68 | + vm.datasources = vm.subscription.datasources; | |
69 | + initializeConfig(); | |
70 | + updateDatasources(); | |
71 | + } | |
72 | + }); | |
73 | + | |
74 | + $scope.$on('multiple-input-data-updated', function(event, formId) { | |
75 | + if (vm.formId == formId) { | |
76 | + updateRowData(vm.subscription.data); | |
77 | + $scope.$digest(); | |
78 | + } | |
79 | + }); | |
80 | + | |
81 | + function defaultStyle() { | |
82 | + return {}; | |
83 | + } | |
84 | + | |
85 | + function cellStyle(key) { | |
86 | + var style = {}; | |
87 | + if (key) { | |
88 | + var styleInfo = vm.stylesInfo[key.label]; | |
89 | + var value = key.currentValue; | |
90 | + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { | |
91 | + try { | |
92 | + style = styleInfo.cellStyleFunction(value); | |
93 | + } catch (e) { | |
94 | + style = {}; | |
95 | + } | |
96 | + } else { | |
97 | + style = defaultStyle(); | |
98 | + } | |
99 | + } | |
100 | + return style; | |
101 | + } | |
102 | + | |
103 | + function discardAll() { | |
104 | + for (var r = 0; r < vm.rows.length; r++) { | |
105 | + var row = vm.rows[r]; | |
106 | + for (var d = 0; d < row.data.length; d++ ) { | |
107 | + row.data[d].currentValue = row.data[d].originalValue; | |
108 | + } | |
109 | + } | |
110 | + vm.hasAnyChange = false; | |
111 | + } | |
112 | + | |
113 | + function inputChanged() { | |
114 | + var newValue = false; | |
115 | + for (var r = 0; r < vm.rows.length; r++) { | |
116 | + var row = vm.rows[r]; | |
117 | + for (var d = 0; d < row.data.length; d++ ) { | |
118 | + if (!row.data[d].currentValue) { | |
119 | + return; | |
120 | + } | |
121 | + if (row.data[d].currentValue !== row.data[d].originalValue) { | |
122 | + newValue = true; | |
123 | + } | |
124 | + } | |
125 | + } | |
126 | + vm.hasAnyChange = newValue; | |
127 | + } | |
128 | + | |
129 | + function postData() { | |
130 | + var promises = []; | |
131 | + for (var r = 0; r < vm.rows.length; r++) { | |
132 | + var row = vm.rows[r]; | |
133 | + var datasource = row.datasource; | |
134 | + var attributes = []; | |
135 | + var newValues = false; | |
136 | + | |
137 | + for (var d = 0; d < row.data.length; d++ ) { | |
138 | + if (row.data[d].currentValue !== row.data[d].originalValue) { | |
139 | + attributes.push({ | |
140 | + key : row.data[d].name, | |
141 | + value : row.data[d].currentValue, | |
142 | + }); | |
143 | + newValues = true; | |
144 | + } | |
145 | + } | |
146 | + | |
147 | + if (newValues) { | |
148 | + promises.push(attributeService.saveEntityAttributes( | |
149 | + datasource.entityType, | |
150 | + datasource.entityId, | |
151 | + vm.attributeScope, | |
152 | + attributes)); | |
153 | + } | |
154 | + } | |
155 | + | |
156 | + if (promises.length) { | |
157 | + $q.all(promises).then( | |
158 | + function success() { | |
159 | + for (var d = 0; d < row.data.length; d++ ) { | |
160 | + row.data[d].originalValue = row.data[d].currentValue; | |
161 | + } | |
162 | + vm.hasAnyChange = false; | |
163 | + if (vm.settings.showResultMessage) { | |
164 | + toast.showSuccess('Update successful', 1000, angular.element(vm.ctx.$container), 'bottom left'); | |
165 | + } | |
166 | + }, | |
167 | + function fail() { | |
168 | + if (vm.settings.showResultMessage) { | |
169 | + toast.showError('Update failed', angular.element(vm.ctx.$container), 'bottom left'); | |
170 | + } | |
171 | + } | |
172 | + ); | |
173 | + } | |
174 | + } | |
175 | + | |
176 | + function initializeConfig() { | |
177 | + | |
178 | + if (vm.settings.widgetTitle && vm.settings.widgetTitle.length) { | |
179 | + vm.widgetTitle = utils.customTranslation(vm.settings.widgetTitle, vm.settings.widgetTitle); | |
180 | + } else { | |
181 | + vm.widgetTitle = vm.ctx.widgetConfig.title; | |
182 | + } | |
183 | + | |
184 | + vm.ctx.widgetTitle = vm.widgetTitle; | |
185 | + | |
186 | + vm.attributeScope = vm.settings.attributesShared ? types.attributesScope.shared.value : types.attributesScope.server.value; | |
187 | + } | |
188 | + | |
189 | + function updateDatasources() { | |
190 | + | |
191 | + vm.stylesInfo = {}; | |
192 | + vm.rows = []; | |
193 | + vm.rowIndex = 0; | |
194 | + | |
195 | + if (vm.datasources) { | |
196 | + vm.entityDetected = true; | |
197 | + for (var ds = 0; ds < vm.datasources.length; ds++) { | |
198 | + var row = {}; | |
199 | + var datasource = vm.datasources[ds]; | |
200 | + row.datasource = datasource; | |
201 | + row.data = []; | |
202 | + if (datasource.dataKeys) { | |
203 | + vm.dataKeyDetected = true; | |
204 | + for (var a = 0; a < datasource.dataKeys.length; a++ ) { | |
205 | + var dataKey = datasource.dataKeys[a]; | |
206 | + | |
207 | + if (dataKey.units) { | |
208 | + dataKey.label += ' (' + dataKey.units + ')'; | |
209 | + } | |
210 | + | |
211 | + var keySettings = dataKey.settings; | |
212 | + if (keySettings.inputTypeNumber) { | |
213 | + keySettings.inputType = 'number'; | |
214 | + } else { | |
215 | + keySettings.inputType = 'text'; | |
216 | + } | |
217 | + | |
218 | + var cellStyleFunction = null; | |
219 | + var useCellStyleFunction = false; | |
220 | + | |
221 | + if (keySettings.useCellStyleFunction === true) { | |
222 | + if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) { | |
223 | + try { | |
224 | + cellStyleFunction = new Function('value', keySettings.cellStyleFunction); | |
225 | + useCellStyleFunction = true; | |
226 | + } catch (e) { | |
227 | + cellStyleFunction = null; | |
228 | + useCellStyleFunction = false; | |
229 | + } | |
230 | + } | |
231 | + } | |
232 | + | |
233 | + vm.stylesInfo[dataKey.label] = { | |
234 | + useCellStyleFunction: useCellStyleFunction, | |
235 | + cellStyleFunction: cellStyleFunction | |
236 | + }; | |
237 | + | |
238 | + row.data.push(dataKey); | |
239 | + } | |
240 | + vm.rows.push(row); | |
241 | + } | |
242 | + } | |
243 | + } | |
244 | + } | |
245 | + | |
246 | + function updateRowData(data) { | |
247 | + var dataIndex = 0; | |
248 | + for (var r = 0; r < vm.rows.length; r++) { | |
249 | + var row = vm.rows[r]; | |
250 | + for (var d = 0; d < row.data.length; d++ ) { | |
251 | + var keyData = data[dataIndex++].data; | |
252 | + if (keyData && keyData.length && keyData[0].length > 1) { | |
253 | + row.data[d].currentValue = row.data[d].originalValue = keyData[0][1]; | |
254 | + } | |
255 | + } | |
256 | + } | |
257 | + } | |
258 | + | |
259 | +} | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +.tb-multiple-input { | |
17 | + height: 100%; | |
18 | + | |
19 | + .md-button.md-icon-button { | |
20 | + width: 32px; | |
21 | + min-width: 32px; | |
22 | + height: 32px; | |
23 | + min-height: 32px; | |
24 | + padding: 0 !important; | |
25 | + margin: 0; | |
26 | + line-height: 20px; | |
27 | + } | |
28 | + | |
29 | + .md-icon-button md-icon { | |
30 | + width: 20px; | |
31 | + min-width: 20px; | |
32 | + height: 20px; | |
33 | + min-height: 20px; | |
34 | + font-size: 20px; | |
35 | + | |
36 | + &:not([disabled]) { | |
37 | + color: #f66; | |
38 | + } | |
39 | + } | |
40 | +} | |
41 | + | |
42 | +md-toast { | |
43 | + min-width: 0; | |
44 | + | |
45 | + .md-toast-content { | |
46 | + font-size: 14px !important; | |
47 | + } | |
48 | +} | |
49 | + | |
50 | +.footer { | |
51 | + position: absolute; | |
52 | + bottom: 0; | |
53 | + left: 0; | |
54 | + width: 100%; | |
55 | +} | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2019 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<form class="tb-multiple-input" name="multipleInputForm" ng-submit="vm.postData($event)"> | |
19 | + <div style="padding: 0 8px; margin: auto 0;"> | |
20 | + <div ng-show="vm.entityDetected" layout="row" flex ng-repeat="row in vm.rows track by $index"> | |
21 | + <div layout="column" flex ng-repeat="key in row.data track by $index"> | |
22 | + <md-input-container class="md-icon-float" ng-style="vm.cellStyle(key)"> | |
23 | + <label>{{key.label}}</label> | |
24 | + <md-icon class="material-icons" ng-if="key.settings.icon"> | |
25 | + {{key.settings.icon}} | |
26 | + </md-icon> | |
27 | + <input name="key.name" | |
28 | + ng-model="key.currentValue" | |
29 | + type="{{key.settings.inputType}}" | |
30 | + step="{{key.settings.step}}" | |
31 | + md-select-on-focus | |
32 | + ng-change="vm.inputChanged()"> | |
33 | + </md-input-container> | |
34 | + </div> | |
35 | + </div> | |
36 | + | |
37 | + <div style="text-align: center; font-size: 18px; color: #a0a0a0;" ng-hide="vm.entityDetected" ng-bind="vm.message" | |
38 | + ></div> | |
39 | + <div style="text-align: center; font-size: 18px; color: #a0a0a0;" ng-show="vm.entityDetected && !vm.dataKeyDetected"> | |
40 | + No attribute is selected | |
41 | + </div> | |
42 | + <div style="text-align: center; font-size: 18px; color: #a0a0a0;" ng-show="vm.entityDetected && !vm.isValidParameter"> | |
43 | + Timeseries parameter cannot be used in this widget | |
44 | + </div> | |
45 | + </div> | |
46 | + <div class="footer md-padding" layout="row" layout-align="end center" ng-show="vm.entityDetected && vm.dataKeyDetected"> | |
47 | + <md-button class="md-primary" ng-click="vm.discardAll()" style="max-height: 50px;margin-right:20px;" ng-disabled="!vm.hasAnyChange"> | |
48 | + {{ 'action.discard-changes' | translate }} | |
49 | + </md-button> | |
50 | + <md-button class="md-raised md-primary" type="submit" value="Submit" style="max-height: 50px;margin-right:20px;" | |
51 | + ng-disabled="!vm.hasAnyChange" ng-click="vm.isFocused = false"> | |
52 | + {{ 'action.save' | translate }} | |
53 | + </md-button> | |
54 | + </div> | |
55 | +</form> | |
\ No newline at end of file | ... | ... |