diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json
index 6dc7f2aad0..cec243c975 100644
--- a/application/src/main/data/json/system/widget_bundles/input_widgets.json
+++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json
@@ -196,6 +196,22 @@
"dataKeySettingsSchema": "{}\n",
"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\":{}}"
}
+ },
+ {
+ "alias": "update_multiple_attributes",
+ "name": "Update Multiple Attributes",
+ "descriptor": {
+ "type": "latest",
+ "sizeX": 7.5,
+ "sizeY": 3.5,
+ "resources": [],
+ "templateHtml": "\n",
+ "templateCss": "",
+ "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",
+ "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}",
+ "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"readOnly\": {\n \"title\": \"Value is read only\",\n \"type\": \"boolean\",\n \"default\": false\n },\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 \"readOnly\",\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",
+ "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\":{}}"
+ }
}
]
-}
\ No newline at end of file
+}
diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js
index 4859f7f409..ea59130484 100644
--- a/ui/src/app/api/widget.service.js
+++ b/ui/src/app/api/widget.service.js
@@ -24,6 +24,7 @@ import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget'
import thingsboardEntitiesHierarchyWidget from '../widget/lib/entities-hierarchy-widget';
import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget';
import thingsboardDateRangeNavigatorWidget from '../widget/lib/date-range-navigator/date-range-navigator';
+import thingsboardMultipleInputWidget from '../widget/lib/multiple-input-widget';
import thingsboardRpcWidgets from '../widget/lib/rpc';
@@ -49,7 +50,7 @@ import thingsboardUtils from '../common/utils.service';
export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight,
thingsboardTimeseriesTableWidget, thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget,
thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardDateRangeNavigatorWidget,
- thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget])
+ thingsboardMultipleInputWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget])
.factory('widgetService', WidgetService)
.name;
diff --git a/ui/src/app/components/json-form.directive.js b/ui/src/app/components/json-form.directive.js
index f653f576ed..84f62f939a 100644
--- a/ui/src/app/components/json-form.directive.js
+++ b/ui/src/app/components/json-form.directive.js
@@ -22,13 +22,17 @@ import ReactSchemaForm from './react/json-form-react.jsx';
import jsonFormTemplate from './json-form.tpl.html';
import { utils } from 'react-schema-form';
+import MaterialIconsDialogController from './material-icons-dialog.controller';
+import materialIconsDialogTemplate from './material-icons-dialog.tpl.html';
+
export default angular.module('thingsboard.directives.jsonForm', [])
.directive('tbJsonForm', JsonForm)
+ .controller('MaterialIconsDialogController', MaterialIconsDialogController)
.value('ReactSchemaForm', ReactSchemaForm)
.name;
/*@ngInject*/
-function JsonForm($compile, $templateCache, $mdColorPicker) {
+function JsonForm($compile, $templateCache, $mdColorPicker, $mdDialog, $document) {
var linker = function (scope, element) {
@@ -90,6 +94,9 @@ function JsonForm($compile, $templateCache, $mdColorPicker) {
onColorClick: function(event, key, val) {
scope.showColorPicker(event, val);
},
+ onIconClick: function(event) {
+ scope.openIconDialog(event);
+ },
onToggleFullscreen: function() {
scope.isFullscreen = !scope.isFullscreen;
scope.formProps.isFullscreen = scope.isFullscreen;
@@ -123,6 +130,23 @@ function JsonForm($compile, $templateCache, $mdColorPicker) {
});
}
+ scope.openIconDialog = function(event) {
+ $mdDialog.show({
+ controller: 'MaterialIconsDialogController',
+ controllerAs: 'vm',
+ templateUrl: materialIconsDialogTemplate,
+ parent: angular.element($document[0].body),
+ locals: {icon: scope.icon},
+ multiple: true,
+ fullscreen: true,
+ targetEvent: event
+ }).then(function (icon) {
+ if (event.data && event.data.onValueChanged) {
+ event.data.onValueChanged(icon);
+ }
+ });
+ }
+
scope.onFullscreenChanged = function() {}
scope.validate = function(){
diff --git a/ui/src/app/components/react/json-form-array.jsx b/ui/src/app/components/react/json-form-array.jsx
index 46f6457b0f..839b55a4fb 100644
--- a/ui/src/app/components/react/json-form-array.jsx
+++ b/ui/src/app/components/react/json-form-array.jsx
@@ -131,7 +131,7 @@ class ThingsboardArray extends React.Component {
}
let forms = this.props.form.items.map(function(form, index){
var copy = this.copyWithIndex(form, i);
- return this.props.builder(copy, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder);
+ return this.props.builder(copy, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder);
}.bind(this));
arrays.push(
diff --git a/ui/src/app/components/react/json-form-fieldset.jsx b/ui/src/app/components/react/json-form-fieldset.jsx
index 5a0f94017c..3c99ca4be7 100644
--- a/ui/src/app/components/react/json-form-fieldset.jsx
+++ b/ui/src/app/components/react/json-form-fieldset.jsx
@@ -19,7 +19,7 @@ class ThingsboardFieldSet extends React.Component {
render() {
let forms = this.props.form.items.map(function(form, index){
- return this.props.builder(form, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder);
+ return this.props.builder(form, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder);
}.bind(this));
return (
diff --git a/ui/src/app/components/react/json-form-icon.jsx b/ui/src/app/components/react/json-form-icon.jsx
new file mode 100644
index 0000000000..44cf321fe0
--- /dev/null
+++ b/ui/src/app/components/react/json-form-icon.jsx
@@ -0,0 +1,134 @@
+/*
+ * Copyright © 2016-2019 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import $ from 'jquery';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import ThingsboardBaseComponent from './json-form-base-component.jsx';
+import reactCSS from 'reactcss';
+import TextField from 'material-ui/TextField';
+import IconButton from 'material-ui/IconButton';
+
+class ThingsboardIcon extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.onValueChanged = this.onValueChanged.bind(this);
+ this.onIconClick = this.onIconClick.bind(this);
+ this.onClear = this.onClear.bind(this);
+ var icon = props.value ? props.value : '';
+ this.state = {
+ icon: icon
+ };
+ }
+
+ componentDidMount() {
+ var node = ReactDOM.findDOMNode(this);
+ var iconContainer = $(node).children('#icon-container');
+ iconContainer.click(this, function(event) {
+ event.data.onIconClick(event);
+ });
+ }
+
+ componentWillUnmount () {
+ var node = ReactDOM.findDOMNode(this);
+ var iconContainer = $(node).children('#icon-container');
+ iconContainer.off( "click" );
+ }
+
+ onValueChanged(value) {
+ var icon = value;
+
+ this.setState({
+ icon: value
+ })
+ this.props.onChange(this.props.form.key, value);
+ }
+
+ onIconClick(event) {
+ this.props.onIconClick(event);
+ }
+
+ onClear(event) {
+ if (event) {
+ event.stopPropagation();
+ }
+ this.onValueChanged('');
+ }
+
+ render() {
+
+ const styles = reactCSS({
+ 'default': {
+ clear: {
+ marginTop: '15px'
+ },
+ container: {
+ display: 'flex'
+ },
+ icon: {
+ display: 'inline-block',
+ marginRight: '10px',
+ marginTop: '16px',
+ marginBottom: 'auto',
+ cursor: 'pointer',
+ border: 'solid 1px rgba(0, 0, 0, .27)'
+ },
+ iconContainer: {
+ display: 'flex',
+ width: '100%'
+ },
+ iconText: {
+ display: 'inline-block',
+ width: '100%'
+ },
+ },
+ });
+
+ var fieldClass = "tb-field";
+ if (this.props.form.required) {
+ fieldClass += " tb-required";
+ }
+ if (this.state.focused) {
+ fieldClass += " tb-focused";
+ }
+
+ var pickedIcon = 'more_horiz';
+ if (this.state.icon != '') {
+ pickedIcon = this.state.icon;
+ }
+
+ return (
+
+
+
+ {pickedIcon}
+
+
+
+
clear
+
+ );
+ }
+}
+
+export default ThingsboardBaseComponent(ThingsboardIcon);
diff --git a/ui/src/app/components/react/json-form-react.jsx b/ui/src/app/components/react/json-form-react.jsx
index ad73359199..3c11f5f850 100644
--- a/ui/src/app/components/react/json-form-react.jsx
+++ b/ui/src/app/components/react/json-form-react.jsx
@@ -52,6 +52,7 @@ ReactSchemaForm.propTypes = {
option: React.PropTypes.object,
onModelChange: React.PropTypes.func,
onColorClick: React.PropTypes.func,
+ onIconClick: React.PropTypes.func,
onToggleFullscreen: React.PropTypes.func
}
diff --git a/ui/src/app/components/react/json-form-schema-form.jsx b/ui/src/app/components/react/json-form-schema-form.jsx
index 8d9ec3e874..13c4c97a21 100644
--- a/ui/src/app/components/react/json-form-schema-form.jsx
+++ b/ui/src/app/components/react/json-form-schema-form.jsx
@@ -32,6 +32,7 @@ import ThingsboardImage from './json-form-image.jsx';
import ThingsboardCheckbox from './json-form-checkbox.jsx';
import Help from 'react-schema-form/lib/Help';
import ThingsboardFieldSet from './json-form-fieldset.jsx';
+import ThingsboardIcon from './json-form-icon.jsx';
import _ from 'lodash';
@@ -58,11 +59,13 @@ class ThingsboardSchemaForm extends React.Component {
'css': ThingsboardCss,
'color': ThingsboardColor,
'rc-select': ThingsboardRcSelect,
- 'fieldset': ThingsboardFieldSet
+ 'fieldset': ThingsboardFieldSet,
+ 'icon': ThingsboardIcon
};
this.onChange = this.onChange.bind(this);
this.onColorClick = this.onColorClick.bind(this);
+ this.onIconClick = this.onIconClick.bind(this);
this.onToggleFullscreen = this.onToggleFullscreen.bind(this);
this.hasConditions = false;
}
@@ -79,12 +82,16 @@ class ThingsboardSchemaForm extends React.Component {
this.props.onColorClick(event, key, val);
}
+ onIconClick(event) {
+ this.props.onIconClick(event);
+ }
+
onToggleFullscreen() {
this.props.onToggleFullscreen();
}
- builder(form, model, index, onChange, onColorClick, onToggleFullscreen, mapper) {
+ builder(form, model, index, onChange, onColorClick, onIconClick, onToggleFullscreen, mapper) {
var type = form.type;
let Field = this.mapper[type];
if(!Field) {
@@ -97,7 +104,7 @@ class ThingsboardSchemaForm extends React.Component {
return null;
}
}
- return
+ return
}
createSchema(theForm) {
@@ -107,7 +114,7 @@ class ThingsboardSchemaForm extends React.Component {
mapper = _.merge(this.mapper, this.props.mapper);
}
let forms = merged.map(function(form, index) {
- return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, this.onToggleFullscreen, mapper);
+ return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, this.onIconClick, this.onToggleFullscreen, mapper);
}.bind(this));
let formClass = 'SchemaForm';
@@ -158,4 +165,4 @@ class ThingsboardSchemaGroup extends React.Component{
{this.props.forms}
);
}
-}
\ No newline at end of file
+}
diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json
index 689bd149ef..af49a7b0b3 100644
--- a/ui/src/app/locale/locale.constant-en_US.json
+++ b/ui/src/app/locale/locale.constant-en_US.json
@@ -49,7 +49,8 @@
"import": "Import",
"export": "Export",
"share-via": "Share via {{provider}}",
- "continue": "Continue"
+ "continue": "Continue",
+ "discard-changes": "Discard Changes"
},
"aggregation": {
"aggregation": "Aggregation",
diff --git a/ui/src/app/locale/locale.constant-es_ES.json b/ui/src/app/locale/locale.constant-es_ES.json
index b36b41616f..fbda6661af 100644
--- a/ui/src/app/locale/locale.constant-es_ES.json
+++ b/ui/src/app/locale/locale.constant-es_ES.json
@@ -49,6 +49,7 @@
"import": "Importar",
"export": "Exportar",
"share-via": "Compartir vía {{provider}}",
+ "discard-changes": "Cancelar los cambios",
"continue": "Continuar"
},
"aggregation": {
diff --git a/ui/src/app/locale/locale.constant-fr_FR.json b/ui/src/app/locale/locale.constant-fr_FR.json
index 5dc4dfbc13..9713de7626 100644
--- a/ui/src/app/locale/locale.constant-fr_FR.json
+++ b/ui/src/app/locale/locale.constant-fr_FR.json
@@ -48,7 +48,8 @@
"undo": "Annuler",
"update": "mise à jour",
"view": "Afficher",
- "yes": "Oui"
+ "yes": "Oui",
+ "discard-changes": "Annuler les modifications"
},
"admin": {
"base-url": "URL de base",
diff --git a/ui/src/app/locale/locale.constant-it_IT.json b/ui/src/app/locale/locale.constant-it_IT.json
index 71b14747d7..f2f04c47a9 100644
--- a/ui/src/app/locale/locale.constant-it_IT.json
+++ b/ui/src/app/locale/locale.constant-it_IT.json
@@ -48,7 +48,8 @@
"paste-reference": "Incolla riferimento",
"import": "Importa",
"export": "Esporta",
- "share-via": "Condividi con {{provider}}"
+ "share-via": "Condividi con {{provider}}",
+ "discard-changes": "Annulla le modifiche"
},
"aggregation": {
"aggregation": "Aggregazione",
diff --git a/ui/src/app/widget/lib/multiple-input-widget.js b/ui/src/app/widget/lib/multiple-input-widget.js
new file mode 100644
index 0000000000..ad8972ff7d
--- /dev/null
+++ b/ui/src/app/widget/lib/multiple-input-widget.js
@@ -0,0 +1,288 @@
+/*
+ * Copyright © 2016-2019 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './multiple-input-widget.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import multipleInputWidgetTemplate from './multiple-input-widget.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.widgets.multipleInputWidget', [])
+ .directive('tbMultipleInputWidget', MultipleInputWidget)
+ .name;
+
+/*@ngInject*/
+function MultipleInputWidget() {
+ return {
+ restrict: "E",
+ scope: true,
+ bindToController: {
+ formId: '=',
+ ctx: '='
+ },
+ controller: MultipleInputWidgetController,
+ controllerAs: 'vm',
+ templateUrl: multipleInputWidgetTemplate
+ };
+}
+
+/*@ngInject*/
+function MultipleInputWidgetController($q, $scope, attributeService, toast, types, utils) {
+ var vm = this;
+
+ vm.dataKeyDetected = false;
+ vm.hasAnyChange = false;
+ vm.entityDetected = false;
+ vm.isValidParameter = true;
+ vm.message = 'No entity selected';
+
+ vm.rows = [];
+ vm.rowIndex = 0;
+
+ vm.datasources = null;
+
+ vm.cellStyle = cellStyle;
+ vm.textColor = textColor;
+ vm.discardAll = discardAll;
+ vm.inputChanged = inputChanged;
+ vm.postData = postData;
+
+ $scope.$watch('vm.ctx', function() {
+ if (vm.ctx && vm.ctx.defaultSubscription) {
+ vm.settings = vm.ctx.settings;
+ vm.widgetConfig = vm.ctx.widgetConfig;
+ vm.subscription = vm.ctx.defaultSubscription;
+ vm.datasources = vm.subscription.datasources;
+ initializeConfig();
+ updateDatasources();
+ }
+ });
+
+ $scope.$on('multiple-input-data-updated', function(event, formId) {
+ if (vm.formId == formId) {
+ updateRowData(vm.subscription.data);
+ $scope.$digest();
+ }
+ });
+
+ function defaultStyle() {
+ return {};
+ }
+
+ function cellStyle(key, rowIndex, firstKey, lastKey) {
+ var style = {};
+ if (key) {
+ var styleInfo = vm.stylesInfo[key.label];
+ var value = key.currentValue;
+ if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
+ try {
+ style = styleInfo.cellStyleFunction(value);
+ } catch (e) {
+ style = {};
+ }
+ } else {
+ style = defaultStyle();
+ }
+ }
+ if (vm.settings.rowMargin) {
+ if (angular.isUndefined(style.marginTop) && rowIndex != 0) {
+ style.marginTop = (vm.settings.rowMargin / 2) + 'px';
+ }
+ if (angular.isUndefined(style.marginBottom)) {
+ style.marginBottom = (vm.settings.rowMargin / 2) + 'px';
+ }
+ }
+ if (vm.settings.columnMargin) {
+ if (angular.isUndefined(style.marginLeft) && !firstKey) {
+ style.marginLeft = (vm.settings.columnMargin / 2) + 'px';
+ }
+ if (angular.isUndefined(style.marginRight) && !lastKey) {
+ style.marginRight = (vm.settings.columnMargin / 2) + 'px';
+ }
+ }
+ return style;
+ }
+
+ function textColor(key) {
+ var style = {};
+ if (key) {
+ var styleInfo = vm.stylesInfo[key.label];
+ if (styleInfo.color) {
+ style = { color: styleInfo.color };
+ }
+ }
+ return style;
+ }
+
+ function discardAll() {
+ for (var r = 0; r < vm.rows.length; r++) {
+ var row = vm.rows[r];
+ for (var d = 0; d < row.data.length; d++ ) {
+ row.data[d].currentValue = row.data[d].originalValue;
+ }
+ }
+ vm.hasAnyChange = false;
+ }
+
+ function inputChanged() {
+ var newValue = false;
+ for (var r = 0; r < vm.rows.length; r++) {
+ var row = vm.rows[r];
+ for (var d = 0; d < row.data.length; d++ ) {
+ if (!row.data[d].currentValue) {
+ return;
+ }
+ if (row.data[d].currentValue !== row.data[d].originalValue) {
+ newValue = true;
+ }
+ }
+ }
+ vm.hasAnyChange = newValue;
+ }
+
+ function postData() {
+ var promises = [];
+ for (var r = 0; r < vm.rows.length; r++) {
+ var row = vm.rows[r];
+ var datasource = row.datasource;
+ var attributes = [];
+ var newValues = false;
+
+ for (var d = 0; d < row.data.length; d++ ) {
+ if (row.data[d].currentValue !== row.data[d].originalValue) {
+ attributes.push({
+ key : row.data[d].name,
+ value : row.data[d].currentValue,
+ });
+ newValues = true;
+ }
+ }
+
+ if (newValues) {
+ promises.push(attributeService.saveEntityAttributes(
+ datasource.entityType,
+ datasource.entityId,
+ vm.attributeScope,
+ attributes));
+ }
+ }
+
+ if (promises.length) {
+ $q.all(promises).then(
+ function success() {
+ for (var d = 0; d < row.data.length; d++ ) {
+ row.data[d].originalValue = row.data[d].currentValue;
+ }
+ vm.hasAnyChange = false;
+ if (vm.settings.showResultMessage) {
+ toast.showSuccess('Update successful', 1000, angular.element(vm.ctx.$container), 'bottom left');
+ }
+ },
+ function fail() {
+ if (vm.settings.showResultMessage) {
+ toast.showError('Update failed', angular.element(vm.ctx.$container), 'bottom left');
+ }
+ }
+ );
+ }
+ }
+
+ function initializeConfig() {
+
+ if (vm.settings.widgetTitle && vm.settings.widgetTitle.length) {
+ vm.widgetTitle = utils.customTranslation(vm.settings.widgetTitle, vm.settings.widgetTitle);
+ } else {
+ vm.widgetTitle = vm.ctx.widgetConfig.title;
+ }
+
+ vm.ctx.widgetTitle = vm.widgetTitle;
+
+ vm.attributeScope = vm.settings.attributesShared ? types.attributesScope.shared.value : types.attributesScope.server.value;
+ }
+
+ function updateDatasources() {
+
+ vm.stylesInfo = {};
+ vm.rows = [];
+ vm.rowIndex = 0;
+
+ if (vm.datasources) {
+ vm.entityDetected = true;
+ for (var ds = 0; ds < vm.datasources.length; ds++) {
+ var row = {};
+ var datasource = vm.datasources[ds];
+ row.datasource = datasource;
+ row.data = [];
+ if (datasource.dataKeys) {
+ vm.dataKeyDetected = true;
+ for (var a = 0; a < datasource.dataKeys.length; a++ ) {
+ var dataKey = datasource.dataKeys[a];
+
+ if (dataKey.units) {
+ dataKey.label += ' (' + dataKey.units + ')';
+ }
+
+ var keySettings = dataKey.settings;
+ if (keySettings.inputTypeNumber) {
+ keySettings.inputType = 'number';
+ } else {
+ keySettings.inputType = 'text';
+ }
+
+ var cellStyleFunction = null;
+ var useCellStyleFunction = false;
+
+ if (keySettings.useCellStyleFunction === true) {
+ if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {
+ try {
+ cellStyleFunction = new Function('value', keySettings.cellStyleFunction);
+ useCellStyleFunction = true;
+ } catch (e) {
+ cellStyleFunction = null;
+ useCellStyleFunction = false;
+ }
+ }
+ }
+
+ vm.stylesInfo[dataKey.label] = {
+ useCellStyleFunction: useCellStyleFunction,
+ cellStyleFunction: cellStyleFunction,
+ color: keySettings.color
+ };
+
+ row.data.push(dataKey);
+ }
+ vm.rows.push(row);
+ }
+ }
+ }
+ }
+
+ function updateRowData(data) {
+ var dataIndex = 0;
+ for (var r = 0; r < vm.rows.length; r++) {
+ var row = vm.rows[r];
+ for (var d = 0; d < row.data.length; d++ ) {
+ var keyData = data[dataIndex++].data;
+ if (keyData && keyData.length && keyData[0].length > 1) {
+ row.data[d].currentValue = row.data[d].originalValue = keyData[0][1];
+ }
+ }
+ }
+ }
+
+}
diff --git a/ui/src/app/widget/lib/multiple-input-widget.scss b/ui/src/app/widget/lib/multiple-input-widget.scss
new file mode 100644
index 0000000000..7e16b3f967
--- /dev/null
+++ b/ui/src/app/widget/lib/multiple-input-widget.scss
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016-2019 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.tb-multiple-input {
+ height: 100%;
+
+ .md-button.md-icon-button {
+ width: 32px;
+ min-width: 32px;
+ height: 32px;
+ min-height: 32px;
+ padding: 0 !important;
+ margin: 0;
+ line-height: 20px;
+ }
+
+ .md-icon-button md-icon {
+ width: 20px;
+ min-width: 20px;
+ height: 20px;
+ min-height: 20px;
+ font-size: 20px;
+
+ &:not([disabled]) {
+ color: #f66;
+ }
+ }
+}
+
+md-toast {
+ min-width: 0;
+
+ .md-toast-content {
+ font-size: 14px !important;
+ }
+}
diff --git a/ui/src/app/widget/lib/multiple-input-widget.tpl.html b/ui/src/app/widget/lib/multiple-input-widget.tpl.html
new file mode 100644
index 0000000000..1263919825
--- /dev/null
+++ b/ui/src/app/widget/lib/multiple-input-widget.tpl.html
@@ -0,0 +1,68 @@
+
+