Commit 66f74e961eb7ff9dc5fae0cf606d3c3d2cab41d2
Merge branch 'mircopz-feature/new-multiple-input-widget'
Showing
15 changed files
with
604 additions
and
13 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 \"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); | ... | ... |
... | ... | @@ -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 | +} | ... | ... |
... | ... | @@ -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> | ... | ... |