Commit 66f74e961eb7ff9dc5fae0cf606d3c3d2cab41d2

Authored by Igor Kulikov
2 parents 2945655d 849f85f7

Merge branch 'mircopz-feature/new-multiple-input-widget'

... ... @@ -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 \"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",
  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
... ...
... ... @@ -22,13 +22,17 @@ import ReactSchemaForm from './react/json-form-react.jsx';
22 22 import jsonFormTemplate from './json-form.tpl.html';
23 23 import { utils } from 'react-schema-form';
24 24
  25 +import MaterialIconsDialogController from './material-icons-dialog.controller';
  26 +import materialIconsDialogTemplate from './material-icons-dialog.tpl.html';
  27 +
25 28 export default angular.module('thingsboard.directives.jsonForm', [])
26 29 .directive('tbJsonForm', JsonForm)
  30 + .controller('MaterialIconsDialogController', MaterialIconsDialogController)
27 31 .value('ReactSchemaForm', ReactSchemaForm)
28 32 .name;
29 33
30 34 /*@ngInject*/
31   -function JsonForm($compile, $templateCache, $mdColorPicker) {
  35 +function JsonForm($compile, $templateCache, $mdColorPicker, $mdDialog, $document) {
32 36
33 37 var linker = function (scope, element) {
34 38
... ... @@ -90,6 +94,9 @@ function JsonForm($compile, $templateCache, $mdColorPicker) {
90 94 onColorClick: function(event, key, val) {
91 95 scope.showColorPicker(event, val);
92 96 },
  97 + onIconClick: function(event) {
  98 + scope.openIconDialog(event);
  99 + },
93 100 onToggleFullscreen: function() {
94 101 scope.isFullscreen = !scope.isFullscreen;
95 102 scope.formProps.isFullscreen = scope.isFullscreen;
... ... @@ -123,6 +130,23 @@ function JsonForm($compile, $templateCache, $mdColorPicker) {
123 130 });
124 131 }
125 132
  133 + scope.openIconDialog = function(event) {
  134 + $mdDialog.show({
  135 + controller: 'MaterialIconsDialogController',
  136 + controllerAs: 'vm',
  137 + templateUrl: materialIconsDialogTemplate,
  138 + parent: angular.element($document[0].body),
  139 + locals: {icon: scope.icon},
  140 + multiple: true,
  141 + fullscreen: true,
  142 + targetEvent: event
  143 + }).then(function (icon) {
  144 + if (event.data && event.data.onValueChanged) {
  145 + event.data.onValueChanged(icon);
  146 + }
  147 + });
  148 + }
  149 +
126 150 scope.onFullscreenChanged = function() {}
127 151
128 152 scope.validate = function(){
... ...
... ... @@ -131,7 +131,7 @@ class ThingsboardArray extends React.Component {
131 131 }
132 132 let forms = this.props.form.items.map(function(form, index){
133 133 var copy = this.copyWithIndex(form, i);
134   - return this.props.builder(copy, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder);
  134 + 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);
135 135 }.bind(this));
136 136 arrays.push(
137 137 <li key={keys[i]} className="list-group-item">
... ...
... ... @@ -19,7 +19,7 @@ class ThingsboardFieldSet extends React.Component {
19 19
20 20 render() {
21 21 let forms = this.props.form.items.map(function(form, index){
22   - return this.props.builder(form, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder);
  22 + 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);
23 23 }.bind(this));
24 24
25 25 return (
... ...
  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 $ from 'jquery';
  17 +import React from 'react';
  18 +import ReactDOM from 'react-dom';
  19 +import ThingsboardBaseComponent from './json-form-base-component.jsx';
  20 +import reactCSS from 'reactcss';
  21 +import TextField from 'material-ui/TextField';
  22 +import IconButton from 'material-ui/IconButton';
  23 +
  24 +class ThingsboardIcon extends React.Component {
  25 +
  26 + constructor(props) {
  27 + super(props);
  28 + this.onValueChanged = this.onValueChanged.bind(this);
  29 + this.onIconClick = this.onIconClick.bind(this);
  30 + this.onClear = this.onClear.bind(this);
  31 + var icon = props.value ? props.value : '';
  32 + this.state = {
  33 + icon: icon
  34 + };
  35 + }
  36 +
  37 + componentDidMount() {
  38 + var node = ReactDOM.findDOMNode(this);
  39 + var iconContainer = $(node).children('#icon-container');
  40 + iconContainer.click(this, function(event) {
  41 + event.data.onIconClick(event);
  42 + });
  43 + }
  44 +
  45 + componentWillUnmount () {
  46 + var node = ReactDOM.findDOMNode(this);
  47 + var iconContainer = $(node).children('#icon-container');
  48 + iconContainer.off( "click" );
  49 + }
  50 +
  51 + onValueChanged(value) {
  52 + var icon = value;
  53 +
  54 + this.setState({
  55 + icon: value
  56 + })
  57 + this.props.onChange(this.props.form.key, value);
  58 + }
  59 +
  60 + onIconClick(event) {
  61 + this.props.onIconClick(event);
  62 + }
  63 +
  64 + onClear(event) {
  65 + if (event) {
  66 + event.stopPropagation();
  67 + }
  68 + this.onValueChanged('');
  69 + }
  70 +
  71 + render() {
  72 +
  73 + const styles = reactCSS({
  74 + 'default': {
  75 + clear: {
  76 + marginTop: '15px'
  77 + },
  78 + container: {
  79 + display: 'flex'
  80 + },
  81 + icon: {
  82 + display: 'inline-block',
  83 + marginRight: '10px',
  84 + marginTop: '16px',
  85 + marginBottom: 'auto',
  86 + cursor: 'pointer',
  87 + border: 'solid 1px rgba(0, 0, 0, .27)'
  88 + },
  89 + iconContainer: {
  90 + display: 'flex',
  91 + width: '100%'
  92 + },
  93 + iconText: {
  94 + display: 'inline-block',
  95 + width: '100%'
  96 + },
  97 + },
  98 + });
  99 +
  100 + var fieldClass = "tb-field";
  101 + if (this.props.form.required) {
  102 + fieldClass += " tb-required";
  103 + }
  104 + if (this.state.focused) {
  105 + fieldClass += " tb-focused";
  106 + }
  107 +
  108 + var pickedIcon = 'more_horiz';
  109 + if (this.state.icon != '') {
  110 + pickedIcon = this.state.icon;
  111 + }
  112 +
  113 + return (
  114 + <div style={ styles.container }>
  115 + <div id="icon-container" style={ styles.iconContainer }>
  116 + <IconButton iconClassName="material-icons" style={ styles.icon }>
  117 + {pickedIcon}
  118 + </IconButton>
  119 + <TextField
  120 + className={fieldClass}
  121 + floatingLabelText={this.props.form.title}
  122 + hintText={this.props.form.placeholder}
  123 + errorText={this.props.error}
  124 + value={this.state.icon}
  125 + disabled={this.props.form.readonly}
  126 + style={ styles.iconText } />
  127 + </div>
  128 + <IconButton iconClassName="material-icons" tooltip="Clear" onTouchTap={this.onClear}>clear</IconButton>
  129 + </div>
  130 + );
  131 + }
  132 +}
  133 +
  134 +export default ThingsboardBaseComponent(ThingsboardIcon);
... ...
... ... @@ -52,6 +52,7 @@ ReactSchemaForm.propTypes = {
52 52 option: React.PropTypes.object,
53 53 onModelChange: React.PropTypes.func,
54 54 onColorClick: React.PropTypes.func,
  55 + onIconClick: React.PropTypes.func,
55 56 onToggleFullscreen: React.PropTypes.func
56 57 }
57 58
... ...
... ... @@ -32,6 +32,7 @@ import ThingsboardImage from './json-form-image.jsx';
32 32 import ThingsboardCheckbox from './json-form-checkbox.jsx';
33 33 import Help from 'react-schema-form/lib/Help';
34 34 import ThingsboardFieldSet from './json-form-fieldset.jsx';
  35 +import ThingsboardIcon from './json-form-icon.jsx';
35 36
36 37 import _ from 'lodash';
37 38
... ... @@ -58,11 +59,13 @@ class ThingsboardSchemaForm extends React.Component {
58 59 'css': ThingsboardCss,
59 60 'color': ThingsboardColor,
60 61 'rc-select': ThingsboardRcSelect,
61   - 'fieldset': ThingsboardFieldSet
  62 + 'fieldset': ThingsboardFieldSet,
  63 + 'icon': ThingsboardIcon
62 64 };
63 65
64 66 this.onChange = this.onChange.bind(this);
65 67 this.onColorClick = this.onColorClick.bind(this);
  68 + this.onIconClick = this.onIconClick.bind(this);
66 69 this.onToggleFullscreen = this.onToggleFullscreen.bind(this);
67 70 this.hasConditions = false;
68 71 }
... ... @@ -79,12 +82,16 @@ class ThingsboardSchemaForm extends React.Component {
79 82 this.props.onColorClick(event, key, val);
80 83 }
81 84
  85 + onIconClick(event) {
  86 + this.props.onIconClick(event);
  87 + }
  88 +
82 89 onToggleFullscreen() {
83 90 this.props.onToggleFullscreen();
84 91 }
85 92
86 93
87   - builder(form, model, index, onChange, onColorClick, onToggleFullscreen, mapper) {
  94 + builder(form, model, index, onChange, onColorClick, onIconClick, onToggleFullscreen, mapper) {
88 95 var type = form.type;
89 96 let Field = this.mapper[type];
90 97 if(!Field) {
... ... @@ -97,7 +104,7 @@ class ThingsboardSchemaForm extends React.Component {
97 104 return null;
98 105 }
99 106 }
100   - return <Field model={model} form={form} key={index} onChange={onChange} onColorClick={onColorClick} onToggleFullscreen={onToggleFullscreen} mapper={mapper} builder={this.builder}/>
  107 + return <Field model={model} form={form} key={index} onChange={onChange} onColorClick={onColorClick} onIconClick={onIconClick} onToggleFullscreen={onToggleFullscreen} mapper={mapper} builder={this.builder}/>
101 108 }
102 109
103 110 createSchema(theForm) {
... ... @@ -107,7 +114,7 @@ class ThingsboardSchemaForm extends React.Component {
107 114 mapper = _.merge(this.mapper, this.props.mapper);
108 115 }
109 116 let forms = merged.map(function(form, index) {
110   - return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, this.onToggleFullscreen, mapper);
  117 + return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, this.onIconClick, this.onToggleFullscreen, mapper);
111 118 }.bind(this));
112 119
113 120 let formClass = 'SchemaForm';
... ... @@ -158,4 +165,4 @@ class ThingsboardSchemaGroup extends React.Component{
158 165 <div style={{padding: '20px'}} className={this.state.showGroup?"":"invisible"}>{this.props.forms}</div>
159 166 </section>);
160 167 }
161   -}
\ No newline at end of file
  168 +}
... ...
... ... @@ -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",
... ...
... ... @@ -49,6 +49,7 @@
49 49 "import": "Importar",
50 50 "export": "Exportar",
51 51 "share-via": "Compartir vía {{provider}}",
  52 + "discard-changes": "Cancelar los cambios",
52 53 "continue": "Continuar"
53 54 },
54 55 "aggregation": {
... ...
... ... @@ -48,7 +48,8 @@
48 48 "undo": "Annuler",
49 49 "update": "mise à jour",
50 50 "view": "Afficher",
51   - "yes": "Oui"
  51 + "yes": "Oui",
  52 + "discard-changes": "Annuler les modifications"
52 53 },
53 54 "admin": {
54 55 "base-url": "URL de base",
... ...
... ... @@ -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.textColor = textColor;
  60 + vm.discardAll = discardAll;
  61 + vm.inputChanged = inputChanged;
  62 + vm.postData = postData;
  63 +
  64 + $scope.$watch('vm.ctx', function() {
  65 + if (vm.ctx && vm.ctx.defaultSubscription) {
  66 + vm.settings = vm.ctx.settings;
  67 + vm.widgetConfig = vm.ctx.widgetConfig;
  68 + vm.subscription = vm.ctx.defaultSubscription;
  69 + vm.datasources = vm.subscription.datasources;
  70 + initializeConfig();
  71 + updateDatasources();
  72 + }
  73 + });
  74 +
  75 + $scope.$on('multiple-input-data-updated', function(event, formId) {
  76 + if (vm.formId == formId) {
  77 + updateRowData(vm.subscription.data);
  78 + $scope.$digest();
  79 + }
  80 + });
  81 +
  82 + function defaultStyle() {
  83 + return {};
  84 + }
  85 +
  86 + function cellStyle(key, rowIndex, firstKey, lastKey) {
  87 + var style = {};
  88 + if (key) {
  89 + var styleInfo = vm.stylesInfo[key.label];
  90 + var value = key.currentValue;
  91 + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
  92 + try {
  93 + style = styleInfo.cellStyleFunction(value);
  94 + } catch (e) {
  95 + style = {};
  96 + }
  97 + } else {
  98 + style = defaultStyle();
  99 + }
  100 + }
  101 + if (vm.settings.rowMargin) {
  102 + if (angular.isUndefined(style.marginTop) && rowIndex != 0) {
  103 + style.marginTop = (vm.settings.rowMargin / 2) + 'px';
  104 + }
  105 + if (angular.isUndefined(style.marginBottom)) {
  106 + style.marginBottom = (vm.settings.rowMargin / 2) + 'px';
  107 + }
  108 + }
  109 + if (vm.settings.columnMargin) {
  110 + if (angular.isUndefined(style.marginLeft) && !firstKey) {
  111 + style.marginLeft = (vm.settings.columnMargin / 2) + 'px';
  112 + }
  113 + if (angular.isUndefined(style.marginRight) && !lastKey) {
  114 + style.marginRight = (vm.settings.columnMargin / 2) + 'px';
  115 + }
  116 + }
  117 + return style;
  118 + }
  119 +
  120 + function textColor(key) {
  121 + var style = {};
  122 + if (key) {
  123 + var styleInfo = vm.stylesInfo[key.label];
  124 + if (styleInfo.color) {
  125 + style = { color: styleInfo.color };
  126 + }
  127 + }
  128 + return style;
  129 + }
  130 +
  131 + function discardAll() {
  132 + for (var r = 0; r < vm.rows.length; r++) {
  133 + var row = vm.rows[r];
  134 + for (var d = 0; d < row.data.length; d++ ) {
  135 + row.data[d].currentValue = row.data[d].originalValue;
  136 + }
  137 + }
  138 + vm.hasAnyChange = false;
  139 + }
  140 +
  141 + function inputChanged() {
  142 + var newValue = false;
  143 + for (var r = 0; r < vm.rows.length; r++) {
  144 + var row = vm.rows[r];
  145 + for (var d = 0; d < row.data.length; d++ ) {
  146 + if (!row.data[d].currentValue) {
  147 + return;
  148 + }
  149 + if (row.data[d].currentValue !== row.data[d].originalValue) {
  150 + newValue = true;
  151 + }
  152 + }
  153 + }
  154 + vm.hasAnyChange = newValue;
  155 + }
  156 +
  157 + function postData() {
  158 + var promises = [];
  159 + for (var r = 0; r < vm.rows.length; r++) {
  160 + var row = vm.rows[r];
  161 + var datasource = row.datasource;
  162 + var attributes = [];
  163 + var newValues = false;
  164 +
  165 + for (var d = 0; d < row.data.length; d++ ) {
  166 + if (row.data[d].currentValue !== row.data[d].originalValue) {
  167 + attributes.push({
  168 + key : row.data[d].name,
  169 + value : row.data[d].currentValue,
  170 + });
  171 + newValues = true;
  172 + }
  173 + }
  174 +
  175 + if (newValues) {
  176 + promises.push(attributeService.saveEntityAttributes(
  177 + datasource.entityType,
  178 + datasource.entityId,
  179 + vm.attributeScope,
  180 + attributes));
  181 + }
  182 + }
  183 +
  184 + if (promises.length) {
  185 + $q.all(promises).then(
  186 + function success() {
  187 + for (var d = 0; d < row.data.length; d++ ) {
  188 + row.data[d].originalValue = row.data[d].currentValue;
  189 + }
  190 + vm.hasAnyChange = false;
  191 + if (vm.settings.showResultMessage) {
  192 + toast.showSuccess('Update successful', 1000, angular.element(vm.ctx.$container), 'bottom left');
  193 + }
  194 + },
  195 + function fail() {
  196 + if (vm.settings.showResultMessage) {
  197 + toast.showError('Update failed', angular.element(vm.ctx.$container), 'bottom left');
  198 + }
  199 + }
  200 + );
  201 + }
  202 + }
  203 +
  204 + function initializeConfig() {
  205 +
  206 + if (vm.settings.widgetTitle && vm.settings.widgetTitle.length) {
  207 + vm.widgetTitle = utils.customTranslation(vm.settings.widgetTitle, vm.settings.widgetTitle);
  208 + } else {
  209 + vm.widgetTitle = vm.ctx.widgetConfig.title;
  210 + }
  211 +
  212 + vm.ctx.widgetTitle = vm.widgetTitle;
  213 +
  214 + vm.attributeScope = vm.settings.attributesShared ? types.attributesScope.shared.value : types.attributesScope.server.value;
  215 + }
  216 +
  217 + function updateDatasources() {
  218 +
  219 + vm.stylesInfo = {};
  220 + vm.rows = [];
  221 + vm.rowIndex = 0;
  222 +
  223 + if (vm.datasources) {
  224 + vm.entityDetected = true;
  225 + for (var ds = 0; ds < vm.datasources.length; ds++) {
  226 + var row = {};
  227 + var datasource = vm.datasources[ds];
  228 + row.datasource = datasource;
  229 + row.data = [];
  230 + if (datasource.dataKeys) {
  231 + vm.dataKeyDetected = true;
  232 + for (var a = 0; a < datasource.dataKeys.length; a++ ) {
  233 + var dataKey = datasource.dataKeys[a];
  234 +
  235 + if (dataKey.units) {
  236 + dataKey.label += ' (' + dataKey.units + ')';
  237 + }
  238 +
  239 + var keySettings = dataKey.settings;
  240 + if (keySettings.inputTypeNumber) {
  241 + keySettings.inputType = 'number';
  242 + } else {
  243 + keySettings.inputType = 'text';
  244 + }
  245 +
  246 + var cellStyleFunction = null;
  247 + var useCellStyleFunction = false;
  248 +
  249 + if (keySettings.useCellStyleFunction === true) {
  250 + if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {
  251 + try {
  252 + cellStyleFunction = new Function('value', keySettings.cellStyleFunction);
  253 + useCellStyleFunction = true;
  254 + } catch (e) {
  255 + cellStyleFunction = null;
  256 + useCellStyleFunction = false;
  257 + }
  258 + }
  259 + }
  260 +
  261 + vm.stylesInfo[dataKey.label] = {
  262 + useCellStyleFunction: useCellStyleFunction,
  263 + cellStyleFunction: cellStyleFunction,
  264 + color: keySettings.color
  265 + };
  266 +
  267 + row.data.push(dataKey);
  268 + }
  269 + vm.rows.push(row);
  270 + }
  271 + }
  272 + }
  273 + }
  274 +
  275 + function updateRowData(data) {
  276 + var dataIndex = 0;
  277 + for (var r = 0; r < vm.rows.length; r++) {
  278 + var row = vm.rows[r];
  279 + for (var d = 0; d < row.data.length; d++ ) {
  280 + var keyData = data[dataIndex++].data;
  281 + if (keyData && keyData.length && keyData[0].length > 1) {
  282 + row.data[d].currentValue = row.data[d].originalValue = keyData[0][1];
  283 + }
  284 + }
  285 + }
  286 + }
  287 +
  288 +}
... ...
  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 +}
... ...
  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)" novalidate>
  19 + <div style="padding: 0 8px; margin: auto 0;">
  20 + <div ng-show="vm.entityDetected" layout="row" flex ng-repeat="row in vm.rows" ng-init="rowIndex=$index">
  21 + <div layout="column" flex ng-repeat="key in row.data track by $index" ng-init="keyIndex=$index">
  22 + <md-tooltip class="tb-tooltip-multiline" ng-if="key.settings.tooltipMessage && key.settings.tooltipMessage.length" md-direction="left">
  23 + <span ng-bind-html="key.settings.tooltipMessage"></span>
  24 + </md-tooltip>
  25 + <md-input-container class="md-block" ng-style="vm.cellStyle(key, rowIndex, $first, $last)">
  26 + <label ng-style="vm.textColor(key)">{{key.label}}</label>
  27 + <md-icon ng-style="vm.textColor(key)" class="material-icons" ng-if="key.settings.icon">
  28 + {{key.settings.icon}}
  29 + </md-icon>
  30 + <input name="value{{rowIndex}}{{keyIndex}}"
  31 + ng-style="vm.textColor(key)"
  32 + ng-disabled="key.settings.readOnly"
  33 + ng-model="key.currentValue"
  34 + min="{{key.settings.min}}"
  35 + max="{{key.settings.max}}"
  36 + ng-required="key.settings.required"
  37 + type="{{key.settings.inputType}}"
  38 + step="{{key.settings.step}}"
  39 + md-select-on-focus
  40 + ng-change="vm.inputChanged()">
  41 + <div ng-messages="multipleInputForm['value' + rowIndex + keyIndex].$error">
  42 + <div ng-message="min">Value must be greater than {{key.settings.min}}</div>
  43 + <div ng-message="max">Value must be lower than {{key.settings.max}}</div>
  44 + <div ng-message="required">This field is required</div>
  45 + </div>
  46 + </md-input-container>
  47 + </div>
  48 + </div>
  49 +
  50 + <div style="text-align: center; font-size: 18px; color: #a0a0a0;" ng-hide="vm.entityDetected" ng-bind="vm.message"
  51 + ></div>
  52 + <div style="text-align: center; font-size: 18px; color: #a0a0a0;" ng-show="vm.entityDetected && !vm.dataKeyDetected">
  53 + No attribute is selected
  54 + </div>
  55 + <div style="text-align: center; font-size: 18px; color: #a0a0a0;" ng-show="vm.entityDetected && !vm.isValidParameter">
  56 + Timeseries parameter cannot be used in this widget
  57 + </div>
  58 + </div>
  59 + <div class="md-padding" layout="row" layout-align="end center" ng-show="vm.entityDetected && vm.dataKeyDetected">
  60 + <md-button class="md-primary" ng-click="vm.discardAll()" style="max-height: 50px;margin-right:20px;" ng-disabled="!vm.hasAnyChange">
  61 + {{ 'action.undo' | translate }}
  62 + </md-button>
  63 + <md-button class="md-raised md-primary" type="submit" value="Submit" style="max-height: 50px;margin-right:20px;"
  64 + ng-disabled="!vm.hasAnyChange || multipleInputForm.$invalid" ng-click="vm.isFocused = false">
  65 + {{ 'action.save' | translate }}
  66 + </md-button>
  67 + </div>
  68 +</form>
... ...